feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
+132
-15
@@ -6,6 +6,7 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"data-terminal-cloud/internal/alertnotification"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"data-terminal-cloud/internal/audit"
|
||||
"data-terminal-cloud/internal/billing"
|
||||
"data-terminal-cloud/internal/cache"
|
||||
"data-terminal-cloud/internal/callmemory"
|
||||
"data-terminal-cloud/internal/config"
|
||||
"data-terminal-cloud/internal/contentmodel"
|
||||
"data-terminal-cloud/internal/contentpolicy"
|
||||
@@ -20,8 +22,10 @@ import (
|
||||
"data-terminal-cloud/internal/dataexport"
|
||||
"data-terminal-cloud/internal/device"
|
||||
"data-terminal-cloud/internal/deviceauth"
|
||||
"data-terminal-cloud/internal/devicefleet"
|
||||
"data-terminal-cloud/internal/devicereset"
|
||||
"data-terminal-cloud/internal/diagnostics"
|
||||
"data-terminal-cloud/internal/elderagent"
|
||||
"data-terminal-cloud/internal/embeddingprovider"
|
||||
"data-terminal-cloud/internal/externalprobe"
|
||||
"data-terminal-cloud/internal/extraction"
|
||||
@@ -31,6 +35,7 @@ import (
|
||||
"data-terminal-cloud/internal/idempotency"
|
||||
"data-terminal-cloud/internal/identity"
|
||||
"data-terminal-cloud/internal/media"
|
||||
"data-terminal-cloud/internal/memoir"
|
||||
"data-terminal-cloud/internal/memoryindex"
|
||||
"data-terminal-cloud/internal/memorylifecycle"
|
||||
"data-terminal-cloud/internal/memorytool"
|
||||
@@ -85,6 +90,42 @@ func Run(ctx context.Context, serviceName string) error {
|
||||
var memoryEmbedding embeddingprovider.Provider
|
||||
var providerMemoryClient providermemory.Client
|
||||
var vikingMemory *vikingmemory.Service
|
||||
callMemoryService := callmemory.NewService(databasePool, logger)
|
||||
if bucket := cfg.TOSRecordingBucket; bucket != "" && serviceName == "api" {
|
||||
recordings, recordingErr := callmemory.NewTOSRecordings(bucket, cfg.TOSRecordingRegion, cfg.VolcAccessKeyID, cfg.VolcSecretAccessKey, cfg.VolcSessionToken)
|
||||
if recordingErr != nil {
|
||||
return fmt.Errorf("configure cloud recordings: %w", recordingErr)
|
||||
}
|
||||
if err := recordings.ConfigurePlayback(cfg.TOSRecordingPlaybackEndpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
callMemoryService.ConfigureRecordings(recordings)
|
||||
if account := cfg.TOSRecordingAccountID; account != "" {
|
||||
callMemoryService.ConfigureMixedRecorder(callmemory.NewMixedRecorder(cfg.VolcAccessKeyID, cfg.VolcSecretAccessKey, cfg.VolcSessionToken, account, bucket))
|
||||
}
|
||||
}
|
||||
var directTemplate []byte
|
||||
directTemplatePath := os.Getenv("DIRECT_CHAT_TEMPLATE_FILE")
|
||||
if directTemplatePath == "" {
|
||||
directTemplatePath = cfg.VolcVoiceChatTemplate
|
||||
}
|
||||
if directTemplatePath != "" {
|
||||
directTemplate, err = os.ReadFile(directTemplatePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read direct chat template: %w", err)
|
||||
}
|
||||
}
|
||||
if len(directTemplate) > 0 {
|
||||
directTemplate, err = cfg.ApplyVolcVoiceTemplate(directTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
callMemoryService.ConfigureDirect(callmemory.DirectOptions{Template: directTemplate, BotID: cfg.VolcHardwareAgentBotID, CallbackURL: cfg.SubtitleCallbackURL, CallbackSecret: []byte(cfg.SubtitleCallbackSecret)})
|
||||
var elderAgents *elderagent.Service
|
||||
if len(directTemplate) > 0 && (serviceName == "api" || serviceName == "worker") {
|
||||
elderAgents = &elderagent.Service{Pool: databasePool, Provider: elderagent.NewClient(cfg.VolcAccessKeyID, cfg.VolcSecretAccessKey, cfg.VolcSessionToken, cfg.VolcRTCBaseURL, cfg.VolcRTCRegion).ForProduct(cfg.VolcHardwareInstanceID, cfg.VolcHardwareProductKey), Template: directTemplate, CallbackURL: cfg.SubtitleCallbackURL, CallbackSecret: []byte(cfg.SubtitleCallbackSecret), VikingCollectionName: cfg.VikingCollectionName, VikingEventType: cfg.VikingEventType}
|
||||
}
|
||||
if serviceName == "api" || serviceName == "worker" {
|
||||
if cfg.VikingResourceID != "" {
|
||||
client, vikingErr := vikingmemory.NewClient(vikingmemory.Options{ResourceID: cfg.VikingResourceID, CollectionName: cfg.VikingCollectionName, APIKey: cfg.VikingAPIKey, EventType: cfg.VikingEventType, ProfileType: cfg.VikingProfileType, Timeout: 8 * time.Second})
|
||||
@@ -92,6 +133,10 @@ func Run(ctx context.Context, serviceName string) error {
|
||||
return fmt.Errorf("configure Viking memory: %w", vikingErr)
|
||||
}
|
||||
vikingMemory = vikingmemory.NewService(databasePool, client)
|
||||
callMemoryService.ConfigureVikingConsole(client)
|
||||
managedMemory := vikingmemory.NewManaged(databasePool, client)
|
||||
callMemoryService.ConfigureProjection(managedMemory)
|
||||
callMemoryService.ConfigureManagedSync(managedMemory.SyncNext)
|
||||
}
|
||||
memoryEmbedding, err = newMemoryEmbeddingProvider(cfg)
|
||||
if err != nil {
|
||||
@@ -153,6 +198,7 @@ func Run(ctx context.Context, serviceName string) error {
|
||||
var memoryToolSigner *memorytool.TokenSigner
|
||||
var contentModelService *contentmodel.Service
|
||||
var contentModelRegistry *contentmodel.Registry
|
||||
var memoirService *memoir.Service
|
||||
var hardwareProvisioningService *hardwareprovision.Service
|
||||
var deviceFactoryResetService *devicereset.Service
|
||||
if serviceName == "api" || serviceName == "worker" {
|
||||
@@ -167,6 +213,20 @@ func Run(ctx context.Context, serviceName string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("create content model registry: %w", err)
|
||||
}
|
||||
memoirService = &memoir.Service{Pool: databasePool, Model: cfg.ArkMemoirModel, Safety: contentpolicy.LocalClassifier{}}
|
||||
if cfg.ArkAPIKey != "" {
|
||||
memoirService.Generator, err = extraction.NewArkJSONGenerator(cfg.ArkBaseURL, cfg.ArkAPIKey, cfg.ArkMemoirModel, 180*time.Second, 65536)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
memoirService.Illustrator = memoir.NewIllustrator(cfg.ArkBaseURL, cfg.ArkAPIKey, cfg.ArkMemoirImageModel)
|
||||
}
|
||||
if cfg.DifyEnabled {
|
||||
memoirService.Workflow, err = memoir.NewDifyWorkflow(cfg.DifyBaseURL, cfg.DifyMemoirAPIKey, cfg.DifyBridgeURL, cfg.DifyBridgeSecret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if serviceName == "api" || serviceName == "callbacks" {
|
||||
diagnosticService, err = diagnostics.NewService(databasePool, idempotency.NewExecutor(databasePool), audit.NewRepository())
|
||||
@@ -194,6 +254,10 @@ func Run(ctx context.Context, serviceName string) error {
|
||||
}
|
||||
saasService = saas.NewService(databasePool, audit.NewRepository(), redisClient, cursorCodec)
|
||||
saasService.ConfigureProviderMemory(cfg.ProviderMemoryMode == "managed_http")
|
||||
if elderAgents != nil {
|
||||
saasService.ConfigureVoiceAgents(elderAgents)
|
||||
callMemoryService.ConfigureDirect(callmemory.DirectOptions{Prepare: elderAgents.Prepare, PrepareWithWelcome: elderAgents.PrepareWithWelcome, CallbackURL: cfg.SubtitleCallbackURL, CallbackSecret: []byte(cfg.SubtitleCallbackSecret)})
|
||||
}
|
||||
opsConfigService, err = opsconfig.NewService(databasePool, audit.NewRepository())
|
||||
if err != nil {
|
||||
return fmt.Errorf("create operations configuration service: %w", err)
|
||||
@@ -235,7 +299,10 @@ func Run(ctx context.Context, serviceName string) error {
|
||||
return fmt.Errorf("import legacy device credentials: %w", err)
|
||||
}
|
||||
}
|
||||
opsDeviceService, err = opsdevice.NewService(databasePool, deviceSecretResolver, audit.NewRepository())
|
||||
opsDeviceService, err = opsdevice.NewService(databasePool, deviceSecretResolver, audit.NewRepository(), opsdevice.Options{
|
||||
OnboardingMasterKey: cfg.Onboarding.MasterKey,
|
||||
DeviceOrigin: cfg.Onboarding.EntryURL,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("create operations device service: %w", err)
|
||||
}
|
||||
@@ -349,12 +416,24 @@ func Run(ctx context.Context, serviceName string) error {
|
||||
if (serviceName == "api" || serviceName == "worker") && cfg.Onboarding.MasterKey != "" {
|
||||
var sms onboarding.SMSProvider
|
||||
if cfg.Onboarding.SMSLoginEnabled {
|
||||
sms, err = onboarding.NewHTTPSMSProvider(cfg.Onboarding.SMSGatewayURL, cfg.Onboarding.SMSGatewayToken)
|
||||
if cfg.Onboarding.SMSProvider == "tencent" {
|
||||
if cfg.Onboarding.TencentSMSTemplates != nil {
|
||||
sms, err = onboarding.NewTencentSMSProviderWithTemplates(cfg.Onboarding.TencentSMSSecretID, cfg.Onboarding.TencentSMSSecretKey, cfg.Onboarding.TencentSMSAppID, cfg.Onboarding.TencentSMSSignName, cfg.Onboarding.TencentSMSTemplates, cfg.Onboarding.TencentSMSTemplateParams)
|
||||
} else {
|
||||
sms, err = onboarding.NewTencentSMSProvider(cfg.Onboarding.TencentSMSSecretID, cfg.Onboarding.TencentSMSSecretKey, cfg.Onboarding.TencentSMSAppID, cfg.Onboarding.TencentSMSSignName, cfg.Onboarding.TencentSMSTemplateID)
|
||||
}
|
||||
} else {
|
||||
sms, err = onboarding.NewHTTPSMSProvider(cfg.Onboarding.SMSGatewayURL, cfg.Onboarding.SMSGatewayToken)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
onboardingService, err = onboarding.NewService(databasePool, redisClient, onboarding.Options{Enabled: cfg.Onboarding.Enabled, PauseClaims: cfg.Onboarding.PauseClaims, MasterKey: cfg.Onboarding.MasterKey, EntryURL: cfg.Onboarding.EntryURL, AllowLocalHTTP: cfg.Onboarding.AllowLocalHTTP, TrustedProxyCIDRs: cfg.Onboarding.TrustedProxyCIDRs, TokenVerifier: tokenVerifier, LegacyVerifier: deviceVerifier, ActivationEnabled: cfg.Onboarding.ActivationEnabled, ProviderMasterKey: cfg.VolcHardwareDeviceCredentialMasterKey, InstanceID: cfg.VolcHardwareInstanceID, ProductKey: cfg.VolcHardwareProductKey, ProductSecret: cfg.VolcHardwareProductSecret, BotID: cfg.VolcHardwareAgentBotID, Transport: cfg.VolcHardwareTransport, MinimumSDKVersion: cfg.VolcHardwareMinimumSDKVersion, SMS: sms, SMSLoginEnabled: cfg.Onboarding.SMSLoginEnabled, SelfSignupTenantIDs: cfg.Onboarding.SelfSignupTenantIDs, Logger: logger})
|
||||
captchaProvider, captchaErr := onboarding.NewTencentCaptchaProvider(cfg.Onboarding.TencentCaptcha)
|
||||
if captchaErr != nil {
|
||||
return captchaErr
|
||||
}
|
||||
onboardingService, err = onboarding.NewService(databasePool, redisClient, onboarding.Options{SaaSCaptchaDisabled: cfg.Onboarding.SaaSCaptchaDisabled, Captcha: captchaProvider, Enabled: cfg.Onboarding.Enabled, PauseClaims: cfg.Onboarding.PauseClaims, MasterKey: cfg.Onboarding.MasterKey, EntryURL: cfg.Onboarding.EntryURL, AllowLocalHTTP: cfg.Onboarding.AllowLocalHTTP, TrustedProxyCIDRs: cfg.Onboarding.TrustedProxyCIDRs, TokenVerifier: tokenVerifier, LegacyVerifier: deviceVerifier, ActivationEnabled: cfg.Onboarding.ActivationEnabled, ProviderMasterKey: cfg.VolcHardwareDeviceCredentialMasterKey, InstanceID: cfg.VolcHardwareInstanceID, ProductKey: cfg.VolcHardwareProductKey, ProductSecret: cfg.VolcHardwareProductSecret, BotID: cfg.VolcHardwareAgentBotID, Transport: cfg.VolcHardwareTransport, MinimumSDKVersion: cfg.VolcHardwareMinimumSDKVersion, SMS: sms, WeChatAppID: cfg.Onboarding.WeChatAppID, WeChatAppSecret: cfg.Onboarding.WeChatAppSecret, SMSLoginEnabled: cfg.Onboarding.SMSLoginEnabled, SelfSignupTenantIDs: cfg.Onboarding.SelfSignupTenantIDs, Logger: logger})
|
||||
if err != nil {
|
||||
return fmt.Errorf("create onboarding service: %w", err)
|
||||
}
|
||||
@@ -362,7 +441,34 @@ func Run(ctx context.Context, serviceName string) error {
|
||||
deviceVerifier = onboardingService
|
||||
}
|
||||
}
|
||||
if serviceName == "api" && saasService != nil {
|
||||
downloadBaseURL := strings.TrimRight(strings.TrimSpace(os.Getenv("FIRMWARE_DOWNLOAD_BASE_URL")), "/")
|
||||
if downloadBaseURL == "" {
|
||||
scheme := "http"
|
||||
if cfg.ObjectStorePublicSecure {
|
||||
scheme = "https"
|
||||
}
|
||||
downloadBaseURL = scheme + "://" + cfg.ObjectStorePublic
|
||||
}
|
||||
fleet := &devicefleet.Service{Pool: databasePool, Store: exportStore, BrokerURL: os.Getenv("DEVICE_MQTT_PUBLIC_URL"), DownloadBaseURL: downloadBaseURL}
|
||||
address := os.Getenv("DEVICE_MQTT_ADDRESS")
|
||||
if address == "" {
|
||||
address = ":1883"
|
||||
}
|
||||
closeBroker, fleetErr := fleet.StartBroker(ctx, address, logger)
|
||||
if fleetErr != nil {
|
||||
return fmt.Errorf("start device MQTT: %w", fleetErr)
|
||||
}
|
||||
defer closeBroker()
|
||||
saasService.Fleet = fleet
|
||||
if onboardingService != nil {
|
||||
onboardingService.Fleet = fleet
|
||||
}
|
||||
}
|
||||
if serviceName == "worker" {
|
||||
if elderAgents != nil {
|
||||
go elderAgents.RunPersonaSync(ctx, logger)
|
||||
}
|
||||
webEventService = webevents.NewService(databasePool)
|
||||
outbox.RegisterMetrics(databasePool)
|
||||
observability.RegisterBusinessMetrics(databasePool)
|
||||
@@ -392,6 +498,10 @@ func Run(ctx context.Context, serviceName string) error {
|
||||
if readErr != nil {
|
||||
return fmt.Errorf("read Volcengine voice-chat template: %w", readErr)
|
||||
}
|
||||
template, err = cfg.ApplyVolcVoiceTemplate(template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
providerClient, err = voiceprovider.NewVolcengineClient(voiceprovider.VolcengineOptions{
|
||||
BaseURL: cfg.VolcRTCBaseURL, AccessKeyID: cfg.VolcAccessKeyID, SecretAccessKey: cfg.VolcSecretAccessKey,
|
||||
SessionToken: cfg.VolcSessionToken, Region: cfg.VolcRTCRegion, AppID: cfg.RTCAppID,
|
||||
@@ -434,6 +544,12 @@ func Run(ctx context.Context, serviceName string) error {
|
||||
outboxWorker.Register("conversation.agent_stop_requested", stopProcessor.Handle)
|
||||
outboxWorker.Register("conversation.agent_stop_reconcile_requested", stopProcessor.Handle)
|
||||
var contentGenerator extraction.Generator = contentModelRegistry
|
||||
if cfg.DifyEnabled {
|
||||
contentGenerator, err = extraction.NewDifyGenerator(cfg.DifyBaseURL, cfg.DifyExtractionAPIKey, cfg.DifyBridgeURL, cfg.DifyBridgeSecret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var contentSafety contentpolicy.Classifier = contentpolicy.LocalClassifier{}
|
||||
if cfg.ContentSafetyProvider == "managed_http" {
|
||||
managedSafety, safetyErr := contentpolicy.NewManagedClassifier(cfg.ContentSafetyEndpoint, cfg.ContentSafetyAPIKey, cfg.ContentSafetyPolicyRef, cfg.ContentSafetyTimeout)
|
||||
@@ -446,17 +562,16 @@ func Run(ctx context.Context, serviceName string) error {
|
||||
if processorErr != nil {
|
||||
return fmt.Errorf("create content extraction processor: %w", processorErr)
|
||||
}
|
||||
if vikingMemory != nil {
|
||||
outboxWorker.Register(vikingmemory.SyncEvent, vikingMemory.Handle)
|
||||
outboxWorker.Register("conversation.content_extraction_requested", func(ctx context.Context, event outbox.Event) error {
|
||||
if err := vikingMemory.Enqueue(ctx, event); err != nil {
|
||||
return err
|
||||
}
|
||||
return extractionProcessor.Handle(ctx, event)
|
||||
})
|
||||
} else {
|
||||
outboxWorker.Register("conversation.content_extraction_requested", extractionProcessor.Handle)
|
||||
}
|
||||
// CALL-MEMORY-1.1 has one authoritative extractor. Historical AddSession
|
||||
// queue entries must not repopulate independently extracted provider memory.
|
||||
outboxWorker.Register(vikingmemory.SyncEvent, func(context.Context, outbox.Event) error { return nil })
|
||||
outboxWorker.Register("conversation.content_extraction_requested", extractionProcessor.Handle)
|
||||
outboxWorker.Register(extraction.DirectStoryEvent, extractionProcessor.HandleDirectStory)
|
||||
outboxWorker.Register(extraction.ImportStoryEvent, extractionProcessor.HandleImport)
|
||||
memoirService.Safety = contentSafety
|
||||
go memoirService.Run(ctx)
|
||||
callMemoryService.ConfigureExtractor(contentModelRegistry)
|
||||
go callMemoryService.Run(ctx)
|
||||
memoryIndexProcessor, processorErr := memoryindex.NewProcessor(databasePool, memoryEmbedding)
|
||||
if processorErr != nil {
|
||||
return fmt.Errorf("create memory index processor: %w", processorErr)
|
||||
@@ -549,6 +664,8 @@ func Run(ctx context.Context, serviceName string) error {
|
||||
Readiness: readiness,
|
||||
TokenVerifier: tokenVerifier,
|
||||
IdentityRepository: identityRepository,
|
||||
CallMemoryService: callMemoryService,
|
||||
MemoirService: memoirService,
|
||||
SaaSService: saasService,
|
||||
DeviceVerifier: deviceVerifier,
|
||||
DeviceService: deviceRepository,
|
||||
@@ -588,7 +705,7 @@ func Run(ctx context.Context, serviceName string) error {
|
||||
Handler: otelhttp.NewHandler(httpapi.Middleware(logger, serviceName, 1<<20, rateLimiterAdapter{limiter}, router), serviceName),
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user