Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions SELF_HOSTING_AI.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,29 @@ invoked directly is the reverse again — there the environment outranks `.env`.
startup output is read back from Docker Compose, so the address it prints is the
one the stack is actually published on.

## Optional: server-side LLM features

Chat auto-titling and chat follow-up suggestions ("quick actions") are produced
by a small server-side model call, separate from the agent runtimes. They are
off until the backend has an upstream configured:

```bash
# In .env — either the key or the base URL is enough to enable the layer.
MULTICA_LLM_API_KEY=sk-...
MULTICA_LLM_BASE_URL= # optional: any OpenAI-compatible gateway
MULTICA_LLM_DEFAULT_MODEL= # optional: defaults to a small built-in model
```

Both features degrade silently when this is unset — titles stay as the first
message and no follow-up suggestions appear — so confirm the state from the
backend's startup log rather than from the UI:

```bash
docker compose -f docker-compose.selfhost.yml logs backend | grep "llm layer"
# "llm layer enabled" -> configured
# "llm layer disabled" -> unset; the two features above will not appear
```

## Troubleshooting

- **Backend not ready:** `docker compose -f docker-compose.selfhost.yml logs backend`
Expand Down
7 changes: 7 additions & 0 deletions deploy/helm/multica/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ images:
# The chart references this Secret by name; it does not template it, so real
# values never need to land in git.
# -----------------------------------------------------------------------------
# Name of a pre-created Secret mounted onto the backend with envFrom, so any
# key in it becomes an environment variable. Server-side LLM features (chat
# auto-titling, chat follow-up suggestions) are configured here rather than in
# this file, because they are credentials: add MULTICA_LLM_API_KEY (and
# optionally MULTICA_LLM_BASE_URL / MULTICA_LLM_DEFAULT_MODEL) to this Secret.
# Without them the backend logs "llm layer disabled" at startup and both
# features stay silently off.
existingSecret: multica-secrets

# -----------------------------------------------------------------------------
Expand Down
8 changes: 8 additions & 0 deletions docker-compose.selfhost.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ services:
DISABLE_WORKSPACE_CREATION: ${DISABLE_WORKSPACE_CREATION:-}
GITHUB_APP_SLUG: ${GITHUB_APP_SLUG:-}
GITHUB_WEBHOOK_SECRET: ${GITHUB_WEBHOOK_SECRET:-}
# Server-internal LLM layer (MUL-4238). Backs chat auto-titling and chat
# follow-up suggestions. Setting either the key or the base URL enables
# it; leaving both empty disables those features silently. Without these
# three lines the vars in .env never reach the container, so the features
# cannot be turned on at all no matter what the operator configures.
MULTICA_LLM_API_KEY: ${MULTICA_LLM_API_KEY:-}
MULTICA_LLM_BASE_URL: ${MULTICA_LLM_BASE_URL:-}
MULTICA_LLM_DEFAULT_MODEL: ${MULTICA_LLM_DEFAULT_MODEL:-}
# Public URL the API is reachable at from the open internet, no
# trailing slash. Used to mint absolute webhook URLs for autopilot
# webhook triggers. Leave unset behind a same-origin reverse proxy
Expand Down
43 changes: 43 additions & 0 deletions server/internal/handler/chat_input_ownership_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"strings"
"testing"
"time"

"github.com/multica-ai/multica/server/internal/service"
db "github.com/multica-ai/multica/server/pkg/db/generated"
Expand Down Expand Up @@ -674,6 +675,48 @@ func TestChatQuickActions_ContextAnchorsOnTargetTurn(t *testing.T) {
}
}

// TestChatQuickActions_AutomaticPassEndToEnd exercises the path a real chat
// turn takes — CompleteTask decides eligibility, broadcasts, and starts the
// pass — rather than calling the generator directly. Everything between the
// completion callback and the pills landing on the row is only covered here.
func TestChatQuickActions_AutomaticPassEndToEnd(t *testing.T) {
if testHandler == nil {
t.Skip("database not available")
}
ctx := context.Background()
agentID, sessionID, _, _ := setupDirectChatSession(t, ctx, "quick-actions auto e2e")

taskID := sendDirectChat(t, ctx, agentID, sessionID, "what should I do next?")
markTaskRunning(t, ctx, taskID)

restore := installStubQuickActions()
defer restore()

if _, err := testHandler.TaskService.CompleteTask(
ctx, parseUUID(taskID), completeResult(t, "Here is the plan."), "", "", false, ""); err != nil {
t.Fatalf("complete task: %v", err)
}

// The pass runs on a detached goroutine; poll for its write.
var actions []protocol.ChatQuickAction
for i := 0; i < 100; i++ {
rows := assistantRows(t, ctx, sessionID)
if len(rows) == 1 {
actions = nil
if err := json.Unmarshal(rows[0].QuickActions, &actions); err != nil {
t.Fatalf("decode quick actions: %v", err)
}
if len(actions) > 0 {
break
}
}
time.Sleep(20 * time.Millisecond)
}
if len(actions) != 1 || actions[0].Label != "Next" {
t.Fatalf("automatic pass must attach suggestions to the completed turn, got %+v", actions)
}
}

// installStubQuickActions enables suggestion generation and returns the undo.
// Scoped tightly around the synchronous RegenerateChatQuickActions calls rather
// than installed for a whole test: CompleteTask starts a background pass when
Expand Down
11 changes: 11 additions & 0 deletions server/internal/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,17 @@ func New(queries *db.Queries, txStarter txStarter, hub *realtime.Hub, bus *event
DefaultModel: cfg.LLMDefaultModel,
})

// One boot-time line so an operator can tell configured-and-working apart
// from silently-off. Every consumer of this layer degrades quietly by
// design (auto-titling keeps the original title, chat follow-up suggestions
// simply never appear), which without this log makes an unset key
// indistinguishable from a broken feature.
if llmClient.Enabled() {
slog.Info("llm layer enabled", "default_model", llmClient.DefaultModel())
} else {
slog.Warn("llm layer disabled: set MULTICA_LLM_API_KEY or MULTICA_LLM_BASE_URL to enable chat auto-titling and chat follow-up suggestions")
}

taskSvc := service.NewTaskService(queries, txStarter, hub, bus, daemonHub)
taskSvc.Analytics = analyticsClient
// Chat follow-up suggestions run through the same internal LLM layer that
Expand Down
Loading