e5a83843db
Add resilient usage analytics, OOBE gateway flows, clipboard semantic ranking, purchase recovery, style learning, and managed current-information search.
997 lines
35 KiB
Swift
997 lines
35 KiB
Swift
// AppleNaturalLanguageCapabilityTests.swift
|
|
// OSGKeyboardTests
|
|
//
|
|
// Exploratory, fully on-device evaluation for Apple's traditional Natural
|
|
// Language APIs. This intentionally does not use Foundation Models or network.
|
|
|
|
import Foundation
|
|
import NaturalLanguage
|
|
import XCTest
|
|
|
|
final class AppleNaturalLanguageCapabilityTests: XCTestCase {
|
|
private struct LanguageSample {
|
|
let id: String
|
|
let text: String
|
|
let expectedLanguage: String?
|
|
let isClear: Bool
|
|
}
|
|
|
|
private struct LanguageHypothesis: Codable {
|
|
let language: String
|
|
let probability: Double
|
|
}
|
|
|
|
private struct LanguageResult: Codable {
|
|
let id: String
|
|
let text: String
|
|
let expectedLanguage: String?
|
|
let dominantLanguage: String?
|
|
let confidence: Double
|
|
let correct: Bool?
|
|
let hypotheses: [LanguageHypothesis]
|
|
}
|
|
|
|
private struct EntityExpectation {
|
|
let text: String
|
|
let tag: NLTag
|
|
}
|
|
|
|
private struct EntitySample {
|
|
let id: String
|
|
let text: String
|
|
let language: NLLanguage
|
|
let expected: [EntityExpectation]
|
|
}
|
|
|
|
private struct EntityMatch: Codable {
|
|
let text: String
|
|
let tag: String
|
|
}
|
|
|
|
private struct EntityResult: Codable {
|
|
let id: String
|
|
let text: String
|
|
let expected: [EntityMatch]
|
|
let detected: [EntityMatch]
|
|
let matchedCount: Int
|
|
let exactMatchedCount: Int
|
|
}
|
|
|
|
private struct DetectorSample {
|
|
let id: String
|
|
let text: String
|
|
let expectedTypes: Set<String>
|
|
}
|
|
|
|
private struct DetectorMatch: Codable {
|
|
let type: String
|
|
let text: String
|
|
}
|
|
|
|
private struct DetectorResult: Codable {
|
|
let id: String
|
|
let text: String
|
|
let expectedTypes: [String]
|
|
let detected: [DetectorMatch]
|
|
let matchedTypes: [String]
|
|
}
|
|
|
|
private struct SemanticAnchor {
|
|
let skillID: String
|
|
let examples: [String]
|
|
}
|
|
|
|
private struct SemanticSample {
|
|
let id: String
|
|
let text: String
|
|
let expectedSkillID: String
|
|
let isAdversarial: Bool
|
|
}
|
|
|
|
private struct SemanticCorpus {
|
|
let language: NLLanguage
|
|
let languageID: String
|
|
let anchors: [SemanticAnchor]
|
|
let samples: [SemanticSample]
|
|
}
|
|
|
|
private struct SkillDistance: Codable {
|
|
let skillID: String
|
|
let distance: Double
|
|
}
|
|
|
|
private struct SemanticResult: Codable {
|
|
let id: String
|
|
let text: String
|
|
let expectedSkillID: String
|
|
let predictedSkillID: String?
|
|
let topThree: [SkillDistance]
|
|
let topOneCorrect: Bool
|
|
let topThreeCorrect: Bool
|
|
let isAdversarial: Bool
|
|
}
|
|
|
|
private struct SemanticControl {
|
|
let id: String
|
|
let query: String
|
|
let related: String
|
|
let unrelated: String
|
|
}
|
|
|
|
private struct SemanticControlResult: Codable {
|
|
let id: String
|
|
let relatedDistance: Double?
|
|
let unrelatedDistance: Double?
|
|
let passed: Bool
|
|
}
|
|
|
|
private struct SemanticLanguageReport: Codable {
|
|
let language: String
|
|
let embeddingAvailable: Bool
|
|
let dimension: Int?
|
|
let revision: Int?
|
|
let controlAccuracy: Double?
|
|
let topOneAccuracy: Double?
|
|
let topThreeAccuracy: Double?
|
|
let regularTopOneAccuracy: Double?
|
|
let adversarialTopOneAccuracy: Double?
|
|
let controls: [SemanticControlResult]
|
|
let results: [SemanticResult]
|
|
}
|
|
|
|
private struct LatencyReport: Codable {
|
|
let operation: String
|
|
let iterations: Int
|
|
let averageMilliseconds: Double
|
|
}
|
|
|
|
private struct EvaluationReport: Codable {
|
|
let osVersion: String
|
|
let languageClearAccuracy: Double
|
|
let languageResults: [LanguageResult]
|
|
let entityLooseRecall: Double
|
|
let entityExactRecall: Double
|
|
let entityResults: [EntityResult]
|
|
let detectorRecall: Double
|
|
let detectorResults: [DetectorResult]
|
|
let semanticReports: [SemanticLanguageReport]
|
|
let latency: [LatencyReport]
|
|
}
|
|
|
|
func testAppleNaturalLanguageCapability() throws {
|
|
let languageResults = evaluateLanguages()
|
|
let entityResults = evaluateEntities()
|
|
let detector = try makeDataDetector()
|
|
let detectorResults = evaluateDataDetection(using: detector)
|
|
let semanticCorpora = makeSemanticCorpora()
|
|
let semanticReports = semanticCorpora.map(evaluateSemantics)
|
|
|
|
let clearLanguageResults = languageResults.compactMap(\.correct)
|
|
let languageAccuracy = ratio(
|
|
numerator: clearLanguageResults.filter { $0 }.count,
|
|
denominator: clearLanguageResults.count
|
|
)
|
|
let expectedEntityCount = entityResults.reduce(0) { $0 + $1.expected.count }
|
|
let matchedEntityCount = entityResults.reduce(0) { $0 + $1.matchedCount }
|
|
let exactMatchedEntityCount = entityResults.reduce(0) {
|
|
$0 + $1.exactMatchedCount
|
|
}
|
|
let expectedDetectorCount = detectorResults.reduce(0) { $0 + $1.expectedTypes.count }
|
|
let matchedDetectorCount = detectorResults.reduce(0) { $0 + $1.matchedTypes.count }
|
|
|
|
let report = EvaluationReport(
|
|
osVersion: ProcessInfo.processInfo.operatingSystemVersionString,
|
|
languageClearAccuracy: languageAccuracy,
|
|
languageResults: languageResults,
|
|
entityLooseRecall: ratio(
|
|
numerator: matchedEntityCount,
|
|
denominator: expectedEntityCount
|
|
),
|
|
entityExactRecall: ratio(
|
|
numerator: exactMatchedEntityCount,
|
|
denominator: expectedEntityCount
|
|
),
|
|
entityResults: entityResults,
|
|
detectorRecall: ratio(
|
|
numerator: matchedDetectorCount,
|
|
denominator: expectedDetectorCount
|
|
),
|
|
detectorResults: detectorResults,
|
|
semanticReports: semanticReports,
|
|
latency: benchmark(detector: detector, semanticCorpora: semanticCorpora)
|
|
)
|
|
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
|
|
let data = try encoder.encode(report)
|
|
let json = try XCTUnwrap(String(data: data, encoding: .utf8))
|
|
|
|
// One stable marker lets the command-line runner extract the complete report.
|
|
print("APPLE_NL_EVAL_JSON_BEGIN")
|
|
print(json)
|
|
print("APPLE_NL_EVAL_JSON_END")
|
|
|
|
XCTAssertFalse(languageResults.isEmpty)
|
|
XCTAssertFalse(entityResults.isEmpty)
|
|
XCTAssertFalse(detectorResults.isEmpty)
|
|
XCTAssertEqual(semanticReports.count, semanticCorpora.count)
|
|
}
|
|
|
|
private func evaluateLanguages() -> [LanguageResult] {
|
|
makeLanguageSamples().map { sample in
|
|
let recognizer = NLLanguageRecognizer()
|
|
recognizer.processString(sample.text)
|
|
let hypotheses = recognizer.languageHypotheses(withMaximum: 3)
|
|
.map {
|
|
LanguageHypothesis(
|
|
language: $0.key.rawValue,
|
|
probability: rounded($0.value)
|
|
)
|
|
}
|
|
.sorted { $0.probability > $1.probability }
|
|
let dominant = recognizer.dominantLanguage?.rawValue
|
|
let confidence = hypotheses.first(where: { $0.language == dominant })?.probability ?? 0
|
|
return LanguageResult(
|
|
id: sample.id,
|
|
text: sample.text,
|
|
expectedLanguage: sample.expectedLanguage,
|
|
dominantLanguage: dominant,
|
|
confidence: confidence,
|
|
correct: sample.isClear
|
|
? dominant == sample.expectedLanguage
|
|
: nil,
|
|
hypotheses: hypotheses
|
|
)
|
|
}
|
|
}
|
|
|
|
private func makeLanguageSamples() -> [LanguageSample] {
|
|
[
|
|
LanguageSample(
|
|
id: "zh-clear",
|
|
text: "请把会议纪要整理后发给产品和设计团队。",
|
|
expectedLanguage: "zh-Hans",
|
|
isClear: true
|
|
),
|
|
LanguageSample(
|
|
id: "en-clear",
|
|
text: "Please send the revised proposal before Friday afternoon.",
|
|
expectedLanguage: "en",
|
|
isClear: true
|
|
),
|
|
LanguageSample(
|
|
id: "ja-clear",
|
|
text: "来週の会議資料を金曜日までに送ってください。",
|
|
expectedLanguage: "ja",
|
|
isClear: true
|
|
),
|
|
LanguageSample(
|
|
id: "ko-clear",
|
|
text: "다음 주 회의 자료를 금요일까지 보내 주세요.",
|
|
expectedLanguage: "ko",
|
|
isClear: true
|
|
),
|
|
LanguageSample(
|
|
id: "fr-clear",
|
|
text: "Veuillez envoyer la proposition révisée avant vendredi.",
|
|
expectedLanguage: "fr",
|
|
isClear: true
|
|
),
|
|
LanguageSample(
|
|
id: "es-clear",
|
|
text: "Por favor, envía la propuesta revisada antes del viernes.",
|
|
expectedLanguage: "es",
|
|
isClear: true
|
|
),
|
|
LanguageSample(
|
|
id: "zh-mixed",
|
|
text: "请 review 一下这个 PR,确认 API response 有没有 breaking change。",
|
|
expectedLanguage: nil,
|
|
isClear: false
|
|
),
|
|
LanguageSample(
|
|
id: "short-ok",
|
|
text: "OK",
|
|
expectedLanguage: nil,
|
|
isClear: false
|
|
),
|
|
LanguageSample(
|
|
id: "short-han",
|
|
text: "行",
|
|
expectedLanguage: nil,
|
|
isClear: false
|
|
),
|
|
LanguageSample(
|
|
id: "brand",
|
|
text: "Apple Intelligence",
|
|
expectedLanguage: nil,
|
|
isClear: false
|
|
),
|
|
LanguageSample(
|
|
id: "numbers",
|
|
text: "2026-08-21 15:30",
|
|
expectedLanguage: nil,
|
|
isClear: false
|
|
)
|
|
]
|
|
}
|
|
|
|
private func evaluateEntities() -> [EntityResult] {
|
|
makeEntitySamples().map { sample in
|
|
let tagger = NLTagger(tagSchemes: [.nameType])
|
|
tagger.string = sample.text
|
|
tagger.setLanguage(
|
|
sample.language,
|
|
range: sample.text.startIndex..<sample.text.endIndex
|
|
)
|
|
|
|
var detected: [EntityMatch] = []
|
|
tagger.enumerateTags(
|
|
in: sample.text.startIndex..<sample.text.endIndex,
|
|
unit: .word,
|
|
scheme: .nameType,
|
|
options: [.omitWhitespace, .omitPunctuation, .joinNames]
|
|
) { tag, range in
|
|
guard let tag else { return true }
|
|
detected.append(
|
|
EntityMatch(
|
|
text: String(sample.text[range]),
|
|
tag: tag.rawValue
|
|
)
|
|
)
|
|
return true
|
|
}
|
|
|
|
let expected = sample.expected.map {
|
|
EntityMatch(text: $0.text, tag: $0.tag.rawValue)
|
|
}
|
|
let matchedCount = expected.filter { expectation in
|
|
detected.contains { candidate in
|
|
candidate.tag == expectation.tag
|
|
&& normalized(candidate.text).contains(normalized(expectation.text))
|
|
}
|
|
}.count
|
|
let exactMatchedCount = expected.filter { expectation in
|
|
detected.contains { candidate in
|
|
candidate.tag == expectation.tag
|
|
&& normalized(candidate.text) == normalized(expectation.text)
|
|
}
|
|
}.count
|
|
|
|
return EntityResult(
|
|
id: sample.id,
|
|
text: sample.text,
|
|
expected: expected,
|
|
detected: detected,
|
|
matchedCount: matchedCount,
|
|
exactMatchedCount: exactMatchedCount
|
|
)
|
|
}
|
|
}
|
|
|
|
private func makeEntitySamples() -> [EntitySample] {
|
|
[
|
|
EntitySample(
|
|
id: "en-people-org-place",
|
|
text: "Tim Cook will meet Microsoft executives in Seattle.",
|
|
language: .english,
|
|
expected: [
|
|
EntityExpectation(text: "Tim Cook", tag: .personalName),
|
|
EntityExpectation(text: "Microsoft", tag: .organizationName),
|
|
EntityExpectation(text: "Seattle", tag: .placeName)
|
|
]
|
|
),
|
|
EntitySample(
|
|
id: "en-business",
|
|
text: "Sarah from Acme Corporation is visiting London next week.",
|
|
language: .english,
|
|
expected: [
|
|
EntityExpectation(text: "Sarah", tag: .personalName),
|
|
EntityExpectation(text: "Acme Corporation", tag: .organizationName),
|
|
EntityExpectation(text: "London", tag: .placeName)
|
|
]
|
|
),
|
|
EntitySample(
|
|
id: "zh-people-org-place",
|
|
text: "李雷下周去上海拜访腾讯公司。",
|
|
language: .simplifiedChinese,
|
|
expected: [
|
|
EntityExpectation(text: "李雷", tag: .personalName),
|
|
EntityExpectation(text: "上海", tag: .placeName),
|
|
EntityExpectation(text: "腾讯公司", tag: .organizationName)
|
|
]
|
|
),
|
|
EntitySample(
|
|
id: "zh-business",
|
|
text: "王芳将在深圳与华为团队讨论新项目。",
|
|
language: .simplifiedChinese,
|
|
expected: [
|
|
EntityExpectation(text: "王芳", tag: .personalName),
|
|
EntityExpectation(text: "深圳", tag: .placeName),
|
|
EntityExpectation(text: "华为", tag: .organizationName)
|
|
]
|
|
)
|
|
]
|
|
}
|
|
|
|
private func makeDataDetector() throws -> NSDataDetector {
|
|
let types: NSTextCheckingResult.CheckingType = [
|
|
.link,
|
|
.phoneNumber,
|
|
.date,
|
|
.address
|
|
]
|
|
return try NSDataDetector(types: types.rawValue)
|
|
}
|
|
|
|
private func evaluateDataDetection(
|
|
using detector: NSDataDetector
|
|
) -> [DetectorResult] {
|
|
makeDetectorSamples().map { sample in
|
|
let range = NSRange(sample.text.startIndex..., in: sample.text)
|
|
let detected = detector.matches(
|
|
in: sample.text,
|
|
options: [],
|
|
range: range
|
|
).compactMap { match -> DetectorMatch? in
|
|
guard let swiftRange = Range(match.range, in: sample.text),
|
|
let type = detectorTypeName(match.resultType) else {
|
|
return nil
|
|
}
|
|
return DetectorMatch(
|
|
type: type,
|
|
text: String(sample.text[swiftRange])
|
|
)
|
|
}
|
|
let detectedTypes = Set(detected.map(\.type))
|
|
return DetectorResult(
|
|
id: sample.id,
|
|
text: sample.text,
|
|
expectedTypes: sample.expectedTypes.sorted(),
|
|
detected: detected,
|
|
matchedTypes: sample.expectedTypes
|
|
.intersection(detectedTypes)
|
|
.sorted()
|
|
)
|
|
}
|
|
}
|
|
|
|
private func makeDetectorSamples() -> [DetectorSample] {
|
|
[
|
|
DetectorSample(
|
|
id: "en-url-phone",
|
|
text: "See https://www.apple.com and call +1 408-996-1010.",
|
|
expectedTypes: ["link", "phone"]
|
|
),
|
|
DetectorSample(
|
|
id: "en-date",
|
|
text: "Let's meet on August 28, 2026 at 3:00 PM.",
|
|
expectedTypes: ["date"]
|
|
),
|
|
DetectorSample(
|
|
id: "en-address",
|
|
text: "Please navigate to 1 Apple Park Way, Cupertino, CA 95014.",
|
|
expectedTypes: ["address"]
|
|
),
|
|
DetectorSample(
|
|
id: "zh-url-phone",
|
|
text: "详情见 https://www.apple.com.cn,联系电话 400-666-8800。",
|
|
expectedTypes: ["link", "phone"]
|
|
),
|
|
DetectorSample(
|
|
id: "zh-date",
|
|
text: "会议安排在2026年8月28日下午3点。",
|
|
expectedTypes: ["date"]
|
|
),
|
|
DetectorSample(
|
|
id: "zh-address",
|
|
text: "请导航到深圳市南山区科技园科苑路15号。",
|
|
expectedTypes: ["address"]
|
|
)
|
|
]
|
|
}
|
|
|
|
private func detectorTypeName(
|
|
_ type: NSTextCheckingResult.CheckingType
|
|
) -> String? {
|
|
switch type {
|
|
case .link:
|
|
return "link"
|
|
case .phoneNumber:
|
|
return "phone"
|
|
case .date:
|
|
return "date"
|
|
case .address:
|
|
return "address"
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private func evaluateSemantics(
|
|
_ corpus: SemanticCorpus
|
|
) -> SemanticLanguageReport {
|
|
guard let embedding = NLEmbedding.sentenceEmbedding(for: corpus.language) else {
|
|
return SemanticLanguageReport(
|
|
language: corpus.languageID,
|
|
embeddingAvailable: false,
|
|
dimension: nil,
|
|
revision: nil,
|
|
controlAccuracy: nil,
|
|
topOneAccuracy: nil,
|
|
topThreeAccuracy: nil,
|
|
regularTopOneAccuracy: nil,
|
|
adversarialTopOneAccuracy: nil,
|
|
controls: [],
|
|
results: []
|
|
)
|
|
}
|
|
|
|
let anchorVectors = corpus.anchors.map { anchor in
|
|
(
|
|
skillID: anchor.skillID,
|
|
vectors: anchor.examples.compactMap(embedding.vector(for:))
|
|
)
|
|
}
|
|
let results = corpus.samples.map { sample in
|
|
let sampleVector = embedding.vector(for: sample.text)
|
|
let distances = anchorVectors.map { anchor in
|
|
let distance = sampleVector.map { vector in
|
|
anchor.vectors
|
|
.map { cosineDistance(vector, $0) }
|
|
.min() ?? 2
|
|
} ?? 2
|
|
return SkillDistance(
|
|
skillID: anchor.skillID,
|
|
distance: rounded(distance)
|
|
)
|
|
}.sorted { $0.distance < $1.distance }
|
|
let predicted = distances.first?.skillID
|
|
let topThree = Array(distances.prefix(3))
|
|
return SemanticResult(
|
|
id: sample.id,
|
|
text: sample.text,
|
|
expectedSkillID: sample.expectedSkillID,
|
|
predictedSkillID: predicted,
|
|
topThree: topThree,
|
|
topOneCorrect: predicted == sample.expectedSkillID,
|
|
topThreeCorrect: topThree.contains {
|
|
$0.skillID == sample.expectedSkillID
|
|
},
|
|
isAdversarial: sample.isAdversarial
|
|
)
|
|
}
|
|
|
|
let controls = semanticControls(for: corpus.language).map { control in
|
|
let query = embedding.vector(for: control.query)
|
|
let related = embedding.vector(for: control.related)
|
|
let unrelated = embedding.vector(for: control.unrelated)
|
|
let relatedDistance = pairwiseDistance(query, related)
|
|
let unrelatedDistance = pairwiseDistance(query, unrelated)
|
|
let passed: Bool
|
|
if let relatedDistance, let unrelatedDistance {
|
|
passed = relatedDistance < unrelatedDistance
|
|
} else {
|
|
passed = false
|
|
}
|
|
return SemanticControlResult(
|
|
id: control.id,
|
|
relatedDistance: relatedDistance.map(rounded),
|
|
unrelatedDistance: unrelatedDistance.map(rounded),
|
|
passed: passed
|
|
)
|
|
}
|
|
let regular = results.filter { !$0.isAdversarial }
|
|
let adversarial = results.filter(\.isAdversarial)
|
|
return SemanticLanguageReport(
|
|
language: corpus.languageID,
|
|
embeddingAvailable: true,
|
|
dimension: embedding.dimension,
|
|
revision: embedding.revision,
|
|
controlAccuracy: accuracy(controls, keyPath: \.passed),
|
|
topOneAccuracy: accuracy(results, keyPath: \.topOneCorrect),
|
|
topThreeAccuracy: accuracy(results, keyPath: \.topThreeCorrect),
|
|
regularTopOneAccuracy: accuracy(regular, keyPath: \.topOneCorrect),
|
|
adversarialTopOneAccuracy: accuracy(adversarial, keyPath: \.topOneCorrect),
|
|
controls: controls,
|
|
results: results
|
|
)
|
|
}
|
|
|
|
private func semanticControls(for language: NLLanguage) -> [SemanticControl] {
|
|
if language == .simplifiedChinese {
|
|
return [
|
|
SemanticControl(
|
|
id: "zh-meeting-paraphrase",
|
|
query: "明天下午三点开会",
|
|
related: "会议安排在明天下午三点",
|
|
unrelated: "这个苹果吃起来很甜"
|
|
),
|
|
SemanticControl(
|
|
id: "zh-business-paraphrase",
|
|
query: "请确认报价和交付日期",
|
|
related: "麻烦核实价格以及什么时候可以交货",
|
|
unrelated: "周末我准备去公园跑步"
|
|
),
|
|
SemanticControl(
|
|
id: "zh-navigation-paraphrase",
|
|
query: "导航到深圳南山区科苑路",
|
|
related: "带我去南山区科苑路",
|
|
unrelated: "总结这份季度报告"
|
|
)
|
|
]
|
|
}
|
|
return [
|
|
SemanticControl(
|
|
id: "en-meeting-paraphrase",
|
|
query: "The meeting starts tomorrow at 3 PM.",
|
|
related: "We are scheduled to meet at three tomorrow afternoon.",
|
|
unrelated: "This apple tastes very sweet."
|
|
),
|
|
SemanticControl(
|
|
id: "en-business-paraphrase",
|
|
query: "Please confirm the price and delivery date.",
|
|
related: "Could you verify the quotation and when it will arrive?",
|
|
unrelated: "I plan to run in the park this weekend."
|
|
),
|
|
SemanticControl(
|
|
id: "en-navigation-paraphrase",
|
|
query: "Navigate to Apple Park in Cupertino.",
|
|
related: "Take me to the Apple Park campus.",
|
|
unrelated: "Summarize the quarterly report."
|
|
)
|
|
]
|
|
}
|
|
|
|
private func makeSemanticCorpora() -> [SemanticCorpus] {
|
|
[
|
|
SemanticCorpus(
|
|
language: .english,
|
|
languageID: "en",
|
|
anchors: englishAnchors,
|
|
samples: englishSemanticSamples
|
|
),
|
|
SemanticCorpus(
|
|
language: .simplifiedChinese,
|
|
languageID: "zh-Hans",
|
|
anchors: chineseAnchors,
|
|
samples: chineseSemanticSamples
|
|
)
|
|
]
|
|
}
|
|
|
|
private var englishAnchors: [SemanticAnchor] {
|
|
[
|
|
SemanticAnchor(
|
|
skillID: "reply",
|
|
examples: [
|
|
"A personal message asks me a direct question and expects an answer.",
|
|
"Someone is waiting for my response in a conversation."
|
|
]
|
|
),
|
|
SemanticAnchor(
|
|
skillID: "summarize",
|
|
examples: [
|
|
"A long article explains a topic with many facts and details.",
|
|
"A lengthy document needs its main points condensed."
|
|
]
|
|
),
|
|
SemanticAnchor(
|
|
skillID: "extractEvents",
|
|
examples: [
|
|
"An event invitation contains a date, time, and meeting place.",
|
|
"A scheduled meeting should be added to a calendar."
|
|
]
|
|
),
|
|
SemanticAnchor(
|
|
skillID: "extractTodos",
|
|
examples: [
|
|
"A checklist contains several tasks that need to be completed.",
|
|
"These action items should be turned into a to-do list."
|
|
]
|
|
),
|
|
SemanticAnchor(
|
|
skillID: "navigate",
|
|
examples: [
|
|
"A street address describes a physical destination.",
|
|
"This location should be opened for navigation."
|
|
]
|
|
),
|
|
SemanticAnchor(
|
|
skillID: "businessReply",
|
|
examples: [
|
|
"A formal business email requires a professional response.",
|
|
"A client is discussing a proposal, price, contract, or deadline."
|
|
]
|
|
)
|
|
]
|
|
}
|
|
|
|
private var chineseAnchors: [SemanticAnchor] {
|
|
[
|
|
SemanticAnchor(
|
|
skillID: "reply",
|
|
examples: [
|
|
"一条私人消息正在直接询问我,并等待我的回答。",
|
|
"对方在聊天中提出问题,需要我回复。"
|
|
]
|
|
),
|
|
SemanticAnchor(
|
|
skillID: "summarize",
|
|
examples: [
|
|
"一篇很长的文章包含大量事实、解释和细节。",
|
|
"一份长文档需要提炼重点并缩短篇幅。"
|
|
]
|
|
),
|
|
SemanticAnchor(
|
|
skillID: "extractEvents",
|
|
examples: [
|
|
"活动邀请中包含日期、时间和开会地点。",
|
|
"一项已经安排的会议需要加入日历。"
|
|
]
|
|
),
|
|
SemanticAnchor(
|
|
skillID: "extractTodos",
|
|
examples: [
|
|
"清单中包含多项需要完成的任务。",
|
|
"这些行动项需要整理成待办事项。"
|
|
]
|
|
),
|
|
SemanticAnchor(
|
|
skillID: "navigate",
|
|
examples: [
|
|
"这是一处可以导航前往的街道地址。",
|
|
"文本描述了一个具体地点和目的地。"
|
|
]
|
|
),
|
|
SemanticAnchor(
|
|
skillID: "businessReply",
|
|
examples: [
|
|
"正式商务邮件需要专业回复。",
|
|
"客户正在讨论报价、合同、交付时间或合作方案。"
|
|
]
|
|
)
|
|
]
|
|
}
|
|
|
|
private var englishSemanticSamples: [SemanticSample] {
|
|
[
|
|
SemanticSample(
|
|
id: "en-reply",
|
|
text: "Are you free for a quick call after lunch?",
|
|
expectedSkillID: "reply",
|
|
isAdversarial: false
|
|
),
|
|
SemanticSample(
|
|
id: "en-summary",
|
|
text: """
|
|
The report reviews renewable energy adoption across twelve regions.
|
|
It compares installation costs, grid capacity, policy incentives,
|
|
and five-year demand forecasts before outlining three scenarios.
|
|
""",
|
|
expectedSkillID: "summarize",
|
|
isAdversarial: false
|
|
),
|
|
SemanticSample(
|
|
id: "en-event",
|
|
text: "Design review is Friday, August 28 at 3 PM in Meeting Room 5.",
|
|
expectedSkillID: "extractEvents",
|
|
isAdversarial: false
|
|
),
|
|
SemanticSample(
|
|
id: "en-todos",
|
|
text: "Update the deck\nEmail the client\nBook the meeting room",
|
|
expectedSkillID: "extractTodos",
|
|
isAdversarial: false
|
|
),
|
|
SemanticSample(
|
|
id: "en-navigation",
|
|
text: "1 Apple Park Way, Cupertino, CA 95014",
|
|
expectedSkillID: "navigate",
|
|
isAdversarial: false
|
|
),
|
|
SemanticSample(
|
|
id: "en-business",
|
|
text: "Could you revise the quotation and confirm the delivery deadline?",
|
|
expectedSkillID: "businessReply",
|
|
isAdversarial: false
|
|
),
|
|
SemanticSample(
|
|
id: "en-keyword-trap",
|
|
text: "Can you summarize the contract and send me your answer?",
|
|
expectedSkillID: "reply",
|
|
isAdversarial: true
|
|
),
|
|
SemanticSample(
|
|
id: "en-date-in-article",
|
|
text: "The article says the company was founded on August 28, 1976.",
|
|
expectedSkillID: "summarize",
|
|
isAdversarial: true
|
|
),
|
|
SemanticSample(
|
|
id: "en-address-in-question",
|
|
text: "Is 1 Apple Park Way still your billing address?",
|
|
expectedSkillID: "reply",
|
|
isAdversarial: true
|
|
)
|
|
]
|
|
}
|
|
|
|
private var chineseSemanticSamples: [SemanticSample] {
|
|
[
|
|
SemanticSample(
|
|
id: "zh-reply",
|
|
text: "你今天下班以后有时间聊一下吗?",
|
|
expectedSkillID: "reply",
|
|
isAdversarial: false
|
|
),
|
|
SemanticSample(
|
|
id: "zh-summary",
|
|
text: """
|
|
这份报告比较了十二个地区的可再生能源应用情况,分析了安装成本、
|
|
电网容量、政策激励与未来五年的需求预测,最后提出了三种发展情景。
|
|
""",
|
|
expectedSkillID: "summarize",
|
|
isAdversarial: false
|
|
),
|
|
SemanticSample(
|
|
id: "zh-event",
|
|
text: "设计评审定在8月28日星期五下午3点,地点是五号会议室。",
|
|
expectedSkillID: "extractEvents",
|
|
isAdversarial: false
|
|
),
|
|
SemanticSample(
|
|
id: "zh-todos",
|
|
text: "更新演示文稿\n给客户发邮件\n预订会议室",
|
|
expectedSkillID: "extractTodos",
|
|
isAdversarial: false
|
|
),
|
|
SemanticSample(
|
|
id: "zh-navigation",
|
|
text: "深圳市南山区科技园科苑路15号",
|
|
expectedSkillID: "navigate",
|
|
isAdversarial: false
|
|
),
|
|
SemanticSample(
|
|
id: "zh-business",
|
|
text: "请更新报价,并确认最终交付时间和付款条件。",
|
|
expectedSkillID: "businessReply",
|
|
isAdversarial: false
|
|
),
|
|
SemanticSample(
|
|
id: "zh-keyword-trap",
|
|
text: "你能先总结一下合同,再告诉我你的意见吗?",
|
|
expectedSkillID: "reply",
|
|
isAdversarial: true
|
|
),
|
|
SemanticSample(
|
|
id: "zh-date-in-article",
|
|
text: "文章提到这家公司成立于1976年8月28日。",
|
|
expectedSkillID: "summarize",
|
|
isAdversarial: true
|
|
),
|
|
SemanticSample(
|
|
id: "zh-address-in-question",
|
|
text: "科苑路15号还是你们现在的账单地址吗?",
|
|
expectedSkillID: "reply",
|
|
isAdversarial: true
|
|
)
|
|
]
|
|
}
|
|
|
|
private func benchmark(
|
|
detector: NSDataDetector,
|
|
semanticCorpora: [SemanticCorpus]
|
|
) -> [LatencyReport] {
|
|
let iterations = 50
|
|
let languageText = "请确认明天下午的会议时间,并把更新后的方案发给客户。"
|
|
let detectorText = "Meeting: August 28, 2026 at 3 PM, https://example.com"
|
|
var reports = [
|
|
latencyReport(
|
|
operation: "language-recognition",
|
|
iterations: iterations
|
|
) {
|
|
let recognizer = NLLanguageRecognizer()
|
|
recognizer.processString(languageText)
|
|
_ = recognizer.languageHypotheses(withMaximum: 3)
|
|
},
|
|
latencyReport(
|
|
operation: "data-detection",
|
|
iterations: iterations
|
|
) {
|
|
_ = detector.matches(
|
|
in: detectorText,
|
|
options: [],
|
|
range: NSRange(detectorText.startIndex..., in: detectorText)
|
|
)
|
|
}
|
|
]
|
|
|
|
if let english = semanticCorpora.first,
|
|
let embedding = NLEmbedding.sentenceEmbedding(for: english.language) {
|
|
let anchorVectors = english.anchors.flatMap(\.examples)
|
|
.compactMap(embedding.vector(for:))
|
|
reports.append(
|
|
latencyReport(
|
|
operation: "semantic-routing-precomputed-anchors",
|
|
iterations: iterations
|
|
) {
|
|
guard let vector = embedding.vector(
|
|
for: "Can you call me after lunch?"
|
|
) else {
|
|
return
|
|
}
|
|
_ = anchorVectors.map { cosineDistance(vector, $0) }.min()
|
|
}
|
|
)
|
|
}
|
|
return reports
|
|
}
|
|
|
|
private func latencyReport(
|
|
operation: String,
|
|
iterations: Int,
|
|
body: () -> Void
|
|
) -> LatencyReport {
|
|
let start = ProcessInfo.processInfo.systemUptime
|
|
for _ in 0..<iterations {
|
|
body()
|
|
}
|
|
let elapsed = ProcessInfo.processInfo.systemUptime - start
|
|
return LatencyReport(
|
|
operation: operation,
|
|
iterations: iterations,
|
|
averageMilliseconds: rounded(elapsed * 1_000 / Double(iterations))
|
|
)
|
|
}
|
|
|
|
private func accuracy<T>(
|
|
_ values: [T],
|
|
keyPath: KeyPath<T, Bool>
|
|
) -> Double? {
|
|
guard !values.isEmpty else { return nil }
|
|
return ratio(
|
|
numerator: values.filter { $0[keyPath: keyPath] }.count,
|
|
denominator: values.count
|
|
)
|
|
}
|
|
|
|
private func ratio(numerator: Int, denominator: Int) -> Double {
|
|
guard denominator > 0 else { return 0 }
|
|
return rounded(Double(numerator) / Double(denominator))
|
|
}
|
|
|
|
private func rounded(_ value: Double) -> Double {
|
|
(value * 10_000).rounded() / 10_000
|
|
}
|
|
|
|
private func normalized(_ text: String) -> String {
|
|
text.folding(
|
|
options: [.caseInsensitive, .diacriticInsensitive],
|
|
locale: .current
|
|
)
|
|
}
|
|
|
|
private func pairwiseDistance(
|
|
_ first: [Double]?,
|
|
_ second: [Double]?
|
|
) -> Double? {
|
|
guard let first, let second else { return nil }
|
|
return cosineDistance(first, second)
|
|
}
|
|
|
|
private func cosineDistance(_ first: [Double], _ second: [Double]) -> Double {
|
|
guard first.count == second.count, !first.isEmpty else { return 2 }
|
|
var dotProduct = 0.0
|
|
var firstMagnitude = 0.0
|
|
var secondMagnitude = 0.0
|
|
for index in first.indices {
|
|
dotProduct += first[index] * second[index]
|
|
firstMagnitude += first[index] * first[index]
|
|
secondMagnitude += second[index] * second[index]
|
|
}
|
|
guard firstMagnitude > 0, secondMagnitude > 0 else { return 2 }
|
|
return 1 - dotProduct / (firstMagnitude.squareRoot() * secondMagnitude.squareRoot())
|
|
}
|
|
}
|