feat(memory): add Temporal activities integration for memory service
- Implement 12 Temporal activities for memory operations - Activities: create, update, search, context, diagnose, analyze, document - Add activity registration and worker setup - Full retry/timeout configuration with observability - Include workflow patterns and examples - All tests passing (23/23) Documentation: - MEMORY_INTEGRATION.md: High-level integration guide - MEMORY_ACTIVITIES.md: Complete activities reference - REGISTERED_ACTIVITIES.md: Registry and calling conventions
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.temporal.io/sdk/activity"
|
||||
"go.temporal.io/sdk/temporal"
|
||||
"go.temporal.io/sdk/worker"
|
||||
"go.temporal.io/sdk/workflow"
|
||||
)
|
||||
|
||||
// RegisterMemoryActivities registers all memory service activities with worker
|
||||
func RegisterMemoryActivities(w worker.Worker, service *Service) {
|
||||
activities := NewActivities(service)
|
||||
|
||||
// Register activities (activity name = "ActivityName" → "activityName")
|
||||
w.RegisterActivity(activities.CreateKnowledgeActivity)
|
||||
w.RegisterActivity(activities.UpdateKnowledgeActivity)
|
||||
w.RegisterActivity(activities.SearchKnowledgeActivity)
|
||||
w.RegisterActivity(activities.GetContextActivity)
|
||||
w.RegisterActivity(activities.GetVaultActivity)
|
||||
w.RegisterActivity(activities.HealthCheckActivity)
|
||||
w.RegisterActivity(activities.LearnFromExecutionActivity)
|
||||
w.RegisterActivity(activities.DiagnoseIssueActivity)
|
||||
w.RegisterActivity(activities.AnalyzeErrorActivity)
|
||||
w.RegisterActivity(activities.DocumentDecisionActivity)
|
||||
w.RegisterActivity(activities.SearchAndApplyActivity)
|
||||
w.RegisterActivity(activities.RefreshMemoryActivity)
|
||||
}
|
||||
|
||||
// ActivityOptions memory service activity options
|
||||
type ActivityOptions struct {
|
||||
RetryAttempts int
|
||||
RetryBackoff time.Duration
|
||||
StartTimeout time.Duration
|
||||
HeartbeatRate time.Duration
|
||||
}
|
||||
|
||||
// DefaultActivityOptions returns sensible defaults
|
||||
func DefaultActivityOptions() *ActivityOptions {
|
||||
return &ActivityOptions{
|
||||
RetryAttempts: 3,
|
||||
RetryBackoff: time.Second,
|
||||
StartTimeout: 30 * time.Second,
|
||||
HeartbeatRate: 10 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteCreateKnowledge wrapper for CreateKnowledgeActivity
|
||||
func ExecuteCreateKnowledge(
|
||||
ctx workflow.Context,
|
||||
record *KnowledgeRecord,
|
||||
opts *ActivityOptions,
|
||||
) (string, error) {
|
||||
if opts == nil {
|
||||
opts = DefaultActivityOptions()
|
||||
}
|
||||
|
||||
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
|
||||
ScheduleToCloseTimeout: 2 * time.Minute,
|
||||
StartToCloseTimeout: time.Minute,
|
||||
RetryPolicy: &temporal.RetryPolicy{
|
||||
InitialInterval: opts.RetryBackoff,
|
||||
BackoffCoefficient: 2.0,
|
||||
MaximumInterval: 30 * time.Second,
|
||||
MaximumAttempts: int32(opts.RetryAttempts),
|
||||
NonRetryableErrorTypes: []string{},
|
||||
},
|
||||
})
|
||||
|
||||
var result string
|
||||
err := workflow.ExecuteActivity(activityCtx, "CreateKnowledgeActivity", record).Get(activityCtx, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ExecuteSearchKnowledge wrapper for SearchKnowledgeActivity
|
||||
func ExecuteSearchKnowledge(
|
||||
ctx workflow.Context,
|
||||
query string,
|
||||
opts *RetrievalOptions,
|
||||
activityOpts *ActivityOptions,
|
||||
) ([]KnowledgeRecord, error) {
|
||||
if activityOpts == nil {
|
||||
activityOpts = DefaultActivityOptions()
|
||||
}
|
||||
|
||||
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
|
||||
ScheduleToCloseTimeout: 3 * time.Minute,
|
||||
StartToCloseTimeout: 2 * time.Minute,
|
||||
RetryPolicy: &temporal.RetryPolicy{
|
||||
InitialInterval: activityOpts.RetryBackoff,
|
||||
BackoffCoefficient: 2.0,
|
||||
MaximumInterval: 30 * time.Second,
|
||||
MaximumAttempts: int32(activityOpts.RetryAttempts),
|
||||
},
|
||||
})
|
||||
|
||||
var result []KnowledgeRecord
|
||||
err := workflow.ExecuteActivity(activityCtx, "SearchKnowledgeActivity", query, opts).Get(activityCtx, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ExecuteGetContext wrapper for GetContextActivity
|
||||
func ExecuteGetContext(
|
||||
ctx workflow.Context,
|
||||
tool, task string,
|
||||
budget int,
|
||||
opts *ActivityOptions,
|
||||
) (*ServiceContext, error) {
|
||||
if opts == nil {
|
||||
opts = DefaultActivityOptions()
|
||||
}
|
||||
|
||||
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
|
||||
ScheduleToCloseTimeout: 3 * time.Minute,
|
||||
StartToCloseTimeout: 2 * time.Minute,
|
||||
RetryPolicy: &temporal.RetryPolicy{
|
||||
InitialInterval: opts.RetryBackoff,
|
||||
BackoffCoefficient: 2.0,
|
||||
MaximumInterval: 30 * time.Second,
|
||||
MaximumAttempts: int32(opts.RetryAttempts),
|
||||
},
|
||||
})
|
||||
|
||||
var result *ServiceContext
|
||||
err := workflow.ExecuteActivity(activityCtx, "GetContextActivity", tool, task, budget).Get(activityCtx, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ExecuteDiagnoseIssue wrapper for DiagnoseIssueActivity
|
||||
func ExecuteDiagnoseIssue(
|
||||
ctx workflow.Context,
|
||||
tool, issue string,
|
||||
opts *ActivityOptions,
|
||||
) ([]string, error) {
|
||||
if opts == nil {
|
||||
opts = DefaultActivityOptions()
|
||||
}
|
||||
|
||||
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
|
||||
ScheduleToCloseTimeout: 2 * time.Minute,
|
||||
StartToCloseTimeout: time.Minute,
|
||||
RetryPolicy: &temporal.RetryPolicy{
|
||||
InitialInterval: opts.RetryBackoff,
|
||||
BackoffCoefficient: 2.0,
|
||||
MaximumInterval: 30 * time.Second,
|
||||
MaximumAttempts: int32(opts.RetryAttempts),
|
||||
},
|
||||
})
|
||||
|
||||
var result []string
|
||||
err := workflow.ExecuteActivity(activityCtx, "DiagnoseIssueActivity", tool, issue).Get(activityCtx, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ExecuteAnalyzeError wrapper for AnalyzeErrorActivity
|
||||
func ExecuteAnalyzeError(
|
||||
ctx workflow.Context,
|
||||
errorMsg string,
|
||||
opts *ActivityOptions,
|
||||
) ([]KnowledgeRecord, error) {
|
||||
if opts == nil {
|
||||
opts = DefaultActivityOptions()
|
||||
}
|
||||
|
||||
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
|
||||
ScheduleToCloseTimeout: 2 * time.Minute,
|
||||
StartToCloseTimeout: time.Minute,
|
||||
RetryPolicy: &temporal.RetryPolicy{
|
||||
InitialInterval: opts.RetryBackoff,
|
||||
BackoffCoefficient: 2.0,
|
||||
MaximumInterval: 30 * time.Second,
|
||||
MaximumAttempts: int32(opts.RetryAttempts),
|
||||
},
|
||||
})
|
||||
|
||||
var result []KnowledgeRecord
|
||||
err := workflow.ExecuteActivity(activityCtx, "AnalyzeErrorActivity", errorMsg).Get(activityCtx, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ExecuteHealthCheck wrapper for HealthCheckActivity
|
||||
func ExecuteHealthCheck(
|
||||
ctx workflow.Context,
|
||||
opts *ActivityOptions,
|
||||
) (bool, error) {
|
||||
if opts == nil {
|
||||
opts = DefaultActivityOptions()
|
||||
}
|
||||
|
||||
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
|
||||
ScheduleToCloseTimeout: 1 * time.Minute,
|
||||
StartToCloseTimeout: 30 * time.Second,
|
||||
RetryPolicy: &temporal.RetryPolicy{
|
||||
InitialInterval: opts.RetryBackoff,
|
||||
BackoffCoefficient: 2.0,
|
||||
MaximumInterval: 15 * time.Second,
|
||||
MaximumAttempts: int32(opts.RetryAttempts),
|
||||
},
|
||||
})
|
||||
|
||||
var result bool
|
||||
err := workflow.ExecuteActivity(activityCtx, "HealthCheckActivity").Get(activityCtx, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ExecuteLearnFromExecution wrapper for LearnFromExecutionActivity
|
||||
func ExecuteLearnFromExecution(
|
||||
ctx workflow.Context,
|
||||
taskID, result string,
|
||||
tags []string,
|
||||
opts *ActivityOptions,
|
||||
) (string, error) {
|
||||
if opts == nil {
|
||||
opts = DefaultActivityOptions()
|
||||
}
|
||||
|
||||
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
|
||||
ScheduleToCloseTimeout: 2 * time.Minute,
|
||||
StartToCloseTimeout: time.Minute,
|
||||
RetryPolicy: &temporal.RetryPolicy{
|
||||
InitialInterval: opts.RetryBackoff,
|
||||
BackoffCoefficient: 2.0,
|
||||
MaximumInterval: 30 * time.Second,
|
||||
MaximumAttempts: int32(opts.RetryAttempts),
|
||||
},
|
||||
})
|
||||
|
||||
var recordID string
|
||||
err := workflow.ExecuteActivity(activityCtx, "LearnFromExecutionActivity", taskID, result, tags).Get(activityCtx, &recordID)
|
||||
return recordID, err
|
||||
}
|
||||
|
||||
// ExecuteDocumentDecision wrapper for DocumentDecisionActivity
|
||||
func ExecuteDocumentDecision(
|
||||
ctx workflow.Context,
|
||||
decisionType, decision, reasoning string,
|
||||
opts *ActivityOptions,
|
||||
) (string, error) {
|
||||
if opts == nil {
|
||||
opts = DefaultActivityOptions()
|
||||
}
|
||||
|
||||
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
|
||||
ScheduleToCloseTimeout: 2 * time.Minute,
|
||||
StartToCloseTimeout: time.Minute,
|
||||
RetryPolicy: &temporal.RetryPolicy{
|
||||
InitialInterval: opts.RetryBackoff,
|
||||
BackoffCoefficient: 2.0,
|
||||
MaximumInterval: 30 * time.Second,
|
||||
MaximumAttempts: int32(opts.RetryAttempts),
|
||||
},
|
||||
})
|
||||
|
||||
var recordID string
|
||||
err := workflow.ExecuteActivity(activityCtx, "DocumentDecisionActivity", decisionType, decision, reasoning).Get(activityCtx, &recordID)
|
||||
return recordID, err
|
||||
}
|
||||
|
||||
// ExecuteRefreshMemory wrapper for RefreshMemoryActivity
|
||||
func ExecuteRefreshMemory(
|
||||
ctx workflow.Context,
|
||||
opts *ActivityOptions,
|
||||
) (map[string]interface{}, error) {
|
||||
if opts == nil {
|
||||
opts = DefaultActivityOptions()
|
||||
}
|
||||
|
||||
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
|
||||
ScheduleToCloseTimeout: 2 * time.Minute,
|
||||
StartToCloseTimeout: time.Minute,
|
||||
RetryPolicy: &temporal.RetryPolicy{
|
||||
InitialInterval: opts.RetryBackoff,
|
||||
BackoffCoefficient: 2.0,
|
||||
MaximumInterval: 30 * time.Second,
|
||||
MaximumAttempts: int32(opts.RetryAttempts),
|
||||
},
|
||||
})
|
||||
|
||||
var result map[string]interface{}
|
||||
err := workflow.ExecuteActivity(activityCtx, "RefreshMemoryActivity").Get(activityCtx, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// HeartbeatMemoryActivity sends heartbeat every N seconds
|
||||
// Usage: Long-running memory operations
|
||||
func HeartbeatMemoryActivity(ctx context.Context, maxDuration time.Duration) error {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
deadline := time.Now().Add(maxDuration)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
activity.RecordHeartbeat(ctx, time.Now())
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
if time.Now().After(deadline) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MemoryActivityError wraps errors with activity context
|
||||
type MemoryActivityError struct {
|
||||
ActivityName string
|
||||
Attempt int
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *MemoryActivityError) Error() string {
|
||||
return fmt.Sprintf("memory activity %s (attempt %d): %v", e.ActivityName, e.Attempt, e.Err)
|
||||
}
|
||||
|
||||
// CaptureActivityError captures activity execution errors
|
||||
func CaptureActivityError(activityName string, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &MemoryActivityError{
|
||||
ActivityName: activityName,
|
||||
Attempt: 1,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user