Changeset 0.26.0 (#165)

This commit is contained in:
2026-03-10 13:38:01 +00:00
parent dbc1a97343
commit 400f7dd176
48 changed files with 4923 additions and 208 deletions

View File

@@ -161,7 +161,7 @@ func (h *CompletionHandler) evaluateRouting(
Model: model,
ConfigID: configID,
HealthStatus: healthStatus,
Pricing: map[string]*models.ModelPricing{}, // TODO: populate from pricing store
Pricing: map[string]*models.ModelPricing{}, // populated when cost_limit routing is enabled (deferred)
}
ranked, dec := h.router.Evaluate(routingCtx, policies, candidates)
@@ -457,6 +457,18 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
return
}
// ── Workflow form template injection (v0.26.4) ──
// When the channel is a workflow instance, inject the current stage's
// form_template as a system message so the persona knows what to collect.
if channelType == "workflow" {
if hint := h.buildWorkflowFormHint(c.Request.Context(), channelID); hint != "" {
messages = append(messages[:1], append([]providers.Message{{
Role: "system",
Content: hint,
}}, messages[1:]...)...)
}
}
// Resolve capabilities early — needed for vision gating below
caps := h.getModelCapabilities(c, model, configID)
@@ -709,9 +721,9 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
}
if stream {
h.streamCompletion(c, provider, providerCfg, provReq, channelID, userID, model, providerID, configID, providerScope, personaID, workspaceID)
h.streamCompletion(c, provider, providerCfg, provReq, channelID, userID, model, providerID, configID, providerScope, personaID, workspaceID, teamID)
} else {
h.syncCompletion(c, provider, providerCfg, provReq, channelID, userID, model, providerID, configID, providerScope, personaID, workspaceID)
h.syncCompletion(c, provider, providerCfg, provReq, channelID, userID, model, providerID, configID, providerScope, personaID, workspaceID, teamID)
}
}
@@ -724,7 +736,7 @@ func (h *CompletionHandler) multiModelStream(
c *gin.Context,
targets []models.ChannelModel,
messages []providers.Message,
channelID, userID, personaID, personaSystemPrompt, workspaceID string,
channelID, userID, personaID, personaSystemPrompt, workspaceID, teamID string,
req completionRequest,
) {
// Set SSE headers once for the entire multi-model stream
@@ -821,7 +833,7 @@ func (h *CompletionHandler) multiModelStream(
}
// Stream this model's response using the shared streaming core
result := streamModelResponse(c, provider, providerCfg, &provReq, model, providerID, target.DisplayName, userID, channelID, personaID, workspaceID, configID, h.hub, h.health)
result := streamModelResponse(c, provider, providerCfg, &provReq, model, providerID, target.DisplayName, userID, channelID, personaID, workspaceID, configID, teamID, h.hub, h.health)
// Persist assistant message with model attribution
if result.Content != "" {
@@ -1012,14 +1024,14 @@ func (h *CompletionHandler) streamCompletion(
provider providers.Provider,
cfg providers.ProviderConfig,
req providers.CompletionRequest,
channelID, userID, model, providerID, configID, providerScope, personaID, workspaceID string,
channelID, userID, model, providerID, configID, providerScope, personaID, workspaceID, teamID string,
) {
// Apply provider-specific request hooks (v0.22.1)
if hooks := providers.GetHooks(providerID); hooks != nil {
hooks.PreRequest(cfg, &req)
}
result := streamWithToolLoop(c, provider, cfg, &req, model, providerID, userID, channelID, personaID, workspaceID, configID, h.hub, h.health)
result := streamWithToolLoop(c, provider, cfg, &req, model, providerID, userID, channelID, personaID, workspaceID, configID, teamID, h.hub, h.health)
// Persist assistant response
if result.Content != "" {
@@ -1052,7 +1064,7 @@ func (h *CompletionHandler) syncCompletion(
provider providers.Provider,
cfg providers.ProviderConfig,
req providers.CompletionRequest,
channelID, userID, model, providerID, configID, providerScope, personaID, workspaceID string,
channelID, userID, model, providerID, configID, providerScope, personaID, workspaceID, teamID string,
) {
// Apply provider-specific request hooks (v0.22.1)
if hooks := providers.GetHooks(providerID); hooks != nil {
@@ -1137,6 +1149,7 @@ func (h *CompletionHandler) syncCompletion(
ChannelID: channelID,
PersonaID: personaID,
WorkspaceID: workspaceID,
TeamID: teamID,
}
for _, tc := range resp.ToolCalls {
call := tools.ToolCall{
@@ -1335,6 +1348,71 @@ func (h *CompletionHandler) conversationHasPersonaMessages(ctx context.Context,
return err == nil && id != ""
}
// buildWorkflowFormHint loads the current workflow stage's form_template
// and returns a system message instructing the persona what to collect.
// Returns empty string if not a workflow or no template defined.
func (h *CompletionHandler) buildWorkflowFormHint(ctx context.Context, channelID string) string {
var workflowID *string
var currentStage int
_ = database.DB.QueryRowContext(ctx, database.Q(`
SELECT workflow_id, COALESCE(current_stage, 0)
FROM channels WHERE id = $1 AND type = 'workflow'
`), channelID).Scan(&workflowID, &currentStage)
if workflowID == nil || *workflowID == "" {
return ""
}
if h.stores.Workflows == nil {
return ""
}
stages, err := h.stores.Workflows.ListStages(ctx, *workflowID)
if err != nil || currentStage >= len(stages) {
return ""
}
stage := stages[currentStage]
if len(stage.FormTemplate) == 0 || string(stage.FormTemplate) == "{}" {
return ""
}
// Parse form template to extract field descriptions
var fields map[string]interface{}
if err := json.Unmarshal(stage.FormTemplate, &fields); err != nil {
return ""
}
if len(fields) == 0 {
return ""
}
hint := "You are in workflow stage: " + stage.Name + ".\n\n"
hint += "Collect the following information from the user through natural conversation:\n\n"
for name, spec := range fields {
switch v := spec.(type) {
case map[string]interface{}:
desc, _ := v["description"].(string)
required, _ := v["required"].(bool)
if desc != "" {
hint += "- " + name + ": " + desc
} else {
hint += "- " + name
}
if required {
hint += " (required)"
}
hint += "\n"
default:
hint += "- " + name + "\n"
}
}
hint += "\nWhen you have gathered all required information, call the workflow_advance tool " +
"with the collected data as a JSON object. Do not advance until all required fields are filled."
return hint
}
// buildParticipantHint builds a system message listing available @mentionable
// personas so the LLM knows who it can direct responses to.
// Only includes personas accessible to this user. Excludes the current persona.