feat(ai): unify reply center and refresh keyboard AI features

- Merge invitation, task, blessing, clarification, and empathy actions
  into a single Reply flow, with three fixed, clearly labeled stance
  choices whenever user intent must not be guessed.
- Refine clipboard semantic routing with bilingual schedule,
  confirmation, and follow-up models, conservative language thresholds,
  and explicit-assignment guard for complaint-only text.
- Persist Apple account refresh state, harden session recovery, and
  surface durable account diagnostics across keyboard and app.
- Derive personal-style prompts through two-stage corpus evidence and
  apply real low-confidence ASR tendencies instead of neutral templates.
- Localize the new reply center, clipboard semantics, and personal-style
  surfaces in both English and Simplified Chinese.
This commit is contained in:
Rocky
2026-08-29 11:51:21 +08:00
parent f91a8f2456
commit 3c10d73d7f
30 changed files with 2090 additions and 447 deletions
@@ -11,7 +11,6 @@ import Foundation
public enum ClipboardSkillSemanticRanker {
private static let longTextCharacterThreshold = 360
private static let languageConfidenceThreshold = 0.75
private static let maximumReplyRecommendations = 2
public static func ranked(
skills: [AIClipboardSkill],
@@ -31,8 +30,8 @@ public enum ClipboardSkillSemanticRanker {
)
}
/// Returns semantically relevant skills and always keeps the generic Reply
/// action available as a safe fallback for accepted clipboard text.
/// Returns semantically relevant skills and keeps generic Reply as a safe
/// fallback unless a display-only boundary intent suppresses human routing.
public static func recommended(
skills: [AIClipboardSkill],
sourceText: String,
@@ -47,7 +46,9 @@ public enum ClipboardSkillSemanticRanker {
analysis: analysis,
preferredLanguages: preferredLanguages
)
let genericReply = skills.first { $0.id == AIClipboardSkillCatalog.replyID }
let genericReply = suppressesInterpersonalRouting(analysis)
? nil
: skills.first { $0.id == AIClipboardSkillCatalog.replyID }
if genericReply != nil {
scores[AIClipboardSkillCatalog.replyID, default: 0] = max(
1,
@@ -56,7 +57,6 @@ public enum ClipboardSkillSemanticRanker {
}
let relevant = skills.filter { scores[$0.id, default: 0] > 0 }
var selected: [AIClipboardSkill] = []
var specializedReplyCount = 0
for skill in sorted(relevant, scores: scores) {
let mustReserveGenericReply = genericReply != nil
&& !selected.contains(where: { $0.id == AIClipboardSkillCatalog.replyID })
@@ -67,10 +67,6 @@ public enum ClipboardSkillSemanticRanker {
selected.append(skill)
continue
}
if skill.supportsReplyStyle {
guard specializedReplyCount < maximumReplyRecommendations else { continue }
specializedReplyCount += 1
}
selected.append(skill)
}
if let genericReply,
@@ -116,70 +112,65 @@ public enum ClipboardSkillSemanticRanker {
boost(AIClipboardSkillCatalog.navigateID, 180)
}
if isRoutingEvidence(analysis.invitation) {
if analysis.hasDateOrTime {
boost(AIClipboardSkillCatalog.extractEventsID, 260)
let suppressesInterpersonalRouting = suppressesInterpersonalRouting(analysis)
if !suppressesInterpersonalRouting {
if isRoutingEvidence(analysis.invitation) {
if analysis.hasDateOrTime {
boost(AIClipboardSkillCatalog.extractEventsID, 260)
}
boost(AIClipboardSkillCatalog.replyID, 300)
} else if analysis.hasDateOrTime {
boost(AIClipboardSkillCatalog.extractEventsID, 110)
}
boost(AIClipboardSkillCatalog.acceptInvitationID, 240)
boost(AIClipboardSkillCatalog.declineInvitationID, 230)
boost(AIClipboardSkillCatalog.replyID, 60)
} else if analysis.hasDateOrTime {
boost(AIClipboardSkillCatalog.extractEventsID, 110)
}
// A threshold-crossing, evaluation-gated model may still rank a
// reversible chip; execution always remains explicitly user-initiated.
if isRoutingEvidence(analysis.scheduleNegotiation) {
boost(AIClipboardSkillCatalog.clarifyRequestID, 300)
boost(AIClipboardSkillCatalog.extractEventsID, 200)
boost(AIClipboardSkillCatalog.replyID, 250)
}
if !suppressesInterpersonalRouting {
// A threshold-crossing, evaluation-gated model may still rank a
// reversible chip; execution always remains explicitly user-initiated.
if isRoutingEvidence(analysis.scheduleNegotiation) {
boost(AIClipboardSkillCatalog.extractEventsID, 200)
boost(AIClipboardSkillCatalog.replyID, 300)
}
if isRoutingEvidence(analysis.confirmationDecision) {
boost(AIClipboardSkillCatalog.acceptTaskID, 300)
boost(AIClipboardSkillCatalog.replyID, 280)
}
if isRoutingEvidence(analysis.confirmationDecision) {
boost(AIClipboardSkillCatalog.replyID, 300)
}
if isRoutingEvidence(analysis.followUpReminder) {
boost(AIClipboardSkillCatalog.extractTodosID, 285)
boost(AIClipboardSkillCatalog.acceptTaskID, 250)
boost(AIClipboardSkillCatalog.clarifyRequestID, 170)
boost(AIClipboardSkillCatalog.replyID, 90)
}
if isRoutingEvidence(analysis.followUpReminder) {
boost(AIClipboardSkillCatalog.extractTodosID, 285)
boost(AIClipboardSkillCatalog.replyID, 250)
}
if isRoutingEvidence(analysis.task) {
boost(AIClipboardSkillCatalog.extractTodosID, 155)
boost(AIClipboardSkillCatalog.acceptTaskID, 140)
boost(AIClipboardSkillCatalog.clarifyRequestID, 105)
}
if isRoutingEvidence(analysis.task) {
boost(AIClipboardSkillCatalog.extractTodosID, 155)
boost(AIClipboardSkillCatalog.replyID, 140)
}
if isRoutingEvidence(analysis.question) {
boost(AIClipboardSkillCatalog.replyID, 145)
boost(AIClipboardSkillCatalog.clarifyRequestID, 110)
}
if isRoutingEvidence(analysis.question) {
boost(AIClipboardSkillCatalog.replyID, 145)
}
if isRoutingEvidence(analysis.blessing) {
boost(AIClipboardSkillCatalog.blessingReplyID, 300)
boost(AIClipboardSkillCatalog.replyID, 95)
}
if isRoutingEvidence(analysis.blessing) {
boost(AIClipboardSkillCatalog.replyID, 300)
}
if isRoutingEvidence(analysis.complaint) {
boost(AIClipboardSkillCatalog.empathyReplyID, 105)
boost(AIClipboardSkillCatalog.clarifyRequestID, 90)
boost(AIClipboardSkillCatalog.replyID, 55)
} else if analysis.sentiment == .negative,
isRoutingEvidence(analysis.question) {
boost(AIClipboardSkillCatalog.empathyReplyID, 85)
boost(AIClipboardSkillCatalog.clarifyRequestID, 65)
}
if isRoutingEvidence(analysis.complaint) {
boost(AIClipboardSkillCatalog.replyID, 105)
} else if analysis.sentiment == .negative,
isRoutingEvidence(analysis.question) {
boost(AIClipboardSkillCatalog.replyID, 85)
}
if analysis.hasOrganizationName,
isRoutingEvidence(analysis.task)
|| isRoutingEvidence(analysis.question)
|| isRoutingEvidence(analysis.invitation) {
boost(AIClipboardSkillCatalog.replyID, 125)
} else if analysis.hasOrganizationName {
boost(AIClipboardSkillCatalog.replyID, 70)
if analysis.hasOrganizationName,
isRoutingEvidence(analysis.task)
|| isRoutingEvidence(analysis.question)
|| isRoutingEvidence(analysis.invitation) {
boost(AIClipboardSkillCatalog.replyID, 125)
} else if analysis.hasOrganizationName {
boost(AIClipboardSkillCatalog.replyID, 70)
}
}
if isListLike(sourceText) {
@@ -193,32 +184,80 @@ public enum ClipboardSkillSemanticRanker {
boost(AIClipboardSkillCatalog.saveToNotesID, 85)
}
let hasSpecializedReplyIntent = isRoutingEvidence(analysis.task)
|| isRoutingEvidence(analysis.question)
|| isRoutingEvidence(analysis.invitation)
|| isRoutingEvidence(analysis.scheduleNegotiation)
|| isRoutingEvidence(analysis.confirmationDecision)
|| isRoutingEvidence(analysis.followUpReminder)
|| isRoutingEvidence(analysis.blessing)
|| isRoutingEvidence(analysis.complaint)
if analysis.replyableMessage.isDetected,
!hasSpecializedReplyIntent,
sourceText.count < longTextCharacterThreshold,
!isListLike(sourceText) {
boost(AIClipboardSkillCatalog.replyID, 160)
if analysis.sentiment != .negative,
!isRoutingEvidence(analysis.complaint) {
// Reply now exposes ordinary, formal, and playful variants
// inside one action rather than ranking separate style skills.
boost(AIClipboardSkillCatalog.replyID, 145)
if !suppressesInterpersonalRouting {
let hasSpecializedReplyIntent = isRoutingEvidence(analysis.task)
|| isRoutingEvidence(analysis.question)
|| isRoutingEvidence(analysis.invitation)
|| isRoutingEvidence(analysis.scheduleNegotiation)
|| isRoutingEvidence(analysis.confirmationDecision)
|| isRoutingEvidence(analysis.followUpReminder)
|| isRoutingEvidence(analysis.blessing)
|| isRoutingEvidence(analysis.complaint)
if analysis.replyableMessage.isDetected,
!hasSpecializedReplyIntent,
sourceText.count < longTextCharacterThreshold,
!isListLike(sourceText) {
boost(AIClipboardSkillCatalog.replyID, 160)
if analysis.sentiment != .negative,
!isRoutingEvidence(analysis.complaint) {
// Reply now exposes ordinary, formal, and playful variants
// inside one action rather than ranking separate style skills.
boost(AIClipboardSkillCatalog.replyID, 145)
}
}
if analysis.sentiment == .positive {
boost(AIClipboardSkillCatalog.replyID, 45)
}
}
if analysis.sentiment == .positive {
boost(AIClipboardSkillCatalog.replyID, 45)
}
applyDomainBoosts(
analysis,
sourceText: sourceText,
suppressesInterpersonalRouting: suppressesInterpersonalRouting,
boost: boost
)
return scores
}
private static func applyDomainBoosts(
_ analysis: ClipboardSemanticAnalysis,
sourceText: String,
suppressesInterpersonalRouting: Bool,
boost: (String, Int) -> Void
) {
guard let domain = analysis.domain,
let confidence = analysis.domainConfidence,
confidence > 0 else {
return
}
switch domain {
case .calendar:
if analysis.hasDateOrTime {
boost(AIClipboardSkillCatalog.extractEventsID, 20)
}
case .travel:
if analysis.hasAddress {
boost(AIClipboardSkillCatalog.navigateID, 20)
}
case .media, .generalKnowledge:
if sourceText.count >= longTextCharacterThreshold {
boost(AIClipboardSkillCatalog.summarizeID, 15)
}
case .communication:
if !suppressesInterpersonalRouting,
hasInterpersonalRoutingEvidence(analysis) {
boost(AIClipboardSkillCatalog.replyID, 15)
}
case .finance, .accountService:
if !suppressesInterpersonalRouting,
isRoutingEvidence(analysis.question)
|| isRoutingEvidence(analysis.complaint) {
boost(AIClipboardSkillCatalog.replyID, 10)
}
case .smartHome, .shopping, .dining, .health, .weather:
return
}
}
private static func sorted(
_ skills: [AIClipboardSkill],
scores: [String: Int]
@@ -255,6 +294,34 @@ public enum ClipboardSkillSemanticRanker {
label.isDetected && label.isApprovedForAutomaticRouting
}
private static func isDisplayEvidence(_ label: ClipboardIntentLabel) -> Bool {
label.isDetected
&& label.confidence > 0
&& label.confidence >= label.threshold
}
private static func suppressesInterpersonalRouting(
_ analysis: ClipboardSemanticAnalysis
) -> Bool {
isDisplayEvidence(analysis.assistantCommand)
|| isDisplayEvidence(analysis.informationQuery)
|| isDisplayEvidence(analysis.systemNotification)
}
private static func hasInterpersonalRoutingEvidence(
_ analysis: ClipboardSemanticAnalysis
) -> Bool {
isRoutingEvidence(analysis.task)
|| isRoutingEvidence(analysis.question)
|| isRoutingEvidence(analysis.invitation)
|| isRoutingEvidence(analysis.complaint)
|| isRoutingEvidence(analysis.replyableMessage)
|| isRoutingEvidence(analysis.scheduleNegotiation)
|| isRoutingEvidence(analysis.confirmationDecision)
|| isRoutingEvidence(analysis.followUpReminder)
|| isRoutingEvidence(analysis.blessing)
}
private static func isListLike(_ text: String) -> Bool {
let lines = text
.split(whereSeparator: \.isNewline)