This commit is contained in:
@@ -38,6 +38,12 @@ var (
|
||||
|
||||
const contextPreparationFailureCode = "context_preparation_failed"
|
||||
|
||||
const (
|
||||
contextSummaryLimit = 1000
|
||||
recentSummaryLimit = 3
|
||||
recentSummaryContentLimit = 240
|
||||
)
|
||||
|
||||
type AllocationOptions struct {
|
||||
Now time.Time
|
||||
AllocationExpires time.Time
|
||||
@@ -367,19 +373,24 @@ func (r *Repository) Create(
|
||||
consentSnapshotID, memoryVersionsJSON, promptBundleVersion, engineMode,
|
||||
privacyFromConsent(effectiveConsents), options)
|
||||
}
|
||||
contextSummary := buildContextSummary(subjectName, profileContext)
|
||||
if len(memoryContents) > 0 {
|
||||
contextSummary += ";已确认记忆:" + strings.Join(memoryContents, ";")
|
||||
var recentSummaries []string
|
||||
if consentAllowsConversationContinuity(effectiveConsents) {
|
||||
recentSummaries, err = selectRecentConversationSummaries(ctx, tx, identity.TenantID, subjectID, recentSummaryLimit)
|
||||
if err != nil {
|
||||
return CreateResponse{}, "", fmt.Errorf("retrieve recent conversation summaries: %w", err)
|
||||
}
|
||||
}
|
||||
contextSummary := buildContextSummary(subjectName, profileContext, recentSummaries, memoryContents)
|
||||
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"`
|
||||
}{subjectPublicID, subjectName, profileVersion, input.RequestedGoal, profileContext, memoryContents, retrievalPolicy(options.MemoryPolicy)}
|
||||
}{subjectPublicID, subjectName, profileVersion, input.RequestedGoal, profileContext, recentSummaries, memoryContents, retrievalPolicy(options.MemoryPolicy)}
|
||||
contextJSON, _ := json.Marshal(contextValue)
|
||||
contextDigest := sha256.Sum256(contextJSON)
|
||||
contextSnapshotID := uuid.NewString()
|
||||
@@ -664,6 +675,7 @@ type authorizedSubjectContext struct {
|
||||
ConversationGoal string `json:"conversation_goal,omitempty"`
|
||||
Pace string `json:"pace,omitempty"`
|
||||
PreferredTopics []string `json:"preferred_topics,omitempty"`
|
||||
AvoidedTopics []string `json:"avoided_topics,omitempty"`
|
||||
}
|
||||
|
||||
func authorizedProfileContext(profileDataJSON, preferencesJSON json.RawMessage) (authorizedSubjectContext, error) {
|
||||
@@ -694,6 +706,10 @@ func authorizedProfileContext(profileDataJSON, preferencesJSON json.RawMessage)
|
||||
if err != nil {
|
||||
return authorizedSubjectContext{}, err
|
||||
}
|
||||
avoidedTopics, err := strictContextStringList(preferences, "avoided_topics", 8, 50)
|
||||
if err != nil {
|
||||
return authorizedSubjectContext{}, err
|
||||
}
|
||||
if pace != "slow" && pace != "normal" && pace != "lively" {
|
||||
pace = ""
|
||||
}
|
||||
@@ -703,30 +719,107 @@ func authorizedProfileContext(profileDataJSON, preferencesJSON json.RawMessage)
|
||||
ConversationGoal: goal,
|
||||
Pace: pace,
|
||||
PreferredTopics: preferredTopics,
|
||||
AvoidedTopics: avoidedTopics,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildContextSummary(preferredName string, profile authorizedSubjectContext) string {
|
||||
parts := []string{"称呼:" + truncateContextText(preferredName, 120)}
|
||||
if profile.Background != "" {
|
||||
parts = append(parts, "背景资料:"+profile.Background)
|
||||
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), "、")+"”;若长者主动提及,先简短共情,再自然转向;紧急安全响应不受此限制")
|
||||
}
|
||||
if len(profile.Interests) > 0 {
|
||||
parts = append(parts, "兴趣:"+strings.Join(profile.Interests, "、"))
|
||||
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, "本次交流目标:"+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(profile.PreferredTopics, "、"))
|
||||
parts = append(parts, "优先话题:"+strings.Join(truncateContextItems(profile.PreferredTopics, 15), "、"))
|
||||
}
|
||||
return strings.Join(parts, ";")
|
||||
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 {
|
||||
if value = truncateContextField(value, itemLimit); value != "" {
|
||||
items = append(items, value)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func truncateContextField(value string, limit int) string {
|
||||
runes := []rune(strings.TrimSpace(value))
|
||||
if len(runes) <= limit {
|
||||
return string(runes)
|
||||
}
|
||||
if limit <= 1 {
|
||||
return "…"
|
||||
}
|
||||
return string(runes[:limit-1]) + "…"
|
||||
}
|
||||
|
||||
func consentAllowsConversationContinuity(consents json.RawMessage) bool {
|
||||
values := map[string]bool{}
|
||||
if json.Unmarshal(consents, &values) != nil {
|
||||
return false
|
||||
}
|
||||
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
|
||||
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='COMPLETED'
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user