feat: add natural elder story companion flow

This commit is contained in:
Codex Local
2026-09-06 20:16:01 +08:00
parent 029a6097a2
commit 8c834c0aa7
20 changed files with 582 additions and 149 deletions
+32 -88
View File
@@ -43,9 +43,10 @@ var (
const contextPreparationFailureCode = "context_preparation_failed"
const (
contextSummaryLimit = 1000
recentSummaryLimit = 3
recentSummaryContentLimit = 240
contextSummaryLimit = 1000
recentConversationLimit = 3
recentSummaryContentLimit = 240
recentOpenTopicContentLimit = 240
)
type AllocationOptions struct {
@@ -405,14 +406,14 @@ func (r *Repository) Create(
consentSnapshotID, memoryVersionsJSON, promptBundleVersion, engineMode,
privacyFromConsent(effectiveConsents), options)
}
var recentSummaries []string
var recentContexts []recentConversationContext
if consentAllowsConversationContinuity(effectiveConsents) {
recentSummaries, err = selectRecentConversationSummaries(ctx, tx, identity.TenantID, subjectID, recentSummaryLimit)
recentContexts, err = selectRecentConversationContexts(ctx, tx, identity.TenantID, subjectID, recentConversationLimit)
if err != nil {
return CreateResponse{}, "", fmt.Errorf("retrieve recent conversation summaries: %w", err)
return CreateResponse{}, "", fmt.Errorf("retrieve recent conversation context: %w", err)
}
}
contextSummary := buildContextSummary(subjectName, profileContext, recentSummaries, memoryContents)
contextSummary := buildCompanionContextSummary(subjectName, profileContext, recentContexts, memoryContents)
var vikingContext string
var vikingNativeMemory *HardwareAgentMemoryConfig
if options.VikingSubjectID == subjectID && options.VikingContext != "" && consentAllowsConversationContinuity(effectiveConsents) {
@@ -437,16 +438,30 @@ func (r *Repository) Create(
}
}
contextValue := struct {
SubjectID string `json:"subject_id"`
PreferredName string `json:"preferred_name"`
ProfileVersion int64 `json:"profile_version"`
RequestedGoal string `json:"requested_goal"`
ProfileContext authorizedSubjectContext `json:"profile_context"`
RecentSummaries []string `json:"recent_summaries"`
MemoryContents []string `json:"memory_contents"`
RetrievalPolicy string `json:"retrieval_policy"`
VikingContext string `json:"viking_context,omitempty"`
}{subjectPublicID, subjectName, profileVersion, input.RequestedGoal, profileContext, recentSummaries, memoryContents, retrievalPolicy(options.MemoryPolicy), vikingContext}
SubjectID string `json:"subject_id"`
PreferredName string `json:"preferred_name"`
ProfileVersion int64 `json:"profile_version"`
RequestedGoal string `json:"requested_goal"`
ProfileContext authorizedSubjectContext `json:"profile_context"`
RecentContexts []recentConversationContext `json:"recent_contexts"`
RecentSummaries []string `json:"recent_summaries"`
SuggestedOpenTopic string `json:"suggested_open_topic,omitempty"`
MemoryContents []string `json:"memory_contents"`
RetrievalPolicy string `json:"retrieval_policy"`
VikingContext string `json:"viking_context,omitempty"`
}{
SubjectID: subjectPublicID,
PreferredName: subjectName,
ProfileVersion: profileVersion,
RequestedGoal: input.RequestedGoal,
ProfileContext: profileContext,
RecentContexts: recentContexts,
RecentSummaries: recentConversationSummaries(recentContexts),
SuggestedOpenTopic: suggestedOpenTopic(recentContexts),
MemoryContents: memoryContents,
RetrievalPolicy: retrievalPolicy(options.MemoryPolicy),
VikingContext: vikingContext,
}
contextJSON, _ := json.Marshal(contextValue)
contextDigest := sha256.Sum256(contextJSON)
contextSnapshotID := uuid.NewString()
@@ -937,40 +952,6 @@ func authorizedProfileContext(profileDataJSON, preferencesJSON json.RawMessage)
}, nil
}
func buildContextSummary(preferredName string, profile authorizedSubjectContext, recentSummaries, memoryContents []string) string {
parts := make([]string, 0, 10)
if len(profile.AvoidedTopics) > 0 {
parts = append(parts, "交流边界:不要主动提及“"+strings.Join(truncateContextItems(profile.AvoidedTopics, 24), "、")+"”;若长者主动提及,先简短共情,再自然转向;紧急安全响应不受此限制")
}
parts = append(parts, "称呼:"+truncateContextField(preferredName, 60))
for index, summary := range recentSummaries {
parts = append(parts, fmt.Sprintf("最近第%d次通话摘要:%s", index+1, truncateContextField(summary, 140)))
}
if len(memoryContents) > 0 {
parts = append(parts, "已人工确认的长期记忆:"+strings.Join(truncateContextItems(memoryContents, 30), ";"))
}
goal := profile.ConversationGoal
if goal == "" {
goal = "进行一次自然、尊重的陪伴交流"
}
parts = append(parts, "本次交流目标:"+truncateContextField(goal, 80))
if profile.Pace != "" {
paceLabels := map[string]string{"slow": "放慢语速并留出回应时间", "normal": "自然语速", "lively": "轻快但保持清晰"}
parts = append(parts, "交流节奏:"+paceLabels[profile.Pace])
}
if len(profile.PreferredTopics) > 0 {
parts = append(parts, "优先话题:"+strings.Join(truncateContextItems(profile.PreferredTopics, 15), "、"))
}
if profile.Background != "" {
parts = append(parts, "背景资料:"+truncateContextField(profile.Background, 80))
}
if len(profile.Interests) > 0 {
parts = append(parts, "兴趣:"+strings.Join(truncateContextItems(profile.Interests, 15), "、"))
}
result := strings.Join(parts, ";")
return truncateContextField(result, contextSummaryLimit)
}
func truncateContextItems(values []string, itemLimit int) []string {
items := make([]string, 0, len(values))
for _, value := range values {
@@ -1000,43 +981,6 @@ func consentAllowsConversationContinuity(consents json.RawMessage) bool {
return values["personalized_memory"] && values["story_extraction"]
}
func selectRecentConversationSummaries(ctx context.Context, tx pgx.Tx, tenantID, subjectID string, limit int) ([]string, error) {
if limit <= 0 {
return nil, nil
}
rows, err := tx.Query(ctx, `
WITH latest_summary AS (
SELECT DISTINCT ON (summary.conversation_id)
summary.conversation_id, summary.content, conversation.ended_at, conversation.created_at
FROM conversation_summary summary
JOIN conversation ON conversation.id=summary.conversation_id
AND conversation.tenant_id=summary.tenant_id
AND conversation.subject_id=summary.subject_id
WHERE summary.tenant_id=$1::uuid AND summary.subject_id=$2::uuid
AND summary.summary_type='conversation_summary' AND summary.status='READY'
AND conversation.status IN ('COMPLETED','FAILED')
ORDER BY summary.conversation_id, summary.generation_version DESC, summary.created_at DESC
)
SELECT content FROM latest_summary
ORDER BY COALESCE(ended_at,created_at) DESC, conversation_id DESC
LIMIT $3`, tenantID, subjectID, limit)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]string, 0, limit)
for rows.Next() {
var content string
if err := rows.Scan(&content); err != nil {
return nil, err
}
if content = truncateContextText(content, recentSummaryContentLimit); content != "" {
result = append(result, content)
}
}
return result, rows.Err()
}
func strictContextString(values map[string]any, key string, limit int) (string, error) {
raw, exists := values[key]
if !exists || raw == nil {