Skip to content
Merged
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
21 changes: 8 additions & 13 deletions core/taskengine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,23 +339,18 @@ func New(db storage.Storage, config *config.Config, queue *apqueue.Queue, logger
// Initialize AI summarizer (global) from aggregator config
// Only context-memory API is supported - all email content generation is delegated to context-memory
// The aggregator acts as a pass-through for the context-memory response to SendGrid
contextMemorySummarizer := NewContextMemorySummarizerFromAggregatorConfig(config)
contextMemorySummarizer, err := NewContextMemorySummarizerFromAggregatorConfig(config)
if err != nil {
// notifications.summary is enabled but misconfigured — refuse to boot rather than
// silently fall back, so a broken summarizer config surfaces loudly at startup.
logger.Fatal("Invalid notifications.summary configuration — refusing to start", "error", err)
}
Comment on lines +342 to +347
if contextMemorySummarizer != nil {
SetSummarizer(contextMemorySummarizer)
logger.Info("AI summarizer initialized", "provider", "context-memory", "base_url", config.NotificationsSummary.APIEndpoint)
} else {
// Log why context-memory is not available
if !config.NotificationsSummary.Enabled {
logger.Debug("Context-memory API not available: NotificationsSummary.Enabled is false")
} else if strings.ToLower(config.NotificationsSummary.Provider) != "context-memory" {
logger.Debug("Context-memory API not configured: provider is not 'context-memory'", "provider", config.NotificationsSummary.Provider)
} else if strings.TrimSpace(config.NotificationsSummary.APIEndpoint) == "" {
logger.Debug("Context-memory API not available: api_endpoint not configured in notifications.summary")
} else if strings.TrimSpace(config.NotificationsSummary.APIKey) == "" {
logger.Debug("Context-memory API not available: api_key not configured in notifications.summary")
}
// No summarizer configured - deterministic fallback will be used
logger.Debug("Context-memory API not configured, will use deterministic fallback")
// Summarization not enabled — the deterministic summarizer is used.
logger.Debug("notifications.summary.enabled is false; using deterministic summarizer")
}

// Initialize global macro variables and secrets from config
Expand Down
38 changes: 21 additions & 17 deletions core/taskengine/summarizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package taskengine

import (
"context"
"fmt"
"net/http"
"strings"
"time"
Expand Down Expand Up @@ -108,31 +109,34 @@ func ComposeSummarySmart(vm *VM, currentStepName string) Summary {
return s
}

// NewContextMemorySummarizerFromAggregatorConfig creates a ContextMemorySummarizer from aggregator config
// Reads api_endpoint and api_key from notifications.summary section
func NewContextMemorySummarizerFromAggregatorConfig(c *config.Config) Summarizer {
if c == nil {
return nil
}
if !c.NotificationsSummary.Enabled || strings.ToLower(c.NotificationsSummary.Provider) != "context-memory" {
return nil
}
baseURL := c.NotificationsSummary.APIEndpoint
if strings.TrimSpace(baseURL) == "" {
return nil
// NewContextMemorySummarizerFromAggregatorConfig builds the workflow summarizer from the
// aggregator's notifications.summary config (api_endpoint + api_key).
//
// Returns (nil, nil) when summarization is disabled — a valid "off" mode in which callers
// use the deterministic summarizer. Returns (nil, error) when summarization is ENABLED but
// misconfigured (unsupported provider, or empty endpoint/key) so the daemon fails fast at
// startup instead of silently degrading. There is no hardcoded endpoint default: the origin
// must come from config (avs-infra: ${SUMMARIZER_API_URL} / ${SUMMARIZER_API_KEY}).
func NewContextMemorySummarizerFromAggregatorConfig(c *config.Config) (Summarizer, error) {
Comment on lines +112 to +120
if c == nil || !c.NotificationsSummary.Enabled {
return nil, nil // Not enabled — deterministic fallback is used.
}
authToken := c.NotificationsSummary.APIKey
if strings.TrimSpace(authToken) == "" {
return nil
if strings.ToLower(c.NotificationsSummary.Provider) != "context-memory" {
return nil, fmt.Errorf("notifications.summary.enabled is true but provider %q is unsupported (expected \"context-memory\")", c.NotificationsSummary.Provider)
}
baseURL := strings.TrimSpace(c.NotificationsSummary.APIEndpoint)
if baseURL == "" {
baseURL = ContextAPIURL
return nil, fmt.Errorf("notifications.summary.enabled is true but api_endpoint is empty (set SUMMARIZER_API_URL to the Studio origin, e.g. https://app.avaprotocol.org)")
}
Comment on lines +127 to +130
authToken := strings.TrimSpace(c.NotificationsSummary.APIKey)
if authToken == "" {
return nil, fmt.Errorf("notifications.summary.enabled is true but api_key is empty (set SUMMARIZER_API_KEY)")
}
return &ContextMemorySummarizer{
baseURL: baseURL,
authToken: authToken,
httpClient: &http.Client{Timeout: 30 * time.Second},
}
}, nil
}

// FormatForMessageChannels converts a Summary into a concise chat message
Expand Down
18 changes: 7 additions & 11 deletions core/taskengine/summarizer_context_memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,20 @@ import (
"github.com/ethereum/go-ethereum/common"
)

const (
// ContextAPIURL is the production URL for the context-api service
ContextAPIURL = "https://context-api.avaprotocol.org"
)

// ContextMemorySummarizer implements Summarizer using the context-memory API
// ContextMemorySummarizer implements Summarizer by posting to the workflow-summary
// endpoint (Studio's /api/summarize; the standalone context-memory service is deprecated).
type ContextMemorySummarizer struct {
baseURL string
authToken string
httpClient *http.Client
}

// NewContextMemorySummarizer creates a new summarizer that calls context-memory API
// baseURL defaults to ContextAPIURL (production) if empty
// NewContextMemorySummarizer creates a summarizer that posts to {baseURL}/api/summarize.
// baseURL MUST be a bare origin (no "/api/summarize" path and no trailing slash) — the
// client appends the path itself; including it would double the path. No default is
// applied: the origin must be supplied explicitly (production wiring fails fast at
// startup when it is missing, see NewContextMemorySummarizerFromAggregatorConfig).
func NewContextMemorySummarizer(baseURL, authToken string) Summarizer {
if baseURL == "" {
baseURL = ContextAPIURL
}
return &ContextMemorySummarizer{
baseURL: baseURL,
authToken: authToken,
Expand Down
6 changes: 3 additions & 3 deletions core/taskengine/summarizer_context_memory_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ type EdgeDefinition struct {
Target string `json:"target"`
}

// getContextMemoryURL returns the base URL for context-memory API
// Uses CONTEXT_MEMORY_URL env var if set, otherwise defaults to production URL from source code
// getContextMemoryURL returns the base URL for the summarizer API in tests.
// Uses CONTEXT_MEMORY_URL env var if set, otherwise the production origin above.
func getContextMemoryURL() string {
if url := os.Getenv("CONTEXT_MEMORY_URL"); url != "" {
return url
}
return ContextAPIURL
return defaultSummarizerURL
}

// baseURL shared across tests to avoid redeclaration issues
Expand Down
10 changes: 8 additions & 2 deletions core/taskengine/summarizer_format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import (
const (
// Default auth token for local context-memory tests
ContextMemoryAuthToken = "test-auth-token-12345"

// defaultSummarizerURL is the production Studio origin, used ONLY as a convenience
// fallback in tests when CONTEXT_MEMORY_URL is unset. Production code has no hardcoded
// default — it reads the origin from notifications.summary.api_endpoint and fails fast
// at startup if it is missing.
defaultSummarizerURL = "https://app.avaprotocol.org"
)

type fakeSummarizer struct {
Expand Down Expand Up @@ -61,7 +67,7 @@ func TestComposeSummarySmart_UsesAISummarizer(t *testing.T) {
// Use real context-memory API
baseURL := os.Getenv("CONTEXT_MEMORY_URL")
if baseURL == "" {
baseURL = ContextAPIURL // Default to production URL from source code
baseURL = defaultSummarizerURL // Test-only fallback when CONTEXT_MEMORY_URL is unset
}
t.Logf("Using real context-memory API at: %s", baseURL)
summarizer := NewContextMemorySummarizer(baseURL, authToken)
Expand Down Expand Up @@ -523,7 +529,7 @@ func TestComposeSummarySmart_WithRealWorkflowState(t *testing.T) {
// Use real context-memory API (defaults to localhost:3000)
baseURL := os.Getenv("CONTEXT_MEMORY_URL")
if baseURL == "" {
baseURL = ContextAPIURL // Default to production URL from source code
baseURL = defaultSummarizerURL // Test-only fallback when CONTEXT_MEMORY_URL is unset
}
t.Logf("Using real context-memory API at: %s", baseURL)
summarizer := NewContextMemorySummarizer(baseURL, authToken)
Expand Down
5 changes: 4 additions & 1 deletion core/taskengine/vm_runner_rest_sendgrid_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ func TestSendGridEmailWithContextMemoryResponse(t *testing.T) {
}
// Override context_api_endpoint to use mock server
mockConfig.MacroSecrets["context_api_endpoint"] = contextMemoryServer.URL
summarizer := NewContextMemorySummarizerFromAggregatorConfig(&mockConfig)
summarizer, err := NewContextMemorySummarizerFromAggregatorConfig(&mockConfig)
Comment on lines 85 to +87
if err != nil {
t.Fatalf("Failed to create context-memory summarizer from config: %v", err)
}
if summarizer == nil {
t.Fatal("Failed to create context-memory summarizer from config")
}
Expand Down
Loading