Changeset 0.13.1 (#65)

This commit is contained in:
2026-02-25 23:56:27 +00:00
parent 8292a6efa8
commit 113b98ace4
24 changed files with 1565 additions and 24 deletions

View File

@@ -17,6 +17,7 @@ import (
"git.gobha.me/xcaliber/chat-switchboard/models"
"git.gobha.me/xcaliber/chat-switchboard/providers"
"git.gobha.me/xcaliber/chat-switchboard/store"
"git.gobha.me/xcaliber/chat-switchboard/tools/search"
)
type AdminHandler struct {
@@ -260,9 +261,32 @@ func (h *AdminHandler) UpdateGlobalSetting(c *gin.Context) {
}
h.auditLog(c, "settings.update", "global_settings", key, nil)
// Live-apply hooks for settings that have runtime effects
if key == "search_config" {
applySearchConfig(jsonVal)
}
c.JSON(http.StatusOK, gin.H{"message": "setting updated"})
}
// applySearchConfig updates the active search provider at runtime.
func applySearchConfig(raw models.JSONMap) {
b, err := json.Marshal(raw)
if err != nil {
log.Printf("⚠️ Failed to marshal search config: %v", err)
return
}
var cfg search.Config
if err := json.Unmarshal(b, &cfg); err != nil {
log.Printf("⚠️ Failed to parse search config: %v", err)
return
}
if err := search.ApplyConfig(cfg); err != nil {
log.Printf("⚠️ Failed to apply search config: %v", err)
}
}
func (h *AdminHandler) PublicSettings(c *gin.Context) {
// Banner config, branding, etc. — safe subset for non-admin users
banner, _ := h.stores.GlobalConfig.Get(c.Request.Context(), "banner")

View File

@@ -38,6 +38,7 @@ type completionRequest struct {
TopP *float64 `json:"top_p,omitempty"`
Stream *bool `json:"stream,omitempty"`
AttachmentIDs []string `json:"attachment_ids,omitempty"` // staged attachment UUIDs to include in request
DisabledTools []string `json:"disabled_tools,omitempty"` // tool names to exclude from this request
}
// CompletionHandler proxies LLM requests through the backend.
@@ -207,7 +208,7 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
// Attach tool definitions if model supports tool calling and tools are available
hasBrowserTools := h.hub != nil && h.hub.IsConnected(userID)
if caps.ToolCalling && (tools.HasTools() || hasBrowserTools) {
provReq.Tools = h.buildToolDefs(c.Request.Context(), userID, hasBrowserTools)
provReq.Tools = h.buildToolDefs(c.Request.Context(), userID, hasBrowserTools, req.DisabledTools)
}
// Determine streaming
@@ -225,8 +226,15 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
// buildToolDefs converts registered tools to provider-format tool definitions.
// If includeBrowser is true, also fetches tool schemas from enabled browser extensions.
func (h *CompletionHandler) buildToolDefs(ctx context.Context, userID string, includeBrowser bool) []providers.ToolDef {
allTools := tools.AllDefinitions()
// Tools whose names appear in disabledTools are excluded.
func (h *CompletionHandler) buildToolDefs(ctx context.Context, userID string, includeBrowser bool, disabledTools []string) []providers.ToolDef {
// Build disabled set for O(1) lookup
disabled := make(map[string]bool, len(disabledTools))
for _, name := range disabledTools {
disabled[name] = true
}
allTools := tools.AllDefinitionsFiltered(disabled)
defs := make([]providers.ToolDef, 0, len(allTools))
for _, t := range allTools {
defs = append(defs, providers.ToolDef{
@@ -262,6 +270,9 @@ func (h *CompletionHandler) buildToolDefs(ctx context.Context, userID string, in
continue
}
for _, t := range manifest.Tools {
if disabled[t.Name] {
continue
}
defs = append(defs, providers.ToolDef{
Type: "function",
Function: providers.FunctionDef{
@@ -277,6 +288,65 @@ func (h *CompletionHandler) buildToolDefs(ctx context.Context, userID string, in
return defs
}
// ── List Available Tools ────────────────────
// GET /api/v1/tools
//
// Returns all registered server-side tools plus browser extension tools
// for the current user. Used by the frontend to build the tools toggle menu.
type toolInfo struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
Category string `json:"category"`
}
func (h *CompletionHandler) ListTools(c *gin.Context) {
userID := c.GetString("user_id")
// Server-side tools
allDefs := tools.AllDefinitions()
result := make([]toolInfo, 0, len(allDefs))
for _, d := range allDefs {
result = append(result, toolInfo{
Name: d.Name,
DisplayName: d.DisplayName,
Description: d.Description,
Category: d.Category,
})
}
// Browser extension tools
if h.hub != nil && h.hub.IsConnected(userID) {
exts, err := h.stores.Extensions.ListForUser(c.Request.Context(), userID)
if err == nil {
for _, ext := range exts {
if ext.Tier != "browser" {
continue
}
var manifest struct {
Tools []struct {
Name string `json:"name"`
Description string `json:"description"`
} `json:"tools"`
}
if err := json.Unmarshal(ext.Manifest, &manifest); err != nil {
continue
}
for _, t := range manifest.Tools {
result = append(result, toolInfo{
Name: t.Name,
Description: t.Description,
Category: "browser",
})
}
}
}
}
c.JSON(http.StatusOK, gin.H{"data": result})
}
// ── Streaming Completion (SSE) with Tool Loop ──
const maxToolIterations = 10 // safety limit on tool call rounds

View File

@@ -46,11 +46,12 @@ type editRequest struct {
}
type regenerateRequest struct {
Model string `json:"model,omitempty"`
PresetID string `json:"preset_id,omitempty"`
APIConfigID string `json:"provider_config_id,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
Model string `json:"model,omitempty"`
PresetID string `json:"preset_id,omitempty"`
APIConfigID string `json:"provider_config_id,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
DisabledTools []string `json:"disabled_tools,omitempty"`
}
type cursorRequest struct {
@@ -455,7 +456,7 @@ func (h *MessageHandler) Regenerate(c *gin.Context) {
// Attach tool definitions (same as normal completion)
hasBrowserTools := h.hub != nil && h.hub.IsConnected(userID)
if caps.ToolCalling && (tools.HasTools() || hasBrowserTools) {
provReq.Tools = comp.buildToolDefs(c.Request.Context(), userID, hasBrowserTools)
provReq.Tools = comp.buildToolDefs(c.Request.Context(), userID, hasBrowserTools, req.DisabledTools)
}
// ── Stream the response (shared loop handles tools, reasoning, SSE) ──