Cursor: Apply local changes for cloud agent

This commit is contained in:
Rocky
2026-08-27 18:01:46 +08:00
parent 39002336c0
commit 42e6252f01
148 changed files with 120105 additions and 8122 deletions
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""Apply the reviewed deployment thresholds and pinned model selections."""
from __future__ import annotations
import argparse
import json
import shutil
from pathlib import Path
DEFAULT_RESOURCE_DIRECTORY = Path(
"OSGKeyboardShared/Resources/ClipboardSemantics"
)
DEFAULT_BASELINE_DIRECTORY = Path(
"ModelTraining/ClipboardSemantics/baselines/2026-08-26-nine-model-v1"
)
# Core thresholds were selected on the 680-record development holdout and
# checked against the golden gate. Task and complaint were later tightened on
# focused development gates; the 20260830 release profile is acceptance-only.
THRESHOLD_POLICY = {
"task": {
"global": 0.88,
"byLanguage": {"en": 0.88, "zh-Hans": 0.73},
},
"complaint": {
"global": 0.82,
"byLanguage": {"en": 0.84, "zh-Hans": 0.82},
},
"scheduleNegotiation": {
"global": 0.68,
"byLanguage": {"en": 0.68, "zh-Hans": 0.53},
},
"confirmationDecision": {
"global": 0.72,
"byLanguage": {"en": 0.72, "zh-Hans": 0.72},
},
"followUpReminder": {
"global": 0.73,
"byLanguage": {"en": 0.73, "zh-Hans": 0.73},
},
"blessing": {
"global": 0.69,
"byLanguage": {"en": 0.69, "zh-Hans": 0.77},
},
}
PINNED_MODELS = {
"scheduleNegotiation": "ScheduleNegotiationIntentClassifier.mlmodel",
}
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
"--resource-directory",
type=Path,
default=DEFAULT_RESOURCE_DIRECTORY,
)
parser.add_argument(
"--baseline-directory",
type=Path,
default=DEFAULT_BASELINE_DIRECTORY,
)
parser.add_argument(
"--candidate-resource-directory",
type=Path,
default=None,
)
parser.add_argument(
"--promote-classifier",
action="append",
default=[],
)
return parser.parse_args()
def main() -> None:
arguments = parse_arguments()
resource_directory: Path = arguments.resource_directory
baseline_directory: Path = arguments.baseline_directory
manifest_path = resource_directory / "clipboard-semantic-models.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
classifiers = {
classifier["id"]: classifier
for classifier in manifest["classifiers"]
}
promoted_classifiers = arguments.promote_classifier
if promoted_classifiers:
candidate_directory: Path | None = arguments.candidate_resource_directory
if candidate_directory is None:
raise RuntimeError(
"--candidate-resource-directory is required when promoting classifiers"
)
candidate_manifest_path = (
candidate_directory / "clipboard-semantic-models.json"
)
candidate_manifest = json.loads(
candidate_manifest_path.read_text(encoding="utf-8")
)
candidates = {
classifier["id"]: classifier
for classifier in candidate_manifest["classifiers"]
}
for classifier_id in promoted_classifiers:
if classifier_id not in candidates:
raise RuntimeError(
f"Cannot promote missing classifier: {classifier_id}"
)
candidate = dict(candidates[classifier_id])
candidate["trainedAt"] = candidate_manifest["generatedAt"]
candidate["trainingCorpusRecordCount"] = candidate_manifest[
"corpusRecordCount"
]
model_file = candidate["modelFile"]
shutil.copy2(
candidate_directory / model_file,
resource_directory / model_file,
)
if classifier_id in classifiers:
classifiers[classifier_id].clear()
classifiers[classifier_id].update(candidate)
else:
manifest["classifiers"].append(candidate)
classifiers[classifier_id] = candidate
missing = sorted(set(THRESHOLD_POLICY) - set(classifiers))
if missing:
raise RuntimeError(f"Manifest is missing classifiers: {missing}")
for classifier_id, policy in THRESHOLD_POLICY.items():
classifier = classifiers[classifier_id]
classifier["acceptedForAutomaticRouting"] = True
classifier["confidenceThreshold"] = policy["global"]
classifier["confidenceThresholdsByLanguage"] = policy["byLanguage"]
for classifier_id, model_file in PINNED_MODELS.items():
source = baseline_directory / model_file
destination = resource_directory / classifiers[classifier_id]["modelFile"]
if not source.is_file():
raise RuntimeError(f"Pinned model is missing: {source}")
shutil.copy2(source, destination)
manifest_path.write_text(
json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
print(
"DEPLOYMENT_POLICY_DONE "
f"thresholds={len(THRESHOLD_POLICY)} pinnedModels={len(PINNED_MODELS)} "
f"promotedModels={len(promoted_classifiers)}"
)
if __name__ == "__main__":
main()
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""Promote only acceptance-gated verifiers into privacy-safe shadow mode."""
from __future__ import annotations
import argparse
import hashlib
import json
import shutil
from datetime import datetime, timezone
from pathlib import Path
DEFAULT_CANDIDATE_DIRECTORY = Path(
"ModelTraining/ClipboardSemantics/VerifierCandidates"
)
DEFAULT_RESOURCE_DIRECTORY = Path(
"OSGKeyboardShared/Resources/ClipboardSemantics"
)
DEFAULT_REPORT = Path(
"ModelTraining/ClipboardSemantics/verifier-deployment-report.json"
)
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(128 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
"--candidate-directory",
type=Path,
default=DEFAULT_CANDIDATE_DIRECTORY,
)
parser.add_argument(
"--resource-directory",
type=Path,
default=DEFAULT_RESOURCE_DIRECTORY,
)
parser.add_argument("--report", type=Path, default=DEFAULT_REPORT)
parser.add_argument(
"--apply",
action="store_true",
help="Copy eligible models and update the deployed manifest.",
)
return parser.parse_args()
def main() -> None:
arguments = parse_arguments()
candidate_manifest_path = (
arguments.candidate_directory / "clipboard-semantic-models.json"
)
deployed_manifest_path = (
arguments.resource_directory / "clipboard-semantic-models.json"
)
candidate_manifest = json.loads(
candidate_manifest_path.read_text(encoding="utf-8")
)
deployed_manifest = json.loads(
deployed_manifest_path.read_text(encoding="utf-8")
)
candidate_verifiers = candidate_manifest.get("verifiers") or []
eligible = [
verifier
for verifier in candidate_verifiers
if verifier.get("acceptedForAutomaticRouting") is True
and verifier.get("deploymentMode") == "automatic"
]
entries = []
for verifier in candidate_verifiers:
model_path = arguments.candidate_directory / verifier["modelFile"]
if not model_path.is_file():
raise FileNotFoundError(model_path)
entries.append(
{
"id": verifier["id"],
"eligible": verifier in eligible,
"candidateDeploymentMode": verifier.get("deploymentMode"),
"candidateSHA256": sha256(model_path),
"decision": (
"promote-to-shadow"
if verifier in eligible
else "retain-current-models"
),
}
)
applied = False
if arguments.apply and eligible:
promoted_verifiers = []
for verifier in eligible:
source = arguments.candidate_directory / verifier["modelFile"]
destination = arguments.resource_directory / verifier["modelFile"]
shutil.copy2(source, destination)
shadow = dict(verifier)
shadow["acceptedForAutomaticRouting"] = False
shadow["deploymentMode"] = "shadow"
shadow["candidatePassedAcceptance"] = True
promoted_verifiers.append(shadow)
# Always start from the deployed manifest so binary classifiers cannot
# be replaced by an experimental candidate as a side effect.
deployed_manifest["schemaVersion"] = 3
deployed_manifest["verifiers"] = promoted_verifiers
deployed_manifest["verifierGeneratedAt"] = candidate_manifest.get(
"verifierGeneratedAt"
)
deployed_manifest["verifierLabelPolicy"] = candidate_manifest.get(
"verifierLabelPolicy"
)
deployed_manifest_path.write_text(
json.dumps(
deployed_manifest,
ensure_ascii=False,
indent=2,
sort_keys=True,
)
+ "\n",
encoding="utf-8",
)
applied = True
report = {
"schemaVersion": 1,
"generatedAt": datetime.now(timezone.utc).isoformat(),
"applyRequested": arguments.apply,
"applied": applied,
"eligibleVerifierCount": len(eligible),
"deployedClassifierCountPreserved": len(
deployed_manifest.get("classifiers") or []
),
"policy": (
"Only acceptance-gated automatic candidates may be copied, and "
"their first deployed mode is forced to shadow. If none pass, the "
"deployed manifest and current binary models remain untouched."
),
"verifiers": entries,
}
arguments.report.parent.mkdir(parents=True, exist_ok=True)
arguments.report.write_text(
json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
print(
"VERIFIER_DEPLOYMENT_POLICY "
f"eligible={len(eligible)} applied={str(applied).lower()}"
)
if __name__ == "__main__":
main()
@@ -0,0 +1,915 @@
#!/usr/bin/env swift
import Foundation
import NaturalLanguage
private struct HoldoutRecord: Decodable {
let id: String
let text: String
let language: String
let split: String?
let family: String
let task: Bool
let question: Bool
let invitation: Bool
let complaint: Bool
let scheduleNegotiation: Bool
let confirmationDecision: Bool
let followUpReminder: Bool
let blessing: Bool?
let sentiment: String
let replyable: Bool
let sourceDataset: String?
func isPositive(for classifierID: String) -> Bool {
switch classifierID {
case "task": task
case "question": question
case "invitation": invitation
case "complaint": complaint
case "scheduleNegotiation": scheduleNegotiation
case "confirmationDecision": confirmationDecision
case "followUpReminder": followUpReminder
case "blessing": blessing ?? false
case "replyableMessage": replyable
default: false
}
}
func expectedVerifierLabel(for verifierID: String) -> String {
if verifierID == "action" {
if task && complaint {
return "both"
}
if task {
return "taskOnly"
}
if complaint {
return "complaintOnly"
}
if question {
return "questionRequest"
}
return "neither"
}
let coordinationLabels = [
invitation ? "invitation" : nil,
scheduleNegotiation ? "scheduleNegotiation" : nil,
confirmationDecision ? "confirmationDecision" : nil,
followUpReminder ? "followUpReminder" : nil
].compactMap { $0 }
return coordinationLabels.count == 1 ? coordinationLabels[0] : "neither"
}
}
private struct TrainingRecord: Decodable {
let text: String
}
private struct Manifest: Decodable {
let schemaVersion: Int
let classifiers: [ManifestClassifier]
let verifiers: [ManifestVerifier]?
}
private struct ManifestClassifier: Decodable {
let id: String
let modelFile: String
let positiveLabel: String?
let confidenceThreshold: Double?
let confidenceThresholdsByLanguage: [String: Double]?
let acceptedForAutomaticRouting: Bool
}
private struct ManifestVerifier: Decodable {
let id: String
let modelFile: String
let confidenceThreshold: Double
let confidenceThresholdsByLanguage: [String: Double]?
let minimumMargin: Double
let minimumMarginsByLanguage: [String: Double]?
let acceptedForAutomaticRouting: Bool
let deploymentMode: String
}
private struct BinaryMetrics: Encodable {
let total: Int
let truePositive: Int
let trueNegative: Int
let falsePositive: Int
let falseNegative: Int
let accuracy: Double
let precision: Double
let recall: Double
let f1: Double
}
private struct ErrorExample: Encodable {
let id: String
let language: String
let family: String
let sourceDataset: String?
let text: String
let confidence: Double
}
private struct BinaryEvaluation: Encodable {
let id: String
let metrics: BinaryMetrics
let metricsByLanguage: [String: BinaryMetrics]
let metricsBySource: [String: BinaryMetrics]
let thresholdAt90Precision: ThresholdRecommendation?
let thresholdAt95Precision: ThresholdRecommendation?
let thresholdsAt90PrecisionByLanguage: [String: ThresholdRecommendation]
let thresholdsAt95PrecisionByLanguage: [String: ThresholdRecommendation]
let falsePositiveExamples: [ErrorExample]
let falseNegativeExamples: [ErrorExample]
}
private struct ThresholdRecommendation: Encodable {
let threshold: Double
let metrics: BinaryMetrics
}
private struct SentimentMetrics: Encodable {
let total: Int
let correct: Int
let unknown: Int
let accuracy: Double
let unknownRate: Double
let macroF1: Double
let perLabelF1: [String: Double]
}
private struct AggregateMetrics: Encodable {
let accuracy: Double
let precision: Double
let recall: Double
let f1: Double
}
private struct VerifierRoutingMetrics: Encodable {
let total: Int
let expectedSpecialized: Int
let stageACandidates: Int
let routed: Int
let correctRouted: Int
let falseRouted: Int
let stageARecall: Double
let stageBExactAccuracy: Double
let finalPrecision: Double
let finalPrecisionWilsonLower95: Double
let finalRecall: Double
}
private struct VerifierEvaluation: Encodable {
let id: String
let deploymentMode: String
let acceptedForAutomaticRouting: Bool
let metrics: VerifierRoutingMetrics
let metricsByLanguage: [String: VerifierRoutingMetrics]
let metricsBySource: [String: VerifierRoutingMetrics]
let leaveOneSourceOut: [String: VerifierRoutingMetrics]
let coldLoadMilliseconds: Double
let warmMedianMilliseconds: Double
let warmP95Milliseconds: Double
}
private struct Report: Encodable {
let generatedAt: String
let seed: Int
let corpusRecordCount: Int
let familyCount: Int
let languageCounts: [String: Int]
let exactTrainingOverlapCount: Int
let manifestSchemaVersion: Int
let binaryMacro: AggregateMetrics
let binaryMacroByLanguage: [String: AggregateMetrics]
let classifiers: [BinaryEvaluation]
let verifierLayers: [VerifierEvaluation]
let sentiment: SentimentMetrics
let sentimentBySource: [String: SentimentMetrics]
}
private struct BinaryObservation {
let record: HoldoutRecord
let expected: Bool
let predicted: Bool
let confidence: Double
let isSuppressed: Bool
}
private struct VerifierObservation {
let record: HoldoutRecord
let expectedLabel: String
let isStageACandidate: Bool
let predictedLabel: String
let confidence: Double
let margin: Double
let isRouted: Bool
let latencyMilliseconds: Double
}
private let fileManager = FileManager.default
private let root = URL(fileURLWithPath: fileManager.currentDirectoryPath)
private func argumentValue(after flag: String) -> String? {
guard let index = CommandLine.arguments.firstIndex(of: flag),
CommandLine.arguments.indices.contains(index + 1) else {
return nil
}
return CommandLine.arguments[index + 1]
}
private let corpusURL = argumentValue(after: "--corpus").map {
URL(fileURLWithPath: $0, relativeTo: root).standardizedFileURL
} ?? root.appendingPathComponent(
"ModelTraining/ClipboardSemantics/random-holdout-corpus.jsonl"
)
private let trainingCorpusURL = root.appendingPathComponent(
"ModelTraining/ClipboardSemantics/clipboard_semantic_corpus.jsonl"
)
private let manifestURL = argumentValue(after: "--manifest").map {
URL(fileURLWithPath: $0, relativeTo: root).standardizedFileURL
} ?? root.appendingPathComponent(
"OSGKeyboardShared/Resources/ClipboardSemantics/clipboard-semantic-models.json"
)
private let modelDirectory = argumentValue(after: "--models").map {
URL(fileURLWithPath: $0, relativeTo: root).standardizedFileURL
} ?? manifestURL.deletingLastPathComponent()
private let reportURL = argumentValue(after: "--report").map {
URL(fileURLWithPath: $0, relativeTo: root).standardizedFileURL
} ?? root.appendingPathComponent(
"ModelTraining/ClipboardSemantics/random-holdout-report.json"
)
private let sentimentMinimumConfidence = 0.65
private let sentimentMinimumMargin = 0.15
private let holdoutSeed = argumentValue(after: "--seed").flatMap(Int.init) ?? 20260826
private let includesRejectedModels = CommandLine.arguments.contains(
"--include-rejected-models"
)
private let requestedSplit = argumentValue(after: "--split")
private func rounded(_ value: Double) -> Double {
guard value.isFinite else { return 0 }
return (value * 10_000).rounded() / 10_000
}
private func milliseconds(_ duration: Duration) -> Double {
Double(duration.components.seconds) * 1_000
+ Double(duration.components.attoseconds) / 1_000_000_000_000_000
}
private func decodeJSONLines<T: Decodable>(_ type: T.Type, from url: URL) throws -> [T] {
let content = try String(contentsOf: url, encoding: .utf8)
let decoder = JSONDecoder()
return try content.split(separator: "\n").map {
try decoder.decode(type, from: Data($0.utf8))
}
}
private func normalized(_ text: String) -> String {
text
.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: nil)
.split(whereSeparator: \.isWhitespace)
.joined(separator: " ")
}
private func compileModel(sourceURL: URL, outputDirectory: URL) throws -> URL {
let process = Process()
let outputPipe = Pipe()
process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun")
process.arguments = [
"coremlcompiler",
"compile",
sourceURL.path,
outputDirectory.path
]
process.standardOutput = outputPipe
process.standardError = outputPipe
try process.run()
process.waitUntilExit()
guard process.terminationStatus == 0 else {
let data = outputPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(bytes: data, encoding: .utf8) ?? ""
throw NSError(
domain: "RandomHoldoutEvaluation",
code: Int(process.terminationStatus),
userInfo: [NSLocalizedDescriptionKey: output]
)
}
let resourceName = sourceURL.deletingPathExtension().lastPathComponent
return outputDirectory.appendingPathComponent("\(resourceName).mlmodelc")
}
private func binaryMetrics(_ observations: [BinaryObservation]) -> BinaryMetrics {
var truePositive = 0
var trueNegative = 0
var falsePositive = 0
var falseNegative = 0
for observation in observations {
switch (observation.expected, observation.predicted) {
case (true, true): truePositive += 1
case (false, false): trueNegative += 1
case (false, true): falsePositive += 1
case (true, false): falseNegative += 1
}
}
let total = observations.count
let precision = truePositive + falsePositive > 0
? Double(truePositive) / Double(truePositive + falsePositive)
: 0
let recall = truePositive + falseNegative > 0
? Double(truePositive) / Double(truePositive + falseNegative)
: 0
let accuracy = total > 0
? Double(truePositive + trueNegative) / Double(total)
: 0
let f1 = precision + recall > 0
? 2 * precision * recall / (precision + recall)
: 0
return BinaryMetrics(
total: total,
truePositive: truePositive,
trueNegative: trueNegative,
falsePositive: falsePositive,
falseNegative: falseNegative,
accuracy: rounded(accuracy),
precision: rounded(precision),
recall: rounded(recall),
f1: rounded(f1)
)
}
private func thresholdRecommendation(
observations: [BinaryObservation],
minimumPrecision: Double
) -> ThresholdRecommendation? {
let candidates = stride(from: 0.05, through: 0.99, by: 0.01).compactMap { threshold
-> ThresholdRecommendation? in
let adjusted = observations.map {
BinaryObservation(
record: $0.record,
expected: $0.expected,
predicted: !$0.isSuppressed && $0.confidence >= threshold,
confidence: $0.confidence,
isSuppressed: $0.isSuppressed
)
}
let metrics = binaryMetrics(adjusted)
guard metrics.truePositive > 0, metrics.precision >= minimumPrecision else {
return nil
}
return ThresholdRecommendation(
threshold: rounded(threshold),
metrics: metrics
)
}
return candidates.max {
if $0.metrics.recall != $1.metrics.recall {
return $0.metrics.recall < $1.metrics.recall
}
if $0.metrics.precision != $1.metrics.precision {
return $0.metrics.precision < $1.metrics.precision
}
return $0.threshold > $1.threshold
}
}
private func shouldSuppressTask(
text: String,
complaintConfidence: Double
) -> Bool {
guard complaintConfidence >= 0.60 else { return false }
let normalized = text.lowercased()
let explicitTaskMarkers = [
"", "麻烦", "能否", "可以请你", "由你", "交给你", "需要你",
"你负责", "下一步", "行动项", "please ", "can you", "could you",
"would you", "assigned to you", "you are responsible", "we need you",
"would like you", "counting on you", "take ownership", "your task",
"next action", "complete the", "finish the", "send it to",
"deliver it to"
]
return !explicitTaskMarkers.contains { normalized.contains($0) }
}
private func hasExplicitBlessingMarker(in text: String) -> Bool {
let normalized = text.lowercased()
let quotedOrMetaContexts = [
"祝福模板", "祝福语模板", "文章引用", "搜索词", "系统正在检查",
"文档里收录", "贺卡名单", "收集祝福", "greeting template",
"message template", "the article quotes", "search phrase",
"system is checking", "document contains", "card list",
"quotes the phrase", "如何描述生日快乐", "怎么说生日快乐",
"如何写生日祝福", "how would you describe a happy birthday",
"how do you say happy birthday", "what does happy birthday mean",
"宁愿你", "祝你倒闭", "祝你立马倒闭", "祝你去死", "祝你倒霉",
"祝你失败", "祝你完蛋"
]
guard !quotedOrMetaContexts.contains(where: { normalized.contains($0) }) else {
return false
}
let markers = [
"生日快乐", "新年快乐", "春节快乐", "节日快乐", "圣诞快乐",
"中秋快乐", "恭喜", "预祝", "祝你", "祝您", "祝大家", "祝他", "祝她",
"愿你", "愿您", "happy birthday", "happy new year",
"merry christmas", "happy holidays", "congratulations",
"congrats", "best wishes", "good luck", "wishing you",
"wish you", "wish him", "wish her", "wish them", "let us wish",
"let's wish", "we wish", "may you"
]
return markers.contains { normalized.contains($0) }
}
private func aggregate(_ metrics: [BinaryMetrics]) -> AggregateMetrics {
let divisor = Double(max(metrics.count, 1))
return AggregateMetrics(
accuracy: rounded(metrics.reduce(0) { $0 + $1.accuracy } / divisor),
precision: rounded(metrics.reduce(0) { $0 + $1.precision } / divisor),
recall: rounded(metrics.reduce(0) { $0 + $1.recall } / divisor),
f1: rounded(metrics.reduce(0) { $0 + $1.f1 } / divisor)
)
}
private func errorExamples(
from observations: [BinaryObservation],
expected: Bool,
predicted: Bool
) -> [ErrorExample] {
observations
.filter { $0.expected == expected && $0.predicted == predicted }
.sorted { $0.confidence > $1.confidence }
.prefix(5)
.map {
ErrorExample(
id: $0.record.id,
language: $0.record.language,
family: $0.record.family,
sourceDataset: $0.record.sourceDataset,
text: $0.record.text,
confidence: rounded($0.confidence)
)
}
}
private func wilsonLowerBound(successes: Int, total: Int) -> Double {
guard total > 0 else { return 0 }
let z = 1.959_963_984_540_054
let proportion = Double(successes) / Double(total)
let denominator = 1 + z * z / Double(total)
let center = proportion + z * z / (2 * Double(total))
let adjustment = z * sqrt(
(
proportion * (1 - proportion)
+ z * z / (4 * Double(total))
) / Double(total)
)
return rounded((center - adjustment) / denominator)
}
private func verifierMetrics(
_ observations: [VerifierObservation]
) -> VerifierRoutingMetrics {
let expectedSpecialized = observations.filter {
$0.expectedLabel != "neither"
}.count
let stageACandidates = observations.filter(\.isStageACandidate).count
let stageATruePositives = observations.filter {
$0.isStageACandidate && $0.expectedLabel != "neither"
}.count
let stageBObservations = observations.filter(\.isStageACandidate)
let stageBCorrect = stageBObservations.filter {
$0.predictedLabel == $0.expectedLabel
}.count
let routed = observations.filter(\.isRouted)
let correctRouted = routed.filter {
$0.predictedLabel == $0.expectedLabel
}.count
let stageARecall = expectedSpecialized > 0
? Double(stageATruePositives) / Double(expectedSpecialized)
: 0
let stageBAccuracy = stageBObservations.isEmpty
? 0
: Double(stageBCorrect) / Double(stageBObservations.count)
let precision = routed.isEmpty
? 0
: Double(correctRouted) / Double(routed.count)
let recall = expectedSpecialized > 0
? Double(correctRouted) / Double(expectedSpecialized)
: 0
return VerifierRoutingMetrics(
total: observations.count,
expectedSpecialized: expectedSpecialized,
stageACandidates: stageACandidates,
routed: routed.count,
correctRouted: correctRouted,
falseRouted: routed.count - correctRouted,
stageARecall: rounded(stageARecall),
stageBExactAccuracy: rounded(stageBAccuracy),
finalPrecision: rounded(precision),
finalPrecisionWilsonLower95: wilsonLowerBound(
successes: correctRouted,
total: routed.count
),
finalRecall: rounded(recall)
)
}
private func percentile(_ values: [Double], proportion: Double) -> Double {
guard !values.isEmpty else { return 0 }
let sorted = values.sorted()
let index = min(
Int((Double(sorted.count - 1) * proportion).rounded()),
sorted.count - 1
)
return rounded(sorted[index])
}
private func candidateClassifierIDs(for verifierID: String) -> [String] {
if verifierID == "action" {
return ["task", "question", "complaint"]
}
return [
"invitation",
"scheduleNegotiation",
"confirmationDecision",
"followUpReminder"
]
}
private func isStageACandidate(
record: HoldoutRecord,
verifierID: String,
models: [String: NLModel],
configurations: [String: ManifestClassifier]
) -> Bool {
candidateClassifierIDs(for: verifierID).contains { classifierID in
guard let model = models[classifierID],
let configuration = configurations[classifierID],
let positiveLabel = configuration.positiveLabel else {
return false
}
let confidence = model.predictedLabelHypotheses(
for: record.text,
maximumCount: 2
)[positiveLabel] ?? 0
let configuredThreshold = configuration
.confidenceThresholdsByLanguage?[record.language]
?? configuration.confidenceThreshold
?? 1
return confidence >= min(configuredThreshold, 0.50)
}
}
private func sentimentMetrics(
records: [HoldoutRecord],
model: NLModel
) -> SentimentMetrics {
let labels = ["negative", "neutral", "positive"]
var confusion: [String: [String: Int]] = [:]
var correct = 0
var unknown = 0
for record in records {
let ranked = model.predictedLabelHypotheses(for: record.text, maximumCount: 3)
.sorted { $0.value > $1.value }
let winner = ranked.first
let runnerUp = ranked.dropFirst().first?.value ?? 0
let prediction: String
if let winner,
winner.value >= sentimentMinimumConfidence,
winner.value - runnerUp >= sentimentMinimumMargin {
prediction = winner.key
} else {
prediction = "unknown"
unknown += 1
}
confusion[record.sentiment, default: [:]][prediction, default: 0] += 1
if prediction == record.sentiment {
correct += 1
}
}
var perLabelF1: [String: Double] = [:]
for label in labels {
let truePositive = confusion[label]?[label] ?? 0
let falseNegative = (confusion[label] ?? [:])
.filter { $0.key != label }
.reduce(0) { $0 + $1.value }
let falsePositive = labels
.filter { $0 != label }
.reduce(0) { $0 + (confusion[$1]?[label] ?? 0) }
let precision = truePositive + falsePositive > 0
? Double(truePositive) / Double(truePositive + falsePositive)
: 0
let recall = truePositive + falseNegative > 0
? Double(truePositive) / Double(truePositive + falseNegative)
: 0
perLabelF1[label] = rounded(
precision + recall > 0
? 2 * precision * recall / (precision + recall)
: 0
)
}
let macroF1 = perLabelF1.values.reduce(0, +) / Double(labels.count)
return SentimentMetrics(
total: records.count,
correct: correct,
unknown: unknown,
accuracy: rounded(Double(correct) / Double(max(records.count, 1))),
unknownRate: rounded(Double(unknown) / Double(max(records.count, 1))),
macroF1: rounded(macroF1),
perLabelF1: perLabelF1
)
}
private func writeJSON<T: Encodable>(_ value: T, to url: URL) throws {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
try encoder.encode(value).write(to: url, options: .atomic)
}
private func main() throws {
let decodedRecords = try decodeJSONLines(HoldoutRecord.self, from: corpusURL)
let records = requestedSplit.map { split in
decodedRecords.filter { $0.split == split }
} ?? decodedRecords
let trainingRecords = try decodeJSONLines(TrainingRecord.self, from: trainingCorpusURL)
let manifest = try JSONDecoder().decode(
Manifest.self,
from: Data(contentsOf: manifestURL)
)
let trainingTexts = Set(trainingRecords.map { normalized($0.text) })
let exactOverlapCount = records.filter { trainingTexts.contains(normalized($0.text)) }.count
let temporaryDirectory = fileManager.temporaryDirectory.appendingPathComponent(
"osg-random-holdout-\(UUID().uuidString)",
isDirectory: true
)
try fileManager.createDirectory(
at: temporaryDirectory,
withIntermediateDirectories: true
)
defer { try? fileManager.removeItem(at: temporaryDirectory) }
var models: [String: NLModel] = [:]
let classifierConfigurations = Dictionary(
uniqueKeysWithValues: manifest.classifiers.map { ($0.id, $0) }
)
for configuration in manifest.classifiers {
let sourceURL = modelDirectory.appendingPathComponent(configuration.modelFile)
let compiledURL = try compileModel(
sourceURL: sourceURL,
outputDirectory: temporaryDirectory
)
let model = try NLModel(contentsOf: compiledURL)
models[configuration.id] = model
}
var verifierModels: [String: NLModel] = [:]
var verifierLoadMilliseconds: [String: Double] = [:]
for configuration in manifest.verifiers ?? [] {
let sourceURL = modelDirectory.appendingPathComponent(configuration.modelFile)
let compiledURL = try compileModel(
sourceURL: sourceURL,
outputDirectory: temporaryDirectory
)
let startedAt = ContinuousClock.now
verifierModels[configuration.id] = try NLModel(contentsOf: compiledURL)
verifierLoadMilliseconds[configuration.id] = milliseconds(
startedAt.duration(to: .now)
)
}
var binaryEvaluations: [BinaryEvaluation] = []
let sentimentResult = models["sentiment"].map {
sentimentMetrics(records: records, model: $0)
}
for configuration in manifest.classifiers {
if configuration.id == "sentiment" {
continue
}
guard let model = models[configuration.id] else { continue }
guard let positiveLabel = configuration.positiveLabel else {
continue
}
let observations = records.map { record in
let confidence = model.predictedLabelHypotheses(
for: record.text,
maximumCount: 2
)[positiveLabel] ?? 0
let threshold = configuration.confidenceThresholdsByLanguage?[record.language]
?? configuration.confidenceThreshold
?? 1
let complaintConfidence: Double
if configuration.id == "task", let complaintModel = models["complaint"] {
complaintConfidence = complaintModel.predictedLabelHypotheses(
for: record.text,
maximumCount: 2
)["complaint"] ?? 0
} else {
complaintConfidence = 0
}
let hasBlessingMarker = configuration.id == "blessing"
&& hasExplicitBlessingMarker(in: record.text)
let effectiveConfidence = hasBlessingMarker ? 1 : confidence
let isTaskSuppressed = configuration.id == "task"
&& shouldSuppressTask(
text: record.text,
complaintConfidence: complaintConfidence
)
let isBlessingSuppressed = configuration.id == "blessing"
&& !hasBlessingMarker
let isSuppressed = isTaskSuppressed || isBlessingSuppressed
return BinaryObservation(
record: record,
expected: record.isPositive(for: configuration.id),
predicted: (configuration.acceptedForAutomaticRouting || includesRejectedModels)
&& !isSuppressed
&& effectiveConfidence >= threshold,
confidence: effectiveConfidence,
isSuppressed: isSuppressed
)
}
let byLanguage = Dictionary(grouping: observations) { $0.record.language }
let metricsByLanguage = byLanguage.mapValues(binaryMetrics)
let metricsBySource = Dictionary(grouping: observations) {
$0.record.sourceDataset ?? $0.record.family
}.mapValues(binaryMetrics)
binaryEvaluations.append(
BinaryEvaluation(
id: configuration.id,
metrics: binaryMetrics(observations),
metricsByLanguage: metricsByLanguage,
metricsBySource: metricsBySource,
thresholdAt90Precision: thresholdRecommendation(
observations: observations,
minimumPrecision: 0.90
),
thresholdAt95Precision: thresholdRecommendation(
observations: observations,
minimumPrecision: 0.95
),
thresholdsAt90PrecisionByLanguage: byLanguage.compactMapValues {
thresholdRecommendation(observations: $0, minimumPrecision: 0.90)
},
thresholdsAt95PrecisionByLanguage: byLanguage.compactMapValues {
thresholdRecommendation(observations: $0, minimumPrecision: 0.95)
},
falsePositiveExamples: errorExamples(
from: observations,
expected: false,
predicted: true
),
falseNegativeExamples: errorExamples(
from: observations,
expected: true,
predicted: false
)
)
)
}
let verifierEvaluations = (manifest.verifiers ?? []).compactMap { configuration
-> VerifierEvaluation? in
guard let model = verifierModels[configuration.id] else { return nil }
let observations = records.map { record -> VerifierObservation in
let candidate = isStageACandidate(
record: record,
verifierID: configuration.id,
models: models,
configurations: classifierConfigurations
)
guard candidate else {
return VerifierObservation(
record: record,
expectedLabel: record.expectedVerifierLabel(
for: configuration.id
),
isStageACandidate: false,
predictedLabel: "neither",
confidence: 0,
margin: 0,
isRouted: false,
latencyMilliseconds: 0
)
}
let startedAt = ContinuousClock.now
let ranked = model.predictedLabelHypotheses(
for: record.text,
maximumCount: 2
).sorted { $0.value > $1.value }
let elapsed = startedAt.duration(to: .now)
let winner = ranked.first ?? (key: "neither", value: 0)
let margin = winner.value - (ranked.dropFirst().first?.value ?? 0)
let threshold = configuration
.confidenceThresholdsByLanguage?[record.language]
?? configuration.confidenceThreshold
let minimumMargin = configuration
.minimumMarginsByLanguage?[record.language]
?? configuration.minimumMargin
return VerifierObservation(
record: record,
expectedLabel: record.expectedVerifierLabel(
for: configuration.id
),
isStageACandidate: true,
predictedLabel: winner.key,
confidence: winner.value,
margin: margin,
isRouted: winner.key != "neither"
&& winner.value >= threshold
&& margin >= minimumMargin,
latencyMilliseconds: milliseconds(
elapsed
)
)
}
let byLanguage = Dictionary(grouping: observations) {
$0.record.language
}.mapValues(verifierMetrics)
let bySourceGroups = Dictionary(grouping: observations) {
$0.record.sourceDataset ?? $0.record.family
}
let bySource = bySourceGroups.mapValues(verifierMetrics)
let leaveOneSourceOut = Dictionary(uniqueKeysWithValues: bySourceGroups.keys.map { source
in
(
source,
verifierMetrics(
observations.filter {
($0.record.sourceDataset ?? $0.record.family) != source
}
)
)
})
let latencies = observations.filter(\.isStageACandidate)
.map(\.latencyMilliseconds)
return VerifierEvaluation(
id: configuration.id,
deploymentMode: configuration.deploymentMode,
acceptedForAutomaticRouting:
configuration.acceptedForAutomaticRouting,
metrics: verifierMetrics(observations),
metricsByLanguage: byLanguage,
metricsBySource: bySource,
leaveOneSourceOut: leaveOneSourceOut,
coldLoadMilliseconds: rounded(
verifierLoadMilliseconds[configuration.id] ?? 0
),
warmMedianMilliseconds: percentile(latencies, proportion: 0.50),
warmP95Milliseconds: percentile(latencies, proportion: 0.95)
)
}
guard let sentimentResult else {
throw NSError(
domain: "RandomHoldoutEvaluation",
code: 2,
userInfo: [NSLocalizedDescriptionKey: "Sentiment model was not evaluated"]
)
}
let languages = Set(records.map(\.language)).sorted()
let macroByLanguage = Dictionary(uniqueKeysWithValues: languages.map { language in
let metrics = binaryEvaluations.compactMap {
$0.metricsByLanguage[language]
}
return (language, aggregate(metrics))
})
let sentimentModel = models["sentiment"]!
let sentimentBySource = Dictionary(grouping: records) {
$0.sourceDataset ?? $0.family
}.mapValues {
sentimentMetrics(records: $0, model: sentimentModel)
}
let report = Report(
generatedAt: ISO8601DateFormatter().string(from: Date()),
seed: holdoutSeed,
corpusRecordCount: records.count,
familyCount: Set(records.map(\.family)).count,
languageCounts: Dictionary(grouping: records, by: \.language).mapValues(\.count),
exactTrainingOverlapCount: exactOverlapCount,
manifestSchemaVersion: manifest.schemaVersion,
binaryMacro: aggregate(binaryEvaluations.map(\.metrics)),
binaryMacroByLanguage: macroByLanguage,
classifiers: binaryEvaluations,
verifierLayers: verifierEvaluations,
sentiment: sentimentResult,
sentimentBySource: sentimentBySource
)
try writeJSON(report, to: reportURL)
print(
"RANDOM_HOLDOUT_EVAL_DONE records=\(records.count) "
+ "overlap=\(exactOverlapCount) "
+ "macroPrecision=\(report.binaryMacro.precision) "
+ "macroRecall=\(report.binaryMacro.recall) "
+ "macroF1=\(report.binaryMacro.f1) "
+ "verifiers=\(report.verifierLayers.count) "
+ "sentimentMacroF1=\(report.sentiment.macroF1)"
)
}
do {
try main()
} catch {
fputs("RANDOM_HOLDOUT_EVAL_FAILED \(error)\n", stderr)
exit(1)
}
@@ -0,0 +1,658 @@
#!/usr/bin/env python3
"""Prepare public-text labeling queues and build deterministic consensus silver data."""
from __future__ import annotations
import argparse
import hashlib
import json
import math
import random
import re
import unicodedata
from collections import Counter, defaultdict
from pathlib import Path
SEED = 20260827
INTENT_LABELS = (
"task",
"question",
"invitation",
"complaint",
"scheduleNegotiation",
"confirmationDecision",
"followUpReminder",
"blessing",
"replyableMessage",
)
SPECIAL_FLAGS = ("ambiguous", "quotedOrMeta")
ACTION_LABELS = (
"taskOnly",
"complaintOnly",
"both",
"questionRequest",
"neither",
)
COORDINATION_LABELS = (
"invitation",
"scheduleNegotiation",
"confirmationDecision",
"followUpReminder",
"neither",
)
PROMPT_VERSION = "clipboard-consensus-v1"
DEFAULT_DIRECTORY = Path("ModelTraining/ClipboardSemantics/Consensus")
DEFAULT_INPUT = Path("ModelTraining/ClipboardSemantics/open-training-corpus.jsonl")
def normalized_text(value: str) -> str:
return " ".join(
unicodedata.normalize("NFKC", value)
.replace("\u0000", " ")
.split()
).strip()
def stable_hash(value: str) -> int:
return int.from_bytes(hashlib.sha256(value.encode()).digest()[:8], "big")
def read_json_lines(path: Path) -> list[dict]:
return [
json.loads(line)
for line in path.read_text(encoding="utf-8").splitlines()
if line.strip()
]
def write_json_lines(path: Path, records: list[dict]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
serialized = "\n".join(
json.dumps(record, ensure_ascii=False, sort_keys=True)
for record in records
)
path.write_text(serialized + ("\n" if serialized else ""), encoding="utf-8")
def queue_priority(
record: dict,
hard_negative_texts: set[str],
) -> tuple[int, int]:
known_labels = set(record.get("knownLabels") or [])
positive_count = sum(bool(record.get(label)) for label in INTENT_LABELS)
confusion_priority = int(
bool(
known_labels
& {
"task",
"question",
"complaint",
"invitation",
"scheduleNegotiation",
"confirmationDecision",
"followUpReminder",
}
)
)
hard_negative_priority = int(
normalized_text(record["text"]).casefold() in hard_negative_texts
)
return (
hard_negative_priority * 10 + confusion_priority + positive_count,
stable_hash(record["id"]),
)
def stratified_queue(
records: list[dict],
count: int,
seed: int,
hard_negative_texts: set[str],
) -> list[dict]:
grouped: dict[tuple[str, str], list[dict]] = defaultdict(list)
for record in records:
grouped[
(
record.get("sourceDataset", "unknown"),
record.get("language", "unknown"),
)
].append(record)
for key in grouped:
grouped[key].sort(
key=lambda record: queue_priority(record, hard_negative_texts),
reverse=True,
)
keys = sorted(grouped)
rng = random.Random(seed)
rng.shuffle(keys)
offsets = {key: 0 for key in keys}
selected: list[dict] = []
while len(selected) < count:
added = False
for key in keys:
offset = offsets[key]
values = grouped[key]
if offset >= len(values):
continue
selected.append(values[offset])
offsets[key] += 1
added = True
if len(selected) == count:
break
if not added:
break
return sorted(selected, key=lambda record: record["id"])
def labeling_instructions() -> str:
return """# Clipboard semantic consensus labeling
Prompt version: clipboard-consensus-v1
Label every JSONL queue record independently. Use only the text itself; do not
infer missing conversational context. Return one JSON object per input record:
{"id":"same id","labels":["task"],"ambiguous":false,"quotedOrMeta":false,"confidence":0.98}
Allowed labels:
- task: another person is explicitly asked/assigned to perform an action.
- question: a genuine information question, including request-shaped questions.
- invitation: invitation to join an event or social activity.
- complaint: present dissatisfaction, malfunction, bad service, or unresolved problem.
- scheduleNegotiation: proposing, changing, or choosing between times; a fixed time is not enough.
- confirmationDecision: explicit approval, rejection, or selection of an option.
- followUpReminder: request to remind, check back, or follow up later/after a trigger.
- blessing: a genuine birthday, holiday, congratulations, or good-wish message.
- replyableMessage: a direct conversational message that naturally invites a response.
Rules:
- Multi-label is allowed. "Could you send it?" is task + question + replyableMessage.
- Set quotedOrMeta when intent-like words are quoted, documented, searched, or discussed.
- Set ambiguous when the text cannot be labeled without missing context.
- Empty labels mean none of the product intents.
- Negative sentiment alone is not complaint. Fixed appointments are not schedule negotiation.
- Do not expose reasoning or add fields. Confidence must be between 0 and 1.
"""
def prepare(arguments: argparse.Namespace) -> None:
records = read_json_lines(arguments.input)
requested_hard_negatives: set[str] = set()
for path in arguments.hard_negative_report:
report = json.loads(path.read_text(encoding="utf-8"))
for classifier in report.get("classifiers") or []:
for example in classifier.get("falsePositiveExamples") or []:
requested_hard_negatives.add(
normalized_text(example["text"]).casefold()
)
license_safe_texts = {
normalized_text(record["text"]).casefold() for record in records
}
matched_hard_negatives = requested_hard_negatives & license_safe_texts
queue = stratified_queue(
records,
arguments.count,
arguments.seed,
matched_hard_negatives,
)
queue_records = [
{
"id": record["id"],
"text": record["text"],
"language": record["language"],
"sourceDataset": record.get("sourceDataset"),
"sourceLicense": record.get("sourceLicense"),
"sourceURL": record.get("sourceURL"),
"sourceRevision": record.get("sourceRevision"),
"knownLabels": record.get("knownLabels", []),
"sourceLabels": {
label: bool(record.get(label))
for label in INTENT_LABELS
if label in set(record.get("knownLabels") or [])
},
}
for record in queue
]
write_json_lines(arguments.queue, queue_records)
arguments.instructions.parent.mkdir(parents=True, exist_ok=True)
instructions = labeling_instructions()
arguments.instructions.write_text(instructions, encoding="utf-8")
manifest = {
"schemaVersion": 1,
"promptVersion": PROMPT_VERSION,
"promptSHA256": hashlib.sha256(instructions.encode()).hexdigest(),
"seed": arguments.seed,
"input": str(arguments.input),
"queue": str(arguments.queue),
"recordCount": len(queue_records),
"requestedHardNegativeCount": len(requested_hard_negatives),
"licenseSafeMatchedHardNegativeCount": len(matched_hard_negatives),
"sourceCounts": dict(
sorted(
Counter(
record.get("sourceDataset") or "unknown"
for record in queue_records
).items()
)
),
"languageCounts": dict(
sorted(Counter(record["language"] for record in queue_records).items())
),
}
arguments.prepare_report.write_text(
json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
print(
f"CONSENSUS_QUEUE records={len(queue_records)} "
f"promptSHA256={manifest['promptSHA256']}"
)
def parse_labeler_argument(value: str) -> tuple[str, Path]:
name, separator, raw_path = value.partition("=")
if not separator or not name or not raw_path:
raise argparse.ArgumentTypeError("Expected LABELER=PATH")
return name, Path(raw_path)
def validate_labeler_record(record: dict, expected_ids: set[str]) -> dict:
record_id = record.get("id")
if record_id not in expected_ids:
raise ValueError(f"Unexpected labeler record id: {record_id}")
labels = record.get("labels")
if not isinstance(labels, list) or any(label not in INTENT_LABELS for label in labels):
raise ValueError(f"Unsupported labels for {record_id}: {labels}")
confidence = record.get("confidence")
if not isinstance(confidence, (int, float)) or not 0 <= confidence <= 1:
raise ValueError(f"Invalid confidence for {record_id}: {confidence}")
for flag in SPECIAL_FLAGS:
if not isinstance(record.get(flag), bool):
raise ValueError(f"Missing boolean {flag} for {record_id}")
return {
"id": record_id,
"labels": sorted(set(labels)),
"ambiguous": record["ambiguous"],
"quotedOrMeta": record["quotedOrMeta"],
"confidence": round(float(confidence), 4),
}
def action_label(labels: set[str]) -> str:
if "task" in labels and "complaint" in labels:
return "both"
if "task" in labels:
return "taskOnly"
if "complaint" in labels:
return "complaintOnly"
if "question" in labels:
return "questionRequest"
return "neither"
def coordination_label(labels: set[str]) -> str | None:
matches = [
label
for label in COORDINATION_LABELS
if label != "neither" and label in labels
]
if len(matches) > 1:
return None
return matches[0] if matches else "neither"
def cluster_signature(text: str) -> str:
normalized = unicodedata.normalize("NFKC", text).casefold()
normalized = re.sub(r"\d+", "<n>", normalized)
normalized = re.sub(r"[^\w\u4e00-\u9fff<>]+", " ", normalized)
tokens = normalized.split()
# Prefix and suffix retain intent-bearing wording while grouping slot variants.
skeleton = tokens[:10] + (["|"] + tokens[-6:] if len(tokens) > 16 else [])
return " ".join(skeleton)
def split_for(record: dict) -> str:
bucket = stable_hash(cluster_signature(record["text"])) % 100
if bucket < 70:
return "silverTrain"
if bucket < 85:
return "silverCalibration"
return "silverAcceptance"
def fleiss_kappa(
labeler_records: list[dict[str, dict]],
queue_ids: list[str],
) -> float:
if len(labeler_records) < 2 or not queue_ids:
return 0
category_counts = [0, 0]
agreement_total = 0.0
item_count = 0
rater_count = len(labeler_records)
for record_id in queue_ids:
for label in INTENT_LABELS:
yes_count = sum(
label in set(labeler[record_id]["labels"])
for labeler in labeler_records
)
no_count = rater_count - yes_count
category_counts[0] += no_count
category_counts[1] += yes_count
agreement_total += (
no_count * (no_count - 1) + yes_count * (yes_count - 1)
) / (rater_count * (rater_count - 1))
item_count += 1
observed = agreement_total / item_count
total_votes = sum(category_counts)
expected = sum((count / total_votes) ** 2 for count in category_counts)
if math.isclose(expected, 1):
return 1
return round((observed - expected) / (1 - expected), 4)
def merge(arguments: argparse.Namespace) -> None:
queue = read_json_lines(arguments.queue)
queue_by_id = {record["id"]: record for record in queue}
if len(queue_by_id) != len(queue):
raise ValueError("Consensus queue contains duplicate ids")
expected_ids = set(queue_by_id)
if len(arguments.labeler) < 3:
raise ValueError("Consensus requires at least three independent labelers")
labelers: list[tuple[str, dict[str, dict]]] = []
for name, path in arguments.labeler:
values = [
validate_labeler_record(record, expected_ids)
for record in read_json_lines(path)
]
by_id = {record["id"]: record for record in values}
missing = expected_ids.difference(by_id)
if missing:
raise ValueError(f"Labeler {name} is missing {len(missing)} records")
if len(by_id) != len(values):
raise ValueError(f"Labeler {name} contains duplicate ids")
labelers.append((name, by_id))
accepted: list[dict] = []
conflicts: list[dict] = []
for record_id in sorted(expected_ids):
queue_record = queue_by_id[record_id]
votes = Counter(
label
for _, records in labelers
for label in records[record_id]["labels"]
)
source_labels = {
label
for label, value in (queue_record.get("sourceLabels") or {}).items()
if value
}
consensus_labels = {
label
for label, count in votes.items()
if count == len(labelers) or count >= 2 and label in source_labels
}
ambiguous_votes = sum(
records[record_id]["ambiguous"] for _, records in labelers
)
quoted_votes = sum(
records[record_id]["quotedOrMeta"] for _, records in labelers
)
coordination = coordination_label(consensus_labels)
full_agreement = all(
set(records[record_id]["labels"]) == set(
labelers[0][1][record_id]["labels"]
)
and records[record_id]["ambiguous"]
== labelers[0][1][record_id]["ambiguous"]
and records[record_id]["quotedOrMeta"]
== labelers[0][1][record_id]["quotedOrMeta"]
for _, records in labelers[1:]
)
rejected_reason = None
if ambiguous_votes >= 2:
rejected_reason = "ambiguous-majority"
elif quoted_votes >= 2 and consensus_labels:
rejected_reason = "quoted-or-meta-intent"
elif coordination is None:
rejected_reason = "multiple-coordination-labels"
elif not consensus_labels and not full_agreement:
rejected_reason = "no-supported-consensus"
audit = {
"id": record_id,
"labelerVotes": dict(sorted(votes.items())),
"labelerConfidences": {
name: records[record_id]["confidence"]
for name, records in labelers
},
"labelerResponseHashes": {
name: hashlib.sha256(
json.dumps(
records[record_id],
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode()
).hexdigest()
for name, records in labelers
},
"sourceLabels": sorted(source_labels),
"ambiguousVotes": ambiguous_votes,
"quotedOrMetaVotes": quoted_votes,
"fullAgreement": full_agreement,
}
if rejected_reason:
conflicts.append(
{
**queue_record,
**audit,
"rejectedReason": rejected_reason,
}
)
continue
split = split_for(queue_record)
labels = set() if quoted_votes >= 2 else consensus_labels
silver = {
"id": f"consensus-{record_id}",
"sourceRecordID": record_id,
"text": queue_record["text"],
"language": queue_record["language"],
"family": "consensus_public",
"split": split,
**{label: label in labels for label in INTENT_LABELS},
"sentiment": "neutral",
"replyable": "replyableMessage" in labels,
"actionVerifierLabel": action_label(labels),
"coordinationVerifierLabel": coordination_label(labels) or "neither",
"quotedOrMeta": quoted_votes >= 2,
"consensusAgreement": round(
max(
[votes.get(label, 0) for label in INTENT_LABELS] + [
len(labelers) if not labels and full_agreement else 0
]
)
/ len(labelers),
4,
),
"promptVersion": PROMPT_VERSION,
"sourceDataset": queue_record.get("sourceDataset"),
"sourceLicense": queue_record.get("sourceLicense"),
"sourceURL": queue_record.get("sourceURL"),
"sourceRevision": queue_record.get("sourceRevision"),
**audit,
}
accepted.append(silver)
write_json_lines(arguments.consensus, accepted)
write_json_lines(arguments.conflicts, conflicts)
for split, path in (
("silverTrain", arguments.train),
("silverCalibration", arguments.calibration),
("silverAcceptance", arguments.acceptance),
):
write_json_lines(
path,
[record for record in accepted if record["split"] == split],
)
labeler_maps = [records for _, records in labelers]
text_split_map: dict[str, set[str]] = defaultdict(set)
cluster_split_map: dict[str, set[str]] = defaultdict(set)
for record in accepted:
text_split_map[normalized_text(record["text"]).casefold()].add(record["split"])
cluster_split_map[cluster_signature(record["text"])].add(record["split"])
exact_overlap_count = sum(len(splits) > 1 for splits in text_split_map.values())
cluster_overlap_count = sum(
len(splits) > 1 for splits in cluster_split_map.values()
)
if exact_overlap_count or cluster_overlap_count:
raise ValueError("Silver split overlap validation failed")
report = {
"schemaVersion": 1,
"promptVersion": PROMPT_VERSION,
"promptSHA256": hashlib.sha256(labeling_instructions().encode()).hexdigest(),
"queueCount": len(queue),
"acceptedCount": len(accepted),
"conflictCount": len(conflicts),
"conflictRate": round(len(conflicts) / max(len(queue), 1), 4),
"duplicateTextRate": round(
(len(accepted) - len(text_split_map)) / max(len(accepted), 1),
4,
),
"overlapChecks": {
"exactTextAcrossSplits": exact_overlap_count,
"nearDuplicateClusterAcrossSplits": cluster_overlap_count,
},
"fullAgreementCount": sum(record["fullAgreement"] for record in accepted)
+ sum(record["fullAgreement"] for record in conflicts),
"fleissKappa": fleiss_kappa(
labeler_maps,
sorted(expected_ids),
),
"labelers": [name for name, _ in labelers],
"splitCounts": dict(sorted(Counter(record["split"] for record in accepted).items())),
"actionLabelCounts": dict(
sorted(Counter(record["actionVerifierLabel"] for record in accepted).items())
),
"coordinationLabelCounts": dict(
sorted(
Counter(
record["coordinationVerifierLabel"] for record in accepted
).items()
)
),
"intentPositiveCounts": {
label: sum(bool(record[label]) for record in accepted)
for label in INTENT_LABELS
},
"languageCounts": dict(
sorted(Counter(record["language"] for record in accepted).items())
),
"sourceCounts": dict(
sorted(
Counter(
record.get("sourceDataset") or "unknown"
for record in accepted
).items()
)
),
}
arguments.report.write_text(
json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
print(
f"CONSENSUS_DONE accepted={len(accepted)} conflicts={len(conflicts)} "
f"kappa={report['fleissKappa']}"
)
def parser() -> argparse.ArgumentParser:
root = argparse.ArgumentParser()
subparsers = root.add_subparsers(dest="command", required=True)
prepare_parser = subparsers.add_parser("prepare")
prepare_parser.add_argument("--input", type=Path, default=DEFAULT_INPUT)
prepare_parser.add_argument(
"--queue",
type=Path,
default=DEFAULT_DIRECTORY / "labeling-queue.jsonl",
)
prepare_parser.add_argument(
"--instructions",
type=Path,
default=DEFAULT_DIRECTORY / "labeling-instructions.md",
)
prepare_parser.add_argument(
"--prepare-report",
type=Path,
default=DEFAULT_DIRECTORY / "labeling-queue-report.json",
)
prepare_parser.add_argument("--count", type=int, default=360)
prepare_parser.add_argument("--seed", type=int, default=SEED)
prepare_parser.add_argument(
"--hard-negative-report",
action="append",
type=Path,
default=[],
)
prepare_parser.set_defaults(handler=prepare)
merge_parser = subparsers.add_parser("merge")
merge_parser.add_argument(
"--queue",
type=Path,
default=DEFAULT_DIRECTORY / "labeling-queue.jsonl",
)
merge_parser.add_argument(
"--labeler",
action="append",
type=parse_labeler_argument,
required=True,
)
merge_parser.add_argument(
"--consensus",
type=Path,
default=DEFAULT_DIRECTORY / "consensus-silver.jsonl",
)
merge_parser.add_argument(
"--conflicts",
type=Path,
default=DEFAULT_DIRECTORY / "consensus-conflicts.jsonl",
)
merge_parser.add_argument(
"--train",
type=Path,
default=DEFAULT_DIRECTORY / "silver-train.jsonl",
)
merge_parser.add_argument(
"--calibration",
type=Path,
default=DEFAULT_DIRECTORY / "silver-calibration.jsonl",
)
merge_parser.add_argument(
"--acceptance",
type=Path,
default=DEFAULT_DIRECTORY / "silver-acceptance.jsonl",
)
merge_parser.add_argument(
"--report",
type=Path,
default=DEFAULT_DIRECTORY / "consensus-report.json",
)
merge_parser.set_defaults(handler=merge)
return root
def main() -> None:
arguments = parser().parse_args()
arguments.handler(arguments)
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,926 @@
#!/usr/bin/env python3
"""Generate a reproducible blind random-combination corpus for deployed models.
This corpus is evaluation-only. Its templates and slot vocabulary are kept
separate from generate_corpus.py and must never be added to model training.
"""
from __future__ import annotations
import argparse
import json
import random
from dataclasses import asdict, dataclass
from pathlib import Path
DEFAULT_SEED = 20260826
DEFAULT_SAMPLES_PER_FAMILY = 20
@dataclass(frozen=True)
class Labels:
task: bool = False
question: bool = False
invitation: bool = False
complaint: bool = False
scheduleNegotiation: bool = False
confirmationDecision: bool = False
followUpReminder: bool = False
sentiment: str = "neutral"
replyable: bool = False
@dataclass(frozen=True)
class Record:
id: str
text: str
language: str
split: str
family: str
task: bool
question: bool
invitation: bool
complaint: bool
scheduleNegotiation: bool
confirmationDecision: bool
followUpReminder: bool
sentiment: str
replyable: bool
SLOTS = {
"zh-Hans": {
"discourse": ["", "对了,", "另外,", "补充一下,", "还有一件事,", "先说重点,", "顺便提一句,", "刚想到,", "简单说,", "单独记一下,"],
"owner": ["设计同学", "财务", "供应商", "项目负责人", "客服团队", "运营"],
"artifact": ["验收截图", "预算备注", "测试结论", "签字页", "交付清单", "复盘摘要"],
"deadline": ["午休前", "明晚之前", "本周收尾时", "下次评审前", "两个工作日内", "月底前"],
"channel": ["群里", "工单下方", "邮件线程里", "共享文档中", "项目卡片上"],
"topic": ["费用调整", "权限开通", "排期变动", "售后范围", "资料归档", "版本上线"],
"event": ["参加小范围评审", "吃顿便饭", "看内部演示", "碰面聊方案", "参加庆功会"],
"time": ["周六傍晚", "明天十点半", "下周三午后", "今晚九点", "周一早会后"],
"alternate": ["周二四点半", "周四午休后", "明早第一段时间", "周五临下班前", "下周一下午"],
"place": ["楼下咖啡店", "三号会议室", "园区北门", "线上会议室", "客户办公室"],
"issue": ["页面一直空白", "付款结果重复扣款", "附件始终打不开", "物流状态停了五天", "账号又被锁住"],
"impact": ["工作完全卡住", "客户已经在催", "我无法继续操作", "交付时间被耽误", "家人收不到商品"],
"option": ["轻量版本", "第二套报价", "供应商丙", "季度结算", "先灰度发布", "线下处理方案"],
"trigger": ["客户回信", "补丁上线", "款项入账", "复诊结束", "合同盖章", "样品送达"],
"person": ["客户经理", "医生", "仓库负责人", "法务", "房东", "招聘方"],
"thing": ["最终结论", "下一步安排", "到账情况", "补充材料", "交付日期", "处理进度"],
"update": ["我刚到酒店", "演示比预想顺利", "路上有点堵", "今天终于忙完了", "刚看到你发的照片"],
"positive": ["这次响应非常快", "新流程顺手多了", "处理结果超出预期", "讲解特别清楚", "修复后体验很好"],
"negative_news": ["行业指数连续回落", "昨夜航班大面积延误", "原材料价格再次上涨", "部分门店暂停营业", "天气预警已经升级"],
"fact": ["仓库共有三层", "合同附件为 PDF", "当前版本号是 2.1", "展厅周一闭馆", "蓝色标签表示已归档"],
},
"en": {
"discourse": ["", "Also, ", "One more thing: ", "For context, ", "The main point is this: ", "Just to add, ", "By the way, ", "A quick note: ", "In short, ", "For the record, "],
"owner": ["Design", "Finance", "the vendor", "the project lead", "Support", "Operations"],
"artifact": ["acceptance screenshots", "budget notes", "test findings", "signature page", "delivery checklist", "retro summary"],
"deadline": ["before lunch", "by tomorrow evening", "as this week closes", "before the next review", "within two business days", "before month-end"],
"channel": ["in the group thread", "under the ticket", "in the email chain", "inside the shared document", "on the project card"],
"topic": ["the fee adjustment", "access activation", "the timeline change", "support coverage", "document archiving", "the release"],
"event": ["join a small review", "have a casual dinner", "watch the internal demo", "meet to discuss the proposal", "attend the celebration"],
"time": ["Saturday evening", "tomorrow at 10:30", "next Wednesday afternoon", "tonight at nine", "after Monday's stand-up"],
"alternate": ["Tuesday at 4:30", "after lunch on Thursday", "first thing tomorrow", "late Friday afternoon", "next Monday afternoon"],
"place": ["the cafe downstairs", "meeting room three", "the north campus gate", "the online room", "the client office"],
"issue": ["the page stays blank", "the payment was charged twice", "the attachment never opens", "tracking has not moved for five days", "the account is locked again"],
"impact": ["all work is blocked", "the client is already chasing us", "I cannot continue", "delivery is now delayed", "my family cannot receive the item"],
"option": ["the lightweight version", "the second quote", "vendor C", "quarterly billing", "a limited rollout first", "the offline resolution"],
"trigger": ["the client replies", "the patch ships", "the payment lands", "the checkup ends", "the contract is signed", "the sample arrives"],
"person": ["the account manager", "the doctor", "the warehouse lead", "Legal", "the landlord", "the recruiter"],
"thing": ["the final decision", "next steps", "payment status", "the missing documents", "the delivery date", "resolution progress"],
"update": ["I just reached the hotel", "the demo went better than expected", "traffic is a little slow", "I finally wrapped up today", "I just saw the photo you sent"],
"positive": ["the response was exceptionally fast", "the new flow is much easier", "the outcome exceeded expectations", "the explanation was crystal clear", "the fix feels solid"],
"negative_news": ["the industry index fell again", "many flights were delayed overnight", "raw material prices rose again", "several stores paused operations", "the weather alert was upgraded"],
"fact": ["the warehouse has three floors", "the contract attachment is a PDF", "the current version is 2.1", "the showroom closes on Mondays", "a blue label means archived"],
},
}
def labels(**values: object) -> Labels:
return Labels(**values)
FAMILIES = {
"task_request": {
"labels": labels(task=True, replyable=True),
"templates": {
"zh-Hans": [
"{owner}还缺{artifact},麻烦{deadline}补齐后在{channel}留言。",
],
"en": [
"{owner} still needs the {artifact}; please add it {deadline} and leave a note {channel}.",
],
},
},
"task_next_action": {
"labels": labels(
task=True,
followUpReminder=True,
replyable=True,
),
"templates": {
"zh-Hans": [
"下一步由你整理{artifact},请在{deadline}交给{owner}",
],
"en": [
"Your next action is to prepare the {artifact} {deadline} for {owner}.",
],
},
},
"information_question": {
"labels": labels(question=True, replyable=True),
"templates": {
"zh-Hans": ["想核实一下,{topic}现在由谁拍板?", "{topic}目前走到哪一步了,方便说明吗?"],
"en": ["Quick check: who owns the final call on {topic}?", "Where does {topic} stand right now?"],
},
},
"fixed_invitation": {
"labels": labels(question=True, invitation=True, replyable=True),
"templates": {
"zh-Hans": ["我给你留了位置,{time}{place}{event},能来不?", "{time}我们在{place}{event},要不要一起?"],
"en": ["I saved you a spot to {event} {time} at {place}; can you make it?", "Want to {event} with us {time} at {place}?"],
},
},
"complaint_support": {
"labels": labels(question=True, complaint=True, sentiment="negative", replyable=True),
"templates": {
"zh-Hans": ["我已经重试三次,还是{issue},导致{impact}。请问什么时候能处理?", "{issue}到现在没解决,{impact},能给一个明确答复吗?"],
"en": ["I have tried three times and {issue}; {impact}. When will this be fixed?", "{issue} is still unresolved and {impact}. Can I get a clear answer?"],
},
},
"schedule_negotiation": {
"labels": labels(question=True, scheduleNegotiation=True, replyable=True),
"templates": {
"zh-Hans": ["原来的时段卡住了,我只能{alternate},能不能对调?", "{time}赶不过去,换成{alternate}你觉得可行吗?"],
"en": ["The original slot is blocked for me; could we swap to {alternate}?", "I cannot make {time}. Would {alternate} be workable instead?"],
},
},
"confirmation_decision": {
"labels": labels(confirmationDecision=True, replyable=True),
"templates": {
"zh-Hans": ["不用再比较了,就选{option},后续都按这个口径走。", "我正式确认{option},请{deadline}启动后续安排。"],
"en": ["No more comparisons: choose {option} and use it as the final direction.", "I formally approve {option}; start the next steps {deadline}."],
},
},
"follow_up_instruction": {
"labels": labels(
task=True,
followUpReminder=True,
replyable=True,
),
"templates": {
"zh-Hans": ["{trigger}后第二天再找{person}确认{thing},别漏了。"],
"en": ["Once {trigger}, check with {person} the next day about {thing}."],
},
},
"follow_up_personal_reminder": {
"labels": labels(followUpReminder=True, replyable=False),
"templates": {
"zh-Hans": ["个人备忘:提醒我{deadline}联系{person},追一下{thing}"],
"en": ["Note to self: remind me to contact {person} {deadline} about {thing}."],
},
},
"conversation": {
"labels": labels(replyable=True),
"templates": {
"zh-Hans": ["跟你说一声,{update},晚点再聊。", "{update},突然想到你可能会想知道。"],
"en": ["Just letting you know, {update}; we can talk later.", "{update}, and I thought you might want to know."],
},
},
"acknowledgment_boundary": {
"labels": labels(),
"templates": {
"zh-Hans": ["看到了,先这样,不需要回复。", "嗯,内容已收到,我只是确认一下。"],
"en": ["Seen, that is all for now; no reply needed.", "Okay, I received it. This is only an acknowledgment."],
},
},
"quoted_question_boundary": {
"labels": labels(),
"templates": {
"zh-Hans": ["文档标题是“{topic}怎么办?”,这里不是在问你。", "纪要保留了“谁负责{topic}?”这句话,答案已经写在后面。"],
"en": ["The document heading says “What about {topic}?”, but it is not asking you.", "The notes preserve the question “Who owns {topic}?”, which is answered below."],
},
},
"event_boundary": {
"labels": labels(),
"templates": {
"zh-Hans": ["公告只记录一件事:{event}定在{time},地点是{place}", "历史行程显示他们曾在{place}{event},没有邀请任何人。"],
"en": ["The notice only records that they will {event} {time} at {place}.", "The old itinerary says they went to {place} to {event}; nobody is being invited."],
},
},
"vague_future_boundary": {
"labels": labels(),
"templates": {
"zh-Hans": ["哪天有心情再找{person}{thing}吧。", "以后也许会看看{topic},目前没有安排。"],
"en": ["Maybe I will talk to {person} about {thing} someday.", "I may revisit {topic} eventually, but there is no plan."],
},
},
"positive_feedback": {
"labels": labels(sentiment="positive", replyable=True),
"templates": {
"zh-Hans": ["必须夸一下,{positive},谢谢你们。", "{positive},整个过程让人很安心。"],
"en": ["Credit where it is due: {positive}. Thank you.", "{positive}, and the whole process felt reassuring."],
},
},
"negative_news": {
"labels": labels(sentiment="negative"),
"templates": {
"zh-Hans": ["新闻简报显示,{negative_news},本文仅陈述情况。", "数据显示{negative_news},没有提出处理诉求。"],
"en": ["The news brief reports that {negative_news}; this is only a factual summary.", "Data shows that {negative_news}, with no request for support."],
},
},
"neutral_fact": {
"labels": labels(),
"templates": {
"zh-Hans": ["资料页写明:{fact}", "当前记录只包含一个事实:{fact}"],
"en": ["The reference page states that {fact}.", "The current record contains one fact: {fact}."],
},
},
}
FRESH_FAMILIES = {
"fresh_task_assignment": {
"labels": labels(task=True, replyable=True),
"templates": {
"zh-Hans": [
"这份交付由你收尾:把{artifact}整理好,最迟{deadline}交给{owner}",
"{owner}{artifact}分给你处理,完成时间不能晚于{deadline}",
],
"en": [
"You are closing out this deliverable: finish the {artifact} and give it to {owner} {deadline}.",
"{owner} assigned the {artifact} to you, with completion required {deadline}.",
],
},
},
"fresh_task_question": {
"labels": labels(task=True, question=True, replyable=True),
"templates": {
"zh-Hans": [
"{artifact}能由你在{deadline}前收尾吗?完成后告诉{owner}",
"这项分工你能接吗:{deadline}整理{artifact}",
],
"en": [
"Can you close out the {artifact} {deadline} and update {owner}?",
"Can you take this assignment and finish the {artifact} {deadline}?",
],
},
},
"fresh_self_plan_boundary": {
"labels": labels(),
"templates": {
"zh-Hans": [
"我可能自己看看{topic},但还没决定什么时候做。",
"只是个人想法:以后也许整理{artifact},目前没有安排。",
],
"en": [
"I may look into {topic} myself, but I have not decided when.",
"This is only a personal idea: perhaps I will organize the {artifact} someday.",
],
},
},
"fresh_complaint": {
"labels": labels(complaint=True, sentiment="negative", replyable=True),
"templates": {
"zh-Hans": [
"本来承诺今天解决,结果还是{issue},现在{impact}",
"同样的故障第三次出现,{issue},整个事情已经{impact}",
],
"en": [
"This was promised for today, yet {issue}, and now {impact}.",
"The same failure has happened a third time: {issue}, so {impact}.",
],
},
},
"fresh_complaint_request": {
"labels": labels(
question=True,
complaint=True,
sentiment="negative",
replyable=True,
),
"templates": {
"zh-Hans": [
"{issue}已经影响到{impact},你们准备哪天真正解决?",
"因为{issue},现在{impact},能不能给出明确处理时限?",
],
"en": [
"{issue} has reached the point where {impact}. When will you actually resolve it?",
"Because {issue}, {impact}. Can you provide a firm resolution date?",
],
},
},
"fresh_negative_report_boundary": {
"labels": labels(sentiment="negative"),
"templates": {
"zh-Hans": [
"行业通报称{negative_news},这里只摘录公开信息。",
"统计报告记录了{negative_news},没有客户投诉。",
],
"en": [
"The industry bulletin says that {negative_news}; this only quotes public information.",
"The statistical report records that {negative_news}, with no customer complaint.",
],
},
},
"fresh_confirmation": {
"labels": labels(confirmationDecision=True, replyable=True),
"templates": {
"zh-Hans": [
"评审结束,我的最终选择是{option},其他方案关闭。",
"结论正式生效:{topic}采用{option}",
],
"en": [
"The review is over; my final selection is {option}, and the alternatives are closed.",
"The decision is now official: use {option} for {topic}.",
],
},
},
"fresh_pending_boundary": {
"labels": labels(),
"templates": {
"zh-Hans": [
"我看过{option}了,但还没有选择,等明天再决定。",
"{topic}仍在评审,当前没有批准结论。",
],
"en": [
"I reviewed {option}, but have not selected it; the decision waits until tomorrow.",
"{topic} remains under review, with no approval yet.",
],
},
},
"fresh_follow_up": {
"labels": labels(
task=True,
followUpReminder=True,
replyable=True,
),
"templates": {
"zh-Hans": [
"{trigger}满两天后,再找{person}核实{thing}并记录结果。",
"当前事项结束以后,回头联系{person}追踪{thing}",
],
"en": [
"Two days after {trigger}, check with {person} again about {thing} and record the outcome.",
"After the current item closes, return to {person} and track {thing}.",
],
},
},
"fresh_personal_reminder": {
"labels": labels(followUpReminder=True),
"templates": {
"zh-Hans": [
"给自己设一条后续提醒:{deadline}{person}确认{thing}",
"个人行动项:{trigger}以后再次检查{thing}",
],
"en": [
"Set myself a follow-up reminder to ask {person} about {thing} {deadline}.",
"Personal action item: check {thing} again after {trigger}.",
],
},
},
"fresh_plain_task_boundary": {
"labels": labels(task=True, replyable=True),
"templates": {
"zh-Hans": [
"现在请直接整理{artifact}并发给{owner},不需要后续回访。",
"一次性完成{artifact}即可,交给{owner}后任务结束。",
],
"en": [
"Prepare the {artifact} now and send it to {owner}; no later follow-up is needed.",
"Complete the {artifact} once, deliver it to {owner}, and close the task.",
],
},
},
"fresh_vague_follow_boundary": {
"labels": labels(),
"templates": {
"zh-Hans": [
"以后说不定会再问{person},目前没有后续计划。",
"{thing}哪天想起来再看,现在不用提醒。",
],
"en": [
"I might ask {person} again someday, but there is no follow-up plan.",
"Maybe I will revisit {thing} whenever it comes to mind; no reminder is needed.",
],
},
},
}
TARGETED_FINAL_FAMILIES = {
"final_task_direct": {
"labels": labels(task=True, replyable=True),
"templates": {
"zh-Hans": [
"{artifact}现在明确由你负责,{deadline}前交给{owner}",
"请你承担{artifact}的交付,完成后在{channel}确认。",
],
"en": [
"Ownership of the {artifact} now belongs to you; deliver it to {owner} {deadline}.",
"You are responsible for delivering the {artifact}; confirm completion {channel}.",
],
},
},
"final_task_indirect": {
"labels": labels(task=True, replyable=True),
"templates": {
"zh-Hans": [
"{owner}希望你能接下{artifact},并在{deadline}前收尾。",
"这件事想交给你继续推进:整理{artifact}",
],
"en": [
"{owner} is counting on you to pick up the {artifact} and close it out {deadline}.",
"We would like you to carry this forward by preparing the {artifact}.",
],
},
},
"final_task_question": {
"labels": labels(task=True, question=True, replyable=True),
"templates": {
"zh-Hans": [
"能请你负责{artifact}并在{deadline}前完成吗?",
"{artifact}接下来可以由你收尾吗?",
],
"en": [
"Would you be able to own the {artifact} and finish it {deadline}?",
"Could the {artifact} be left with you for final completion?",
],
},
},
"final_personal_action_boundary": {
"labels": labels(followUpReminder=True),
"templates": {
"zh-Hans": [
"这是我自己的行动清单:{trigger}后检查{thing}",
"个人记录,不是委派:{deadline}{person}确认{thing}",
],
"en": [
"This is on my own action list: check {thing} after {trigger}.",
"Personal note, not delegated work: ask {person} about {thing} {deadline}.",
],
},
},
"final_self_intent_boundary": {
"labels": labels(),
"templates": {
"zh-Hans": [
"我自己可能会整理{artifact},但还没有正式计划。",
"以后有空我再看{topic},目前不用安排。",
],
"en": [
"I may organize the {artifact} myself, but there is no firm plan.",
"I might look at {topic} when I have time; nothing is scheduled.",
],
},
},
"final_event_boundary": {
"labels": labels(),
"templates": {
"zh-Hans": [
"{owner}{deadline}举行评审,这只是日程通知。",
"{topic}会议已经定在{deadline},没有分配任务。",
],
"en": [
"{owner} is holding a review {deadline}; this is only a calendar notice.",
"The meeting about {topic} is set for {deadline}, with no assignment.",
],
},
},
"final_complaint_implicit": {
"labels": labels(complaint=True, sentiment="negative", replyable=True),
"templates": {
"zh-Hans": [
"我不应该为同一个问题反复追问,{issue},现在{impact}",
"又是同样的结果:{issue},已经连续影响到{impact}",
],
"en": [
"I should not have to chase the same issue repeatedly: {issue}, and now {impact}.",
"It is the same outcome again: {issue}, repeatedly leaving me with {impact}.",
],
},
},
"final_complaint_short": {
"labels": labels(complaint=True, sentiment="negative", replyable=True),
"templates": {
"zh-Hans": [
"{issue},已经第三次了。",
"等了这么久还是{issue},实在无法接受。",
],
"en": [
"{issue}. This is already the third time.",
"After all this waiting, {issue}; this is not acceptable.",
],
},
},
"final_complaint_request": {
"labels": labels(
question=True,
complaint=True,
sentiment="negative",
replyable=True,
),
"templates": {
"zh-Hans": [
"{issue}一直没有变化,什么时候才能真正处理?",
"因为{issue},现在{impact},谁能给出解决结果?",
],
"en": [
"Nothing has changed with this problem: {issue}. When will it actually be handled?",
"Because {issue}, {impact}. Who can provide a real resolution?",
],
},
},
"final_negative_fact_boundary": {
"labels": labels(sentiment="negative"),
"templates": {
"zh-Hans": [
"公开报告显示{negative_news},这里没有服务申诉。",
"资料仅记录{negative_news},不涉及个人问题。",
],
"en": [
"The public report shows that {negative_news}; there is no service grievance here.",
"The document only records that {negative_news}, not an individual problem.",
],
},
},
"final_resolved_boundary": {
"labels": labels(),
"templates": {
"zh-Hans": [
"之前的{issue}已经彻底恢复,这是一条结案记录。",
"{topic}的问题处理完毕,现在不需要任何支持。",
],
"en": [
"The earlier issue where {issue} is fully resolved; this is a closure record.",
"The problem involving {topic} is complete, with no support needed now.",
],
},
},
"final_operational_task_boundary": {
"labels": labels(task=True, replyable=True),
"templates": {
"zh-Hans": [
"请完成{artifact}后直接结项,不需要投诉或后续回访。",
"{deadline}前把{artifact}交给{owner},随后流程关闭。",
],
"en": [
"Complete the {artifact} and close the item; no complaint or later follow-up is involved.",
"Deliver the {artifact} to {owner} {deadline}, then close the workflow.",
],
},
},
}
TARGETED_CONFIRMATION_FAMILIES = {
"confirmation_task_assignment": {
"labels": labels(task=True, replyable=True),
"templates": {
"zh-Hans": [
"请你把{artifact}负责到底,并在{deadline}{owner}交付。",
"{artifact}接下来归你处理,完成后在{channel}更新状态。",
],
"en": [
"Please see the {artifact} through and deliver it to {owner} {deadline}.",
"The {artifact} is yours to handle next; update the status {channel} when complete.",
],
},
},
"confirmation_task_request": {
"labels": labels(task=True, question=True, replyable=True),
"templates": {
"zh-Hans": [
"可以请你接管{artifact}并在{deadline}前处理好吗?",
"你愿意负责{artifact}的最后交付吗?",
],
"en": [
"Could I ask you to take care of the {artifact} {deadline}?",
"Would you own the final delivery of the {artifact}?",
],
},
},
"confirmation_operational_task": {
"labels": labels(task=True, replyable=True),
"templates": {
"zh-Hans": [
"{artifact}交给{owner}后直接关闭事项,不安排回访。",
"当前只需要完成{artifact},没有投诉处理。",
],
"en": [
"Close the item after sending the {artifact} to {owner}; do not schedule a follow-up.",
"The only requirement is to complete the {artifact}; no grievance handling is involved.",
],
},
},
"confirmation_personal_boundary": {
"labels": labels(followUpReminder=True),
"templates": {
"zh-Hans": [
"我自己的行动备忘:{trigger}后查看{thing}",
"仅记录个人计划,{deadline}联系{person}",
],
"en": [
"My private action note is to review {thing} after {trigger}.",
"This only records my personal plan to contact {person} {deadline}.",
],
},
},
"confirmation_self_plan_boundary": {
"labels": labels(),
"templates": {
"zh-Hans": [
"我也许会自己整理{artifact},目前没有确定安排。",
"{topic}以后再考虑,现在谁都不用处理。",
],
"en": [
"I may prepare the {artifact} myself, but nothing is arranged.",
"{topic} can be considered later; nobody needs to handle it now.",
],
},
},
"confirmation_complaint_implicit": {
"labels": labels(complaint=True, sentiment="negative", replyable=True),
"templates": {
"zh-Hans": [
"我到现在还在面对{issue},而且{impact}",
"处理承诺没有兑现,结果仍然是{issue}",
],
"en": [
"I am still dealing with the fact that {issue}, and {impact}.",
"The promised fix never materialized; the result is still that {issue}.",
],
},
},
"confirmation_complaint_request": {
"labels": labels(
question=True,
complaint=True,
sentiment="negative",
replyable=True,
),
"templates": {
"zh-Hans": [
"{issue}已经持续很久,究竟什么时候恢复?",
"现在因为{issue}{impact},谁来负责处理?",
],
"en": [
"This has continued for far too long: {issue}. When will it be restored?",
"Because {issue}, {impact}. Who is responsible for fixing it?",
],
},
},
"confirmation_complaint_short": {
"labels": labels(complaint=True, sentiment="negative", replyable=True),
"templates": {
"zh-Hans": [
"还是{issue},完全没有改善。",
"{issue}又来了,不能一直这样。",
],
"en": [
"It is still the case that {issue}, with no improvement.",
"The problem is back again: {issue}. This cannot keep happening.",
],
},
},
"confirmation_negative_fact": {
"labels": labels(sentiment="negative"),
"templates": {
"zh-Hans": [
"研究资料指出{negative_news},这不是客户反馈。",
"行业统计记录了{negative_news},没有服务诉求。",
],
"en": [
"The research notes that {negative_news}; this is not customer feedback.",
"Industry statistics record that {negative_news}, with no service request.",
],
},
},
"confirmation_resolved_boundary": {
"labels": labels(),
"templates": {
"zh-Hans": [
"{issue}已经恢复,{owner}确认事项关闭。",
"{topic}当前运行正常,不需要补救。",
],
"en": [
"The earlier state where {issue} is resolved, and {owner} confirmed closure.",
"{topic} is operating normally now, with no remedy needed.",
],
},
},
}
TARGETED_RELEASE_FAMILIES = {
"release_task_owner": {
"labels": labels(task=True, replyable=True),
"templates": {
"zh-Hans": [
"{artifact}的负责人就是你,请在{deadline}完成交付。",
"接下来请你推进{artifact},并向{owner}汇报结果。",
],
"en": [
"You are the owner of the {artifact}; complete delivery {deadline}.",
"Please move the {artifact} forward and report the outcome to {owner}.",
],
},
},
"release_task_polite_request": {
"labels": labels(task=True, question=True, replyable=True),
"templates": {
"zh-Hans": [
"能麻烦你把{artifact}负责到交付完成吗?",
"你可以在{deadline}前处理好{artifact}吗?",
],
"en": [
"May I ask you to own the {artifact} through delivery?",
"Can you have the {artifact} completed {deadline}?",
],
},
},
"release_personal_boundary": {
"labels": labels(followUpReminder=True),
"templates": {
"zh-Hans": [
"这是我的私人待办:{trigger}之后确认{thing}",
"个人备忘,不交给任何人:{deadline}联系{person}",
],
"en": [
"This is my private todo: confirm {thing} after {trigger}.",
"Personal reminder, assigned to nobody else: contact {person} {deadline}.",
],
},
},
"release_complaint_unspoken": {
"labels": labels(complaint=True, sentiment="negative", replyable=True),
"templates": {
"zh-Hans": [
"从上次反馈以后仍然{issue},现在已经{impact}",
"说好的处理没有发生,我看到的还是{issue}",
],
"en": [
"Since my last report, {issue}, and it has now reached the point where {impact}.",
"The promised handling did not happen; I am still seeing that {issue}.",
],
},
},
"release_complaint_question": {
"labels": labels(
question=True,
complaint=True,
sentiment="negative",
replyable=True,
),
"templates": {
"zh-Hans": [
"{issue}到现在都没处理,什么时候能恢复正常?",
"目前因为{issue}{impact},可以给个处理结果吗?",
],
"en": [
"The problem where {issue} has not been addressed. When will normal service return?",
"At the moment, {issue}, so {impact}. Can I get an actual resolution?",
],
},
},
"release_complaint_brief": {
"labels": labels(complaint=True, sentiment="negative", replyable=True),
"templates": {
"zh-Hans": [
"{issue},到现在还是老样子。",
"又一次{issue},这已经影响正常使用。",
],
"en": [
"{issue}, and nothing has changed.",
"Once again, {issue}; normal use is now affected.",
],
},
},
"release_negative_boundary": {
"labels": labels(sentiment="negative"),
"templates": {
"zh-Hans": [
"分析报告提到{negative_news},不代表用户投诉。",
"公开数据包含{negative_news},这里只做事实引用。",
],
"en": [
"The analysis mentions that {negative_news}; it does not represent a user complaint.",
"Public data includes the fact that {negative_news}; this is only a factual citation.",
],
},
},
"release_resolved_boundary": {
"labels": labels(),
"templates": {
"zh-Hans": [
"{issue}的问题已经结束,目前状态稳定。",
"{topic}已确认恢复,{owner}不需要继续介入。",
],
"en": [
"The earlier problem where {issue} is over, and the current state is stable.",
"{topic} is confirmed restored, so {owner} does not need to intervene.",
],
},
},
}
def render(template: str, slots: dict[str, list[str]], rng: random.Random) -> str:
values = {key: rng.choice(options) for key, options in slots.items()}
return values["discourse"] + template.format(**values)
def generate(
seed: int,
samples_per_family: int,
families: dict[str, dict[str, object]],
) -> list[Record]:
rng = random.Random(seed)
records: list[Record] = []
seen: set[str] = set()
for language in ("zh-Hans", "en"):
slots = SLOTS[language]
for family, configuration in families.items():
family_labels: Labels = configuration["labels"]
templates: list[str] = configuration["templates"][language]
generated = 0
attempts = 0
while generated < samples_per_family:
attempts += 1
if attempts > samples_per_family * 200:
raise RuntimeError(f"Unable to generate unique samples for {language}/{family}")
text = render(rng.choice(templates), slots, rng)
normalized = " ".join(text.casefold().split())
if normalized in seen:
continue
seen.add(normalized)
record_id = f"random-{seed}-{language}-{family}-{generated + 1:03d}"
records.append(
Record(
id=record_id,
text=text,
language=language,
split="randomHoldout",
family=family,
**asdict(family_labels),
)
)
generated += 1
rng.shuffle(records)
return records
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--seed", type=int, default=DEFAULT_SEED)
parser.add_argument("--samples-per-family", type=int, default=DEFAULT_SAMPLES_PER_FAMILY)
parser.add_argument(
"--profile",
choices=(
"development",
"fresh",
"targeted-final",
"targeted-confirmation",
"targeted-release",
),
default="development",
)
parser.add_argument(
"--output",
type=Path,
default=None,
)
arguments = parser.parse_args()
profiles = {
"development": FAMILIES,
"fresh": FRESH_FAMILIES,
"targeted-final": TARGETED_FINAL_FAMILIES,
"targeted-confirmation": TARGETED_CONFIRMATION_FAMILIES,
"targeted-release": TARGETED_RELEASE_FAMILIES,
}
families = profiles[arguments.profile]
default_outputs = {
"development": "random-holdout-corpus.jsonl",
"fresh": "fresh-metric-holdout-corpus.jsonl",
"targeted-final": "targeted-final-holdout-corpus.jsonl",
"targeted-confirmation": "targeted-confirmation-holdout-corpus.jsonl",
"targeted-release": "targeted-release-holdout-corpus.jsonl",
}
output_path = arguments.output or Path(
"ModelTraining/ClipboardSemantics/" + default_outputs[arguments.profile]
)
records = generate(arguments.seed, arguments.samples_per_family, families)
output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open("w", encoding="utf-8") as output:
for record in records:
output.write(json.dumps(asdict(record), ensure_ascii=False, sort_keys=True) + "\n")
print(
f"RANDOM_HOLDOUT_DONE records={len(records)} "
f"families={len(families)} profile={arguments.profile} "
f"seed={arguments.seed} output={output_path}"
)
if __name__ == "__main__":
main()
@@ -0,0 +1,109 @@
import json
import subprocess
import tempfile
import unittest
from pathlib import Path
SCRIPT = Path(__file__).resolve().parents[1] / "apply_verifier_deployment_policy.py"
class VerifierDeploymentPolicyTests(unittest.TestCase):
def test_rejected_candidate_cannot_change_deployed_manifest(self):
with self._fixture(accepted=False) as fixture:
self._run(fixture)
deployed = self._read(fixture["deployed_manifest"])
report = self._read(fixture["report"])
self.assertEqual(2, deployed["schemaVersion"])
self.assertFalse(report["applied"])
self.assertEqual(0, report["eligibleVerifierCount"])
def test_passing_candidate_enters_shadow_and_preserves_classifiers(self):
with self._fixture(accepted=True) as fixture:
self._run(fixture)
deployed = self._read(fixture["deployed_manifest"])
report = self._read(fixture["report"])
self.assertEqual(3, deployed["schemaVersion"])
self.assertEqual("task", deployed["classifiers"][0]["id"])
self.assertEqual("shadow", deployed["verifiers"][0]["deploymentMode"])
self.assertFalse(
deployed["verifiers"][0]["acceptedForAutomaticRouting"]
)
self.assertTrue(report["applied"])
def _fixture(self, accepted):
temporary = tempfile.TemporaryDirectory()
root = Path(temporary.name)
candidate = root / "candidate"
resource = root / "resource"
candidate.mkdir()
resource.mkdir()
verifier = {
"id": "action",
"modelFile": "ActionIntentVerifier.mlmodel",
"acceptedForAutomaticRouting": accepted,
"deploymentMode": "automatic" if accepted else "shadow",
}
(candidate / verifier["modelFile"]).write_bytes(b"model")
self._write(
candidate / "clipboard-semantic-models.json",
{
"schemaVersion": 3,
"classifiers": [{"id": "untrusted-candidate"}],
"verifiers": [verifier],
},
)
deployed_manifest = resource / "clipboard-semantic-models.json"
self._write(
deployed_manifest,
{
"schemaVersion": 2,
"classifiers": [{"id": "task"}],
},
)
fixture = {
"temporary": temporary,
"candidate": candidate,
"resource": resource,
"deployed_manifest": deployed_manifest,
"report": root / "report.json",
}
class Context:
def __enter__(self):
return fixture
def __exit__(self, *unused):
temporary.cleanup()
return Context()
def _run(self, fixture):
subprocess.run(
[
"python3",
str(SCRIPT),
"--candidate-directory",
str(fixture["candidate"]),
"--resource-directory",
str(fixture["resource"]),
"--report",
str(fixture["report"]),
"--apply",
],
check=True,
capture_output=True,
text=True,
)
def _write(self, path, value):
path.write_text(json.dumps(value), encoding="utf-8")
def _read(self, path):
return json.loads(path.read_text(encoding="utf-8"))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,138 @@
import json
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import generate_consensus_labels as consensus
class ConsensusLabelTests(unittest.TestCase):
def test_requires_source_support_for_two_of_three_vote(self):
report, accepted, conflicts = self._merge(
source_labels={},
label_sets=[["task"], ["task"], []],
)
self.assertEqual([], accepted)
self.assertEqual("no-supported-consensus", conflicts[0]["rejectedReason"])
self.assertEqual(1, report["conflictCount"])
def test_accepts_two_of_three_when_official_label_supports_vote(self):
report, accepted, conflicts = self._merge(
source_labels={"task": True},
label_sets=[["task"], ["task"], []],
)
self.assertEqual([], conflicts)
self.assertEqual("taskOnly", accepted[0]["actionVerifierLabel"])
self.assertTrue(accepted[0]["task"])
self.assertEqual(1, report["acceptedCount"])
def test_rejects_multi_coordination_consensus(self):
_, accepted, conflicts = self._merge(
source_labels={"invitation": True, "scheduleNegotiation": True},
label_sets=[
["invitation", "scheduleNegotiation"],
["invitation", "scheduleNegotiation"],
["invitation", "scheduleNegotiation"],
],
)
self.assertEqual([], accepted)
self.assertEqual(
"multiple-coordination-labels",
conflicts[0]["rejectedReason"],
)
def test_near_duplicate_slot_variants_share_split(self):
first = {
"text": "Could you send report 123 before Friday?",
}
second = {
"text": "Could you send report 456 before Friday?",
}
self.assertEqual(
consensus.split_for(first),
consensus.split_for(second),
)
def _merge(self, source_labels, label_sets):
with tempfile.TemporaryDirectory() as raw_directory:
directory = Path(raw_directory)
queue_path = directory / "queue.jsonl"
self._write_json_lines(
queue_path,
[
{
"id": "record-1",
"text": "Could you send the report?",
"language": "en",
"sourceDataset": "fixture",
"sourceLicense": "MIT",
"sourceLabels": source_labels,
}
],
)
labelers = []
for index, labels in enumerate(label_sets):
path = directory / f"labeler-{index}.jsonl"
self._write_json_lines(
path,
[
{
"id": "record-1",
"labels": labels,
"ambiguous": False,
"quotedOrMeta": False,
"confidence": 0.95,
}
],
)
labelers.append((f"labeler-{index}", path))
paths = {
name: directory / f"{name}.jsonl"
for name in (
"consensus",
"conflicts",
"train",
"calibration",
"acceptance",
)
}
report_path = directory / "report.json"
consensus.merge(
SimpleNamespace(
queue=queue_path,
labeler=labelers,
report=report_path,
**paths,
)
)
return (
json.loads(report_path.read_text(encoding="utf-8")),
self._read_json_lines(paths["consensus"]),
self._read_json_lines(paths["conflicts"]),
)
def _write_json_lines(self, path, records):
path.write_text(
"\n".join(json.dumps(record) for record in records) + "\n",
encoding="utf-8",
)
def _read_json_lines(self, path):
return [
json.loads(line)
for line in path.read_text(encoding="utf-8").splitlines()
if line
]
if __name__ == "__main__":
unittest.main()
+461 -22
View File
@@ -9,10 +9,16 @@ private struct CorpusRecord: Codable {
let language: String
let split: String
let family: String
let knownLabels: Set<String>?
let sourceDataset: String?
let task: Bool
let question: Bool
let invitation: Bool
let complaint: Bool
let scheduleNegotiation: Bool
let confirmationDecision: Bool
let followUpReminder: Bool
let blessing: Bool
let sentiment: String
let replyable: Bool
}
@@ -44,6 +50,7 @@ private struct CandidateReport: Codable {
let balancedTrainingCount: Int
let balancedValidationCount: Int
let threshold: Double?
let confidenceThresholdsByLanguage: [String: Double]?
let acceptedForAutomaticRouting: Bool
let validationBinary: BinaryMetrics?
let testBinary: BinaryMetrics?
@@ -53,6 +60,7 @@ private struct CandidateReport: Codable {
let goldenFalsePositiveExamples: [String]?
let goldenFalseNegativeExamples: [String]?
let binaryByLanguage: [String: BinaryMetrics]?
let goldenBinaryByLanguage: [String: BinaryMetrics]?
let validationMulticlass: MulticlassMetrics?
let testMulticlass: MulticlassMetrics?
let goldenMulticlass: MulticlassMetrics?
@@ -87,6 +95,7 @@ private struct ManifestClassifier: Codable {
let labels: [String]
let positiveLabel: String?
let confidenceThreshold: Double?
let confidenceThresholdsByLanguage: [String: Double]?
let acceptedForAutomaticRouting: Bool
}
@@ -118,11 +127,19 @@ private enum CandidateAlgorithm: String, CaseIterable {
}
}
private let usesBaselineNegativePolicy = CommandLine.arguments.contains(
"--baseline-negative-policy"
)
private enum ClassifierID: String, CaseIterable {
case task
case question
case invitation
case complaint
case scheduleNegotiation
case confirmationDecision
case followUpReminder
case blessing
case replyableMessage
case sentiment
@@ -132,6 +149,10 @@ private enum ClassifierID: String, CaseIterable {
case .question: "QuestionIntentClassifier"
case .invitation: "InvitationIntentClassifier"
case .complaint: "ComplaintIntentClassifier"
case .scheduleNegotiation: "ScheduleNegotiationIntentClassifier"
case .confirmationDecision: "ConfirmationDecisionIntentClassifier"
case .followUpReminder: "FollowUpReminderIntentClassifier"
case .blessing: "BlessingIntentClassifier"
case .replyableMessage: "ConversationalReplyIntentClassifier"
case .sentiment: "SentimentClassifier"
}
@@ -143,6 +164,10 @@ private enum ClassifierID: String, CaseIterable {
case .question: ["notQuestion", "question"]
case .invitation: ["notInvitation", "invitation"]
case .complaint: ["notComplaint", "complaint"]
case .scheduleNegotiation: ["notScheduleNegotiation", "scheduleNegotiation"]
case .confirmationDecision: ["notConfirmationDecision", "confirmationDecision"]
case .followUpReminder: ["notFollowUpReminder", "followUpReminder"]
case .blessing: ["notBlessing", "blessing"]
case .replyableMessage: ["notReplyableMessage", "replyableMessage"]
case .sentiment: ["negative", "neutral", "positive"]
}
@@ -154,21 +179,168 @@ private enum ClassifierID: String, CaseIterable {
case .question: "question"
case .invitation: "invitation"
case .complaint: "complaint"
case .scheduleNegotiation: "scheduleNegotiation"
case .confirmationDecision: "confirmationDecision"
case .followUpReminder: "followUpReminder"
case .blessing: "blessing"
case .replyableMessage: "replyableMessage"
case .sentiment: nil
}
}
var hardNegativeFamilies: Set<String> {
switch self {
case .task:
return [
"complaint_implicit_failure",
"complaint_incident_diverse",
"complaint_request",
"complaint_statement",
"confirmation_decision",
"confirmation_selection_short",
"event_statement",
"follow_up_personal_reminder",
"neutral_fact",
"personal_action_item_boundary",
"resolved_issue_boundary",
"self_plan"
]
case .invitation:
return [
"event_statement",
"schedule_negotiation",
"task_question",
"task_statement"
]
case .complaint:
return [
"information_question",
"negative_news",
"neutral_fact",
"personal_action_item_boundary",
"positive_feedback",
"quoted_question",
"resolved_issue_boundary",
"self_plan",
"task_assignment_diverse",
"task_completion_boundary",
"task_indirect_assignment",
"task_indirect_question",
"task_statement"
]
case .scheduleNegotiation:
if usesBaselineNegativePolicy {
return [
"event_statement",
"information_question",
"invitation_question",
"schedule_fixed_invitation_boundary",
"task_question",
"task_statement",
"vague_future_boundary"
]
}
return [
"event_statement",
"confirmation_decision",
"confirmation_selection_short",
"follow_up_action",
"follow_up_triggered",
"information_question",
"invitation_question",
"schedule_fixed_invitation_boundary",
"task_question",
"task_statement",
"vague_future_boundary"
]
case .confirmationDecision:
return [
"acknowledgment_decision_boundary",
"event_statement",
"follow_up_action",
"follow_up_personal_reminder",
"follow_up_triggered",
"neutral_fact",
"negative_news",
"schedule_negotiation",
"task_assignment_diverse",
"task_statement",
"vague_future_boundary"
]
case .followUpReminder:
return [
"acknowledgment",
"complaint_request",
"confirmation_decision",
"confirmation_selection_short",
"event_statement",
"invitation_question",
"neutral_fact",
"positive_feedback",
"schedule_negotiation",
"self_plan",
"task_assignment_diverse",
"task_question",
"task_statement",
"vague_future_boundary"
]
case .blessing:
return [
"acknowledgment",
"blessing_boundary",
"conversational_message",
"event_statement",
"invitation_question",
"neutral_fact",
"positive_feedback",
"quoted_question",
"task_question",
"task_statement"
]
case .question, .replyableMessage, .sentiment:
return []
}
}
var hardNegativeFraction: Double {
switch self {
case .task:
0.65
case .complaint, .blessing:
0.65
case .invitation, .followUpReminder:
0.50
case .scheduleNegotiation:
0.75
case .confirmationDecision:
0.90
case .question, .replyableMessage, .sentiment:
0
}
}
func label(for record: CorpusRecord) -> String {
switch self {
case .task: record.task ? "task" : "notTask"
case .question: record.question ? "question" : "notQuestion"
case .invitation: record.invitation ? "invitation" : "notInvitation"
case .complaint: record.complaint ? "complaint" : "notComplaint"
case .scheduleNegotiation:
record.scheduleNegotiation ? "scheduleNegotiation" : "notScheduleNegotiation"
case .confirmationDecision:
record.confirmationDecision ? "confirmationDecision" : "notConfirmationDecision"
case .followUpReminder:
record.followUpReminder ? "followUpReminder" : "notFollowUpReminder"
case .blessing:
record.blessing ? "blessing" : "notBlessing"
case .replyableMessage: record.replyable ? "replyableMessage" : "notReplyableMessage"
case .sentiment: record.sentiment
}
}
func hasKnownLabel(in record: CorpusRecord) -> Bool {
record.knownLabels?.contains(rawValue) ?? true
}
}
private struct SeededGenerator: RandomNumberGenerator {
@@ -195,14 +367,36 @@ private struct TrainedCandidate {
private let fileManager = FileManager.default
private let repositoryRoot = URL(fileURLWithPath: fileManager.currentDirectoryPath)
private let corpusURL = repositoryRoot
.appendingPathComponent("ModelTraining/ClipboardSemantics/clipboard_semantic_corpus.jsonl")
private let candidateDirectory = repositoryRoot
.appendingPathComponent("ModelTraining/ClipboardSemantics/Candidates")
private let resourceDirectory = repositoryRoot
.appendingPathComponent("OSGKeyboardShared/Resources/ClipboardSemantics")
private let reportURL = repositoryRoot
.appendingPathComponent("ModelTraining/ClipboardSemantics/evaluation-report.json")
private func commandLineValue(after flag: String) -> String? {
guard let index = CommandLine.arguments.firstIndex(of: flag),
CommandLine.arguments.indices.contains(index + 1) else {
return nil
}
return CommandLine.arguments[index + 1]
}
private func resolvedURL(flag: String, defaultPath: String) -> URL {
let path = commandLineValue(after: flag) ?? defaultPath
return URL(fileURLWithPath: path, relativeTo: repositoryRoot).standardizedFileURL
}
private let corpusURL = resolvedURL(
flag: "--corpus",
defaultPath: "ModelTraining/ClipboardSemantics/clipboard_semantic_corpus.jsonl"
)
private let candidateDirectory = resolvedURL(
flag: "--candidate-directory",
defaultPath: "ModelTraining/ClipboardSemantics/Candidates"
)
private let resourceDirectory = resolvedURL(
flag: "--resource-directory",
defaultPath: "OSGKeyboardShared/Resources/ClipboardSemantics"
)
private let reportURL = resolvedURL(
flag: "--report",
defaultPath: "ModelTraining/ClipboardSemantics/evaluation-report.json"
)
private func loadCorpus() throws -> [CorpusRecord] {
let content = try String(contentsOf: corpusURL, encoding: .utf8)
@@ -219,6 +413,85 @@ private func stableSeed(for classifier: ClassifierID, split: String) -> UInt64 {
}
}
private func sourceBalancedPrefix(
_ records: [CorpusRecord],
limit: Int,
classifier: ClassifierID,
label: String
) -> [CorpusRecord] {
guard records.count > limit else { return records }
var grouped = Dictionary(grouping: records) {
$0.sourceDataset ?? "generated"
}
for source in grouped.keys.sorted() {
var generator = SeededGenerator(
seed: stableSeed(
for: classifier,
split: "open|\(label)|\(source)"
)
)
grouped[source]?.shuffle(using: &generator)
}
let sources = grouped.keys.sorted()
var offsets = Dictionary(uniqueKeysWithValues: sources.map { ($0, 0) })
var selected: [CorpusRecord] = []
while selected.count < limit {
var addedRecord = false
for source in sources where selected.count < limit {
let offset = offsets[source] ?? 0
guard let values = grouped[source], values.indices.contains(offset) else {
continue
}
selected.append(values[offset])
offsets[source] = offset + 1
addedRecord = true
}
if !addedRecord {
break
}
}
return selected
}
private func curatedTrainingRecords(
_ records: [CorpusRecord],
classifier: ClassifierID
) -> [CorpusRecord] {
let knownRecords = records.filter {
classifier.hasKnownLabel(in: $0)
}
let generatedRecords = knownRecords.filter { $0.sourceDataset == nil }
let openRecords = knownRecords.filter { $0.sourceDataset != nil }
guard !openRecords.isEmpty else { return generatedRecords }
let generatedByLabel = Dictionary(grouping: generatedRecords) {
classifier.label(for: $0)
}
let openByLabel = Dictionary(grouping: openRecords) {
classifier.label(for: $0)
}
let multiplier = switch classifier {
case .blessing:
2.0
case .task, .question, .complaint, .confirmationDecision, .sentiment:
1.0
case .invitation, .scheduleNegotiation, .followUpReminder, .replyableMessage:
0.5
}
let selectedOpenRecords = classifier.labels.flatMap { label in
let generatedCount = generatedByLabel[label]?.count ?? 0
let limit = max(1, Int((Double(generatedCount) * multiplier).rounded()))
return sourceBalancedPrefix(
openByLabel[label] ?? [],
limit: limit,
classifier: classifier,
label: label
)
}
return generatedRecords + selectedOpenRecords
}
private func balancedTexts(
records: [CorpusRecord],
classifier: ClassifierID,
@@ -236,10 +509,31 @@ private func balancedTexts(
var generator = SeededGenerator(
seed: stableSeed(for: classifier, split: split) &+ UInt64(offset)
)
let texts = (grouped[label] ?? [])
.map(\.text)
.shuffled(using: &generator)
result[label] = Array(texts.prefix(minimumCount))
let candidates = grouped[label] ?? []
if label != classifier.positiveLabel,
!classifier.hardNegativeFamilies.isEmpty,
classifier.hardNegativeFraction > 0 {
var hardNegatives = candidates
.filter { classifier.hardNegativeFamilies.contains($0.family) }
.map(\.text)
.shuffled(using: &generator)
var remaining = candidates
.filter { !classifier.hardNegativeFamilies.contains($0.family) }
.map(\.text)
.shuffled(using: &generator)
let requestedHardNegatives = Int(
(Double(minimumCount) * classifier.hardNegativeFraction).rounded(.down)
)
let hardNegativeCount = min(hardNegatives.count, requestedHardNegatives)
hardNegatives = Array(hardNegatives.prefix(hardNegativeCount))
remaining = Array(remaining.prefix(minimumCount - hardNegativeCount))
result[label] = hardNegatives + remaining
} else {
let texts = candidates
.map(\.text)
.shuffled(using: &generator)
result[label] = Array(texts.prefix(minimumCount))
}
}
return result
}
@@ -305,6 +599,57 @@ private func binaryMetrics(
)
}
private func binaryMetrics(
records: [CorpusRecord],
classifier: ClassifierID,
positiveLabel: String,
globalThreshold: Double,
thresholdsByLanguage: [String: Double],
scores: [Double]
) -> BinaryMetrics {
precondition(records.count == scores.count)
let predictions = zip(records, scores).map { record, score in
score >= (thresholdsByLanguage[record.language] ?? globalThreshold)
}
var truePositive = 0
var trueNegative = 0
var falsePositive = 0
var falseNegative = 0
for (record, predictedPositive) in zip(records, predictions) {
let expectedPositive = classifier.label(for: record) == positiveLabel
switch (expectedPositive, predictedPositive) {
case (true, true): truePositive += 1
case (false, false): trueNegative += 1
case (false, true): falsePositive += 1
case (true, false): falseNegative += 1
}
}
let total = records.count
let precision = truePositive + falsePositive > 0
? Double(truePositive) / Double(truePositive + falsePositive)
: 0
let recall = truePositive + falseNegative > 0
? Double(truePositive) / Double(truePositive + falseNegative)
: 0
return BinaryMetrics(
total: total,
truePositive: truePositive,
trueNegative: trueNegative,
falsePositive: falsePositive,
falseNegative: falseNegative,
accuracy: rounded(
total > 0 ? Double(truePositive + trueNegative) / Double(total) : 0
),
precision: rounded(precision),
recall: rounded(recall),
f1: rounded(
precision + recall > 0
? 2 * precision * recall / (precision + recall)
: 0
)
)
}
private func scores(
classifier: MLTextClassifier,
records: [CorpusRecord],
@@ -320,6 +665,7 @@ private func binaryErrorExamples(
classifier: ClassifierID,
positiveLabel: String,
threshold: Double,
thresholdsByLanguage: [String: Double] = [:],
scores: [Double],
expectedPositive: Bool,
predictedPositive: Bool,
@@ -327,7 +673,8 @@ private func binaryErrorExamples(
) -> [String] {
zip(records, scores).compactMap { record, score -> String? in
let isExpectedPositive = classifier.label(for: record) == positiveLabel
let isPredictedPositive = score >= threshold
let effectiveThreshold = thresholdsByLanguage[record.language] ?? threshold
let isPredictedPositive = score >= effectiveThreshold
guard isExpectedPositive == expectedPositive,
isPredictedPositive == predictedPositive else {
return nil
@@ -347,7 +694,15 @@ private func calibratedThreshold(
var candidates: [(Double, BinaryMetrics)] = []
// Low-confidence positives are too unstable for automatic keyboard
// routing even when a synthetic validation split happens to accept them.
for integer in 60...99 {
let minimumThreshold = switch classifierID {
case .scheduleNegotiation, .confirmationDecision:
30
case .followUpReminder:
58
default:
60
}
for integer in minimumThreshold...99 {
let threshold = Double(integer) / 100
candidates.append(
(
@@ -366,6 +721,11 @@ private func calibratedThreshold(
let highPrecision = candidates.filter { $0.1.precision >= 0.97 }
if let best = highPrecision.max(by: {
if $0.1.recall == $1.1.recall {
if $0.1.precision == $1.1.precision {
// Prefer the lowest threshold on an identical validation
// plateau so held-out paraphrases are not needlessly lost.
return $0.0 > $1.0
}
return $0.1.precision < $1.1.precision
}
return $0.1.recall < $1.1.recall
@@ -382,6 +742,40 @@ private func calibratedThreshold(
))
}
private func calibratedThresholdsByLanguage(
records: [CorpusRecord],
classifierID: ClassifierID,
positiveLabel: String,
scores: [Double],
minimumPerClass: Int = 20
) -> [String: Double] {
var result: [String: Double] = [:]
for language in Set(records.map(\.language)).sorted() {
let indexed = records.enumerated().filter { $0.element.language == language }
let languageRecords = indexed.map(\.element)
let positiveCount = languageRecords.filter {
classifierID.label(for: $0) == positiveLabel
}.count
let negativeCount = languageRecords.count - positiveCount
guard positiveCount >= minimumPerClass, negativeCount >= minimumPerClass else {
print(
"CALIBRATION_SKIPPED classifier=\(classifierID.rawValue) "
+ "language=\(language) positives=\(positiveCount) negatives=\(negativeCount)"
)
continue
}
let languageScores = indexed.map { scores[$0.offset] }
let calibration = calibratedThreshold(
records: languageRecords,
classifierID: classifierID,
positiveLabel: positiveLabel,
scores: languageScores
)
result[language] = rounded(calibration.threshold)
}
return result
}
private func multiclassMetrics(
records: [CorpusRecord],
classifierID: ClassifierID,
@@ -462,8 +856,16 @@ private func train(
testRecords: [CorpusRecord],
goldenRecords: [CorpusRecord]
) throws -> TrainedCandidate {
// Open datasets often annotate only a subset of product intents. Excluding
// unknown labels prevents an unannotated intent from becoming a false negative.
// Source-balanced caps then preserve the reviewed base corpus as the boundary
// anchor instead of allowing one large dataset to dominate model weights.
let knownTrainingRecords = curatedTrainingRecords(
trainingRecords,
classifier: classifierID
)
let trainingTexts = balancedTexts(
records: trainingRecords,
records: knownTrainingRecords,
classifier: classifierID,
split: "train"
)
@@ -524,6 +926,15 @@ private func train(
positiveLabel: positiveLabel,
scores: validationScores
)
let calibratedLanguageThresholds = calibratedThresholdsByLanguage(
records: validationRecords,
classifierID: classifierID,
positiveLabel: positiveLabel,
scores: validationScores
)
let thresholdsByLanguage = calibratedLanguageThresholds.mapValues {
max($0, rounded(calibration.threshold))
}
let testScores = try scores(
classifier: classifier,
records: testRecords,
@@ -533,7 +944,8 @@ private func train(
records: testRecords,
classifier: classifierID,
positiveLabel: positiveLabel,
threshold: calibration.threshold,
globalThreshold: calibration.threshold,
thresholdsByLanguage: thresholdsByLanguage,
scores: testScores
)
let goldenScores = try scores(
@@ -545,7 +957,8 @@ private func train(
records: goldenRecords,
classifier: classifierID,
positiveLabel: positiveLabel,
threshold: calibration.threshold,
globalThreshold: calibration.threshold,
thresholdsByLanguage: thresholdsByLanguage,
scores: goldenScores
)
var byLanguage: [String: BinaryMetrics] = [:]
@@ -557,10 +970,21 @@ private func train(
records: records,
classifier: classifierID,
positiveLabel: positiveLabel,
threshold: calibration.threshold,
threshold: thresholdsByLanguage[language] ?? calibration.threshold,
scores: languageScores
)
}
var goldenByLanguage: [String: BinaryMetrics] = [:]
for language in Set(goldenRecords.map(\.language)).sorted() {
let indexed = goldenRecords.enumerated().filter { $0.element.language == language }
goldenByLanguage[language] = binaryMetrics(
records: indexed.map(\.element),
classifier: classifierID,
positiveLabel: positiveLabel,
threshold: thresholdsByLanguage[language] ?? calibration.threshold,
scores: indexed.map { goldenScores[$0.offset] }
)
}
report = CandidateReport(
algorithm: algorithm.rawValue,
modelBytes: modelFileSize(at: modelURL),
@@ -568,6 +992,8 @@ private func train(
balancedTrainingCount: totalCount(trainingTexts),
balancedValidationCount: totalCount(validationTexts),
threshold: rounded(calibration.threshold),
confidenceThresholdsByLanguage:
thresholdsByLanguage.isEmpty ? nil : thresholdsByLanguage,
acceptedForAutomaticRouting: algorithm == .maxEnt
&& calibration.metrics.precision >= 0.97
&& testMetrics.precision >= 0.90
@@ -580,6 +1006,7 @@ private func train(
classifier: classifierID,
positiveLabel: positiveLabel,
threshold: calibration.threshold,
thresholdsByLanguage: thresholdsByLanguage,
scores: testScores,
expectedPositive: false,
predictedPositive: true
@@ -589,6 +1016,7 @@ private func train(
classifier: classifierID,
positiveLabel: positiveLabel,
threshold: calibration.threshold,
thresholdsByLanguage: thresholdsByLanguage,
scores: testScores,
expectedPositive: true,
predictedPositive: false
@@ -598,6 +1026,7 @@ private func train(
classifier: classifierID,
positiveLabel: positiveLabel,
threshold: calibration.threshold,
thresholdsByLanguage: thresholdsByLanguage,
scores: goldenScores,
expectedPositive: false,
predictedPositive: true
@@ -607,11 +1036,13 @@ private func train(
classifier: classifierID,
positiveLabel: positiveLabel,
threshold: calibration.threshold,
thresholdsByLanguage: thresholdsByLanguage,
scores: goldenScores,
expectedPositive: true,
predictedPositive: false
),
binaryByLanguage: byLanguage,
goldenBinaryByLanguage: goldenByLanguage,
validationMulticlass: nil,
testMulticlass: nil,
goldenMulticlass: nil,
@@ -658,6 +1089,7 @@ private func train(
balancedTrainingCount: totalCount(trainingTexts),
balancedValidationCount: totalCount(validationTexts),
threshold: nil,
confidenceThresholdsByLanguage: nil,
acceptedForAutomaticRouting: algorithm == .maxEnt
&& validationMetrics.macroF1 >= 0.85
&& testMetrics.macroF1 >= 0.85
@@ -670,6 +1102,7 @@ private func train(
goldenFalsePositiveExamples: nil,
goldenFalseNegativeExamples: nil,
binaryByLanguage: nil,
goldenBinaryByLanguage: nil,
validationMulticlass: validationMetrics,
testMulticlass: testMetrics,
goldenMulticlass: goldenMetrics,
@@ -723,7 +1156,7 @@ private func selectedAlgorithms() -> [CandidateAlgorithm] {
guard let index = CommandLine.arguments.firstIndex(of: "--algorithms"),
CommandLine.arguments.indices.contains(index + 1)
else {
return CandidateAlgorithm.allCases
return [.maxEnt]
}
let requested = Set(
CommandLine.arguments[index + 1]
@@ -834,6 +1267,8 @@ private func main() throws {
labels: classifierID.labels,
positiveLabel: classifierID.positiveLabel,
confidenceThreshold: selected.report.threshold,
confidenceThresholdsByLanguage:
selected.report.confidenceThresholdsByLanguage,
acceptedForAutomaticRouting: selected.report.acceptedForAutomaticRouting
)
)
@@ -845,14 +1280,18 @@ private func main() throws {
let report = TrainingReport(
generatedAt: generatedAt,
corpusPath: "ModelTraining/ClipboardSemantics/clipboard_semantic_corpus.jsonl",
corpusPath: corpusURL.path,
corpusCount: records.count,
trainingCount: trainingRecords.count,
validationCount: validationRecords.count,
testCount: testRecords.count,
goldenCount: goldenRecords.count,
selectionPolicy:
"Validation only: binary models require precision >= 0.97, then maximize recall; "
"Open records with unknown labels are excluded per classifier, and source-balanced "
+ "caps anchor each label to the reviewed generated corpus size. "
+ "Validation only: global and per-language binary thresholds require precision "
+ ">= 0.97, then maximize recall; languages with fewer than 20 examples per class "
+ "fall back to the global threshold. "
+ "sentiment prioritizes macro-F1. Automatic routing also requires a self-contained "
+ "maxEnt model because BERT embedding assets are not guaranteed in extensions. "
+ "Test and golden data gate deployment but never tune model weights.",
@@ -861,7 +1300,7 @@ private func main() throws {
try writeJSON(report, to: reportURL)
try writeJSON(
ModelManifest(
schemaVersion: 1,
schemaVersion: 2,
generatedAt: generatedAt,
corpusRecordCount: records.count,
classifiers: manifestClassifiers
@@ -0,0 +1,716 @@
#!/usr/bin/env xcrun swift
import CreateML
import Foundation
private struct BaseRecord: Decodable {
let id: String
let text: String
let language: String
let split: String
let task: Bool
let question: Bool
let invitation: Bool
let complaint: Bool
let scheduleNegotiation: Bool
let confirmationDecision: Bool
let followUpReminder: Bool
}
private struct SilverRecord: Decodable {
let id: String
let text: String
let language: String
let split: String
let actionVerifierLabel: String
let coordinationVerifierLabel: String
let sourceDataset: String?
}
private struct VerifierExample {
let id: String
let text: String
let language: String
let split: String
let label: String
let sourceDataset: String?
}
private struct RoutingMetrics: Encodable {
let total: Int
let expectedSpecialized: Int
let routed: Int
let correctRouted: Int
let falseRouted: Int
let missedSpecialized: Int
let precision: Double
let recall: Double
let f1: Double
let routedByLabel: [String: Int]
let correctByLabel: [String: Int]
}
private struct ThresholdSelection: Encodable {
let confidenceThreshold: Double
let minimumMargin: Double
let metrics: RoutingMetrics
}
private struct GateResult: Encodable {
let accepted: Bool
let reason: String
let minimumPredictedPositivesPerLabelAndLanguage: Int
let supportByLabelAndLanguage: [String: Int]
let precisionByLabelAndLanguage: [String: Double]
let maximumSourceShareByLabelAndLanguage: [String: Double]
}
private struct VerifierReport: Encodable {
let id: String
let labels: [String]
let modelFile: String
let modelBytes: Int
let trainingCount: Int
let calibrationCount: Int
let thresholdCalibrationCount: Int
let acceptanceCount: Int
let threshold: ThresholdSelection
let thresholdsByLanguage: [String: ThresholdSelection]
let calibrationMetrics: RoutingMetrics
let acceptanceMetrics: RoutingMetrics
let acceptanceByLanguage: [String: RoutingMetrics]
let gate: GateResult
}
private struct TrainingReport: Encodable {
let generatedAt: String
let baseCorpusPath: String
let silverDirectoryPath: String
let selectionPolicy: String
let verifiers: [VerifierReport]
}
private struct Observation {
let example: VerifierExample
let predictedLabel: String
let confidence: Double
let margin: Double
}
private enum VerifierID: String, CaseIterable {
case action
case coordination
var labels: [String] {
switch self {
case .action:
["taskOnly", "complaintOnly", "both", "questionRequest", "neither"]
case .coordination:
[
"invitation",
"scheduleNegotiation",
"confirmationDecision",
"followUpReminder",
"neither"
]
}
}
var modelFile: String {
switch self {
case .action: "ActionIntentVerifier.mlmodel"
case .coordination: "CoordinationIntentVerifier.mlmodel"
}
}
func baseLabel(for record: BaseRecord) -> String? {
switch self {
case .action:
if record.task && record.complaint {
return "both"
}
if record.task {
return "taskOnly"
}
if record.complaint {
return "complaintOnly"
}
if record.question {
return "questionRequest"
}
return "neither"
case .coordination:
let matches = [
record.invitation ? "invitation" : nil,
record.scheduleNegotiation ? "scheduleNegotiation" : nil,
record.confirmationDecision ? "confirmationDecision" : nil,
record.followUpReminder ? "followUpReminder" : nil
].compactMap { $0 }
guard matches.count <= 1 else { return nil }
return matches.first ?? "neither"
}
}
func silverLabel(for record: SilverRecord) -> String {
switch self {
case .action: record.actionVerifierLabel
case .coordination: record.coordinationVerifierLabel
}
}
}
private struct SeededGenerator: RandomNumberGenerator {
private var state: UInt64
init(seed: UInt64) {
state = seed
}
mutating func next() -> UInt64 {
state &+= 0x9E37_79B9_7F4A_7C15
var value = state
value = (value ^ (value >> 30)) &* 0xBF58_476D_1CE4_E5B9
value = (value ^ (value >> 27)) &* 0x94D0_49BB_1331_11EB
return value ^ (value >> 31)
}
}
private let fileManager = FileManager.default
private let root = URL(fileURLWithPath: fileManager.currentDirectoryPath)
private func argumentValue(after flag: String) -> String? {
guard let index = CommandLine.arguments.firstIndex(of: flag),
CommandLine.arguments.indices.contains(index + 1) else {
return nil
}
return CommandLine.arguments[index + 1]
}
private func resolvedURL(flag: String, defaultPath: String) -> URL {
URL(
fileURLWithPath: argumentValue(after: flag) ?? defaultPath,
relativeTo: root
).standardizedFileURL
}
private let baseCorpusURL = resolvedURL(
flag: "--base-corpus",
defaultPath: "ModelTraining/ClipboardSemantics/clipboard_semantic_corpus.jsonl"
)
private let silverDirectoryURL = resolvedURL(
flag: "--silver-directory",
defaultPath: "ModelTraining/ClipboardSemantics/Consensus"
)
private let baseManifestURL = resolvedURL(
flag: "--base-manifest",
defaultPath:
"OSGKeyboardShared/Resources/ClipboardSemantics/clipboard-semantic-models.json"
)
private let outputDirectoryURL = resolvedURL(
flag: "--output-directory",
defaultPath: "ModelTraining/ClipboardSemantics/VerifierCandidates"
)
private let reportURL = resolvedURL(
flag: "--report",
defaultPath: "ModelTraining/ClipboardSemantics/verifier-training-report.json"
)
private func readJSONLines<T: Decodable>(_ type: T.Type, from url: URL) throws -> [T] {
let content = try String(contentsOf: url, encoding: .utf8)
let decoder = JSONDecoder()
return try content.split(separator: "\n").map {
try decoder.decode(type, from: Data($0.utf8))
}
}
private func rounded(_ value: Double) -> Double {
guard value.isFinite else { return 0 }
return (value * 10_000).rounded() / 10_000
}
private func stableSeed(_ value: String) -> UInt64 {
value.utf8.reduce(0xcbf2_9ce4_8422_2325) { partial, byte in
(partial ^ UInt64(byte)) &* 0x0000_0100_0000_01B3
}
}
private func examples(
verifier: VerifierID,
baseRecords: [BaseRecord],
silverRecords: [SilverRecord],
baseSplit: String,
silverSplit: String
) -> [VerifierExample] {
let base = baseRecords.compactMap { record -> VerifierExample? in
guard record.split == baseSplit,
let label = verifier.baseLabel(for: record) else {
return nil
}
return VerifierExample(
id: record.id,
text: record.text,
language: record.language,
split: baseSplit,
label: label,
sourceDataset: nil
)
}
let silver = silverRecords.compactMap { record -> VerifierExample? in
guard record.split == silverSplit else { return nil }
return VerifierExample(
id: record.id,
text: record.text,
language: record.language,
split: silverSplit,
label: verifier.silverLabel(for: record),
sourceDataset: record.sourceDataset
)
}
return base + silver
}
private func balancedTexts(
examples: [VerifierExample],
verifier: VerifierID,
split: String
) -> [String: [String]] {
var grouped = Dictionary(grouping: examples, by: \.label)
if verifier == .action {
let taskExamples = grouped["taskOnly"] ?? []
let complaintExamples = grouped["complaintOnly"] ?? []
let targetCount = min(taskExamples.count, complaintExamples.count)
var bothExamples = grouped["both"] ?? []
if !taskExamples.isEmpty, !complaintExamples.isEmpty {
for index in bothExamples.count..<targetCount {
let task = taskExamples[index % taskExamples.count]
let complaint = complaintExamples[
Int(
stableSeed("\(split)|both|\(index)")
% UInt64(complaintExamples.count)
)
]
bothExamples.append(
VerifierExample(
id: "composed-both-\(split)-\(index)",
text: "\(complaint.text)\n\(task.text)",
language: task.language,
split: split,
label: "both",
sourceDataset: "generated-composition"
)
)
}
grouped["both"] = bothExamples
}
}
let minimumCount = verifier.labels.compactMap { grouped[$0]?.count }.min() ?? 0
precondition(minimumCount > 0, "Missing \(verifier.rawValue) label in \(split)")
return Dictionary(uniqueKeysWithValues: verifier.labels.enumerated().map { offset, label in
var generator = SeededGenerator(
seed: stableSeed("\(verifier.rawValue)|\(split)|\(label)|\(offset)")
)
let texts = (grouped[label] ?? []).map(\.text).shuffled(using: &generator)
return (label, Array(texts.prefix(minimumCount)))
})
}
private func observations(
model: MLTextClassifier,
examples: [VerifierExample]
) throws -> [Observation] {
try examples.map { example in
let hypotheses = try model.predictionWithConfidence(from: example.text)
.sorted { $0.value > $1.value }
let winner = hypotheses.first ?? (key: "neither", value: 0)
let runnerUp = hypotheses.dropFirst().first?.value ?? 0
return Observation(
example: example,
predictedLabel: winner.key,
confidence: winner.value,
margin: winner.value - runnerUp
)
}
}
private func routingMetrics(
observations: [Observation],
confidenceThreshold: Double,
minimumMargin: Double
) -> RoutingMetrics {
var routed = 0
var correctRouted = 0
var expectedSpecialized = 0
var missedSpecialized = 0
var routedByLabel: [String: Int] = [:]
var correctByLabel: [String: Int] = [:]
for observation in observations {
let expectedIsSpecialized = observation.example.label != "neither"
if expectedIsSpecialized {
expectedSpecialized += 1
}
let shouldRoute = observation.predictedLabel != "neither"
&& observation.confidence >= confidenceThreshold
&& observation.margin >= minimumMargin
if shouldRoute {
routed += 1
routedByLabel[observation.predictedLabel, default: 0] += 1
if observation.predictedLabel == observation.example.label {
correctRouted += 1
correctByLabel[observation.predictedLabel, default: 0] += 1
}
} else if expectedIsSpecialized {
missedSpecialized += 1
}
}
let precision = routed > 0 ? Double(correctRouted) / Double(routed) : 0
let recall = expectedSpecialized > 0
? Double(correctRouted) / Double(expectedSpecialized)
: 0
return RoutingMetrics(
total: observations.count,
expectedSpecialized: expectedSpecialized,
routed: routed,
correctRouted: correctRouted,
falseRouted: routed - correctRouted,
missedSpecialized: missedSpecialized,
precision: rounded(precision),
recall: rounded(recall),
f1: rounded(
precision + recall > 0
? 2 * precision * recall / (precision + recall)
: 0
),
routedByLabel: routedByLabel,
correctByLabel: correctByLabel
)
}
private func selectedThreshold(
observations: [Observation]
) -> ThresholdSelection {
var selections: [ThresholdSelection] = []
for confidenceStep in 50...99 {
for marginStep in 0...10 {
let confidence = Double(confidenceStep) / 100
let margin = Double(marginStep) / 20
let metrics = routingMetrics(
observations: observations,
confidenceThreshold: confidence,
minimumMargin: margin
)
if metrics.precision >= 0.95 {
selections.append(
ThresholdSelection(
confidenceThreshold: confidence,
minimumMargin: margin,
metrics: metrics
)
)
}
}
}
return selections.max {
if $0.metrics.recall != $1.metrics.recall {
return $0.metrics.recall < $1.metrics.recall
}
if $0.metrics.routed != $1.metrics.routed {
return $0.metrics.routed < $1.metrics.routed
}
if $0.confidenceThreshold != $1.confidenceThreshold {
return $0.confidenceThreshold > $1.confidenceThreshold
}
return $0.minimumMargin > $1.minimumMargin
} ?? ThresholdSelection(
confidenceThreshold: 1,
minimumMargin: 1,
metrics: routingMetrics(
observations: observations,
confidenceThreshold: 1,
minimumMargin: 1
)
)
}
private func gateResult(
verifier: VerifierID,
observations: [Observation],
thresholdsByLanguage: [String: ThresholdSelection],
globalThreshold: ThresholdSelection
) -> GateResult {
var support: [String: Int] = [:]
var correct: [String: Int] = [:]
var sourceSupport: [String: [String: Int]] = [:]
for observation in observations {
let selection = thresholdsByLanguage[observation.example.language]
?? globalThreshold
let routed = observation.predictedLabel != "neither"
&& observation.confidence >= selection.confidenceThreshold
&& observation.margin >= selection.minimumMargin
guard routed else { continue }
let key = "\(observation.example.language)|\(observation.predictedLabel)"
support[key, default: 0] += 1
let source = observation.example.sourceDataset ?? "generated-base"
sourceSupport[key, default: [:]][source, default: 0] += 1
if observation.predictedLabel == observation.example.label {
correct[key, default: 0] += 1
}
}
let precision = Dictionary(uniqueKeysWithValues: support.map { key, count in
(
key,
rounded(Double(correct[key] ?? 0) / Double(max(count, 1)))
)
})
let maximumSourceShare = Dictionary(uniqueKeysWithValues: support.map { key, count in
let maximum = sourceSupport[key]?.values.max() ?? 0
return (key, rounded(Double(maximum) / Double(max(count, 1))))
})
let requiredKeys = ["en", "zh-Hans"].flatMap { language in
verifier.labels
.filter { $0 != "neither" }
.map { "\(language)|\($0)" }
}
let accepted = requiredKeys.allSatisfy {
support[$0, default: 0] >= 100
&& precision[$0, default: 0] >= 0.95
&& maximumSourceShare[$0, default: 1] <= 0.65
}
return GateResult(
accepted: accepted,
reason: accepted
? "Every routed label/language meets precision, support, and source-diversity gates."
: "Shadow only: at least one label/language misses precision, support, or source-diversity gates.",
minimumPredictedPositivesPerLabelAndLanguage: 100,
supportByLabelAndLanguage: support,
precisionByLabelAndLanguage: precision,
maximumSourceShareByLabelAndLanguage: maximumSourceShare
)
}
private func writeJSON<T: Encodable>(_ value: T, to url: URL) throws {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
try encoder.encode(value).write(to: url, options: .atomic)
}
private func train(
verifier: VerifierID,
baseRecords: [BaseRecord],
silverRecords: [SilverRecord]
) throws -> (VerifierReport, [String: Any]) {
let training = examples(
verifier: verifier,
baseRecords: baseRecords,
silverRecords: silverRecords,
baseSplit: "train",
silverSplit: "silverTrain"
)
let calibration = examples(
verifier: verifier,
baseRecords: baseRecords,
silverRecords: silverRecords,
baseSplit: "validation",
silverSplit: "silverCalibration"
)
let acceptance = examples(
verifier: verifier,
baseRecords: baseRecords,
silverRecords: silverRecords,
baseSplit: "golden",
silverSplit: "silverAcceptance"
)
let trainingTexts = balancedTexts(
examples: training,
verifier: verifier,
split: "train"
)
let calibrationTexts = balancedTexts(
examples: calibration,
verifier: verifier,
split: "calibration"
)
let parameters = MLTextClassifier.ModelParameters(
validation: .dictionary(calibrationTexts),
algorithm: .maxEnt(revision: 1)
)
let model = try MLTextClassifier(
trainingData: trainingTexts,
parameters: parameters
)
let modelURL = outputDirectoryURL.appendingPathComponent(verifier.modelFile)
try model.write(to: modelURL)
let calibrationObservations = try observations(model: model, examples: calibration)
let acceptanceObservations = try observations(model: model, examples: acceptance)
let externalCalibrationObservations = calibrationObservations.filter {
$0.example.sourceDataset != nil
}
let thresholdObservations = externalCalibrationObservations.count >= 40
? externalCalibrationObservations
: calibrationObservations
let globalThreshold = selectedThreshold(observations: thresholdObservations)
let languages = Set(calibration.map(\.language)).sorted()
let thresholdsByLanguage = Dictionary(uniqueKeysWithValues: languages.map { language in
let externalLanguageObservations = thresholdObservations.filter {
$0.example.language == language
}
let languageObservations = externalLanguageObservations.count >= 15
? externalLanguageObservations
: calibrationObservations.filter {
$0.example.language == language
}
return (
language,
selectedThreshold(
observations: languageObservations
)
)
})
let finalAcceptanceMetrics = routingMetrics(
observations: acceptanceObservations,
confidenceThreshold: globalThreshold.confidenceThreshold,
minimumMargin: globalThreshold.minimumMargin
)
let acceptanceByLanguage: [String: RoutingMetrics] = Dictionary(
uniqueKeysWithValues: languages.map { language in
let threshold = thresholdsByLanguage[language] ?? globalThreshold
return (
language,
routingMetrics(
observations: acceptanceObservations.filter {
$0.example.language == language
},
confidenceThreshold: threshold.confidenceThreshold,
minimumMargin: threshold.minimumMargin
)
)
}
)
let gate = gateResult(
verifier: verifier,
observations: acceptanceObservations,
thresholdsByLanguage: thresholdsByLanguage,
globalThreshold: globalThreshold
)
let modelBytes = (
try fileManager.attributesOfItem(atPath: modelURL.path)[.size] as? NSNumber
)?.intValue ?? 0
let report = VerifierReport(
id: verifier.rawValue,
labels: verifier.labels,
modelFile: verifier.modelFile,
modelBytes: modelBytes,
trainingCount: trainingTexts.values.reduce(0) { $0 + $1.count },
calibrationCount: calibration.count,
thresholdCalibrationCount: thresholdObservations.count,
acceptanceCount: acceptance.count,
threshold: globalThreshold,
thresholdsByLanguage: thresholdsByLanguage,
calibrationMetrics: globalThreshold.metrics,
acceptanceMetrics: finalAcceptanceMetrics,
acceptanceByLanguage: acceptanceByLanguage,
gate: gate
)
let configuration: [String: Any] = [
"id": verifier.rawValue,
"modelFile": verifier.modelFile,
"labels": verifier.labels,
"confidenceThreshold": globalThreshold.confidenceThreshold,
"confidenceThresholdsByLanguage": thresholdsByLanguage.mapValues {
$0.confidenceThreshold
},
"minimumMargin": globalThreshold.minimumMargin,
"minimumMarginsByLanguage": thresholdsByLanguage.mapValues {
$0.minimumMargin
},
"acceptedForAutomaticRouting": gate.accepted,
"deploymentMode": gate.accepted ? "automatic" : "shadow"
]
return (report, configuration)
}
private func main() throws {
try fileManager.createDirectory(
at: outputDirectoryURL,
withIntermediateDirectories: true
)
let baseRecords = try readJSONLines(BaseRecord.self, from: baseCorpusURL)
let silverURLs = [
silverDirectoryURL.appendingPathComponent("silver-train.jsonl"),
silverDirectoryURL.appendingPathComponent("silver-calibration.jsonl"),
silverDirectoryURL.appendingPathComponent("silver-acceptance.jsonl")
]
let silverRecords = try silverURLs.flatMap {
try readJSONLines(SilverRecord.self, from: $0)
}
var reports: [VerifierReport] = []
var configurations: [[String: Any]] = []
for verifier in VerifierID.allCases {
let (report, configuration) = try train(
verifier: verifier,
baseRecords: baseRecords,
silverRecords: silverRecords
)
reports.append(report)
configurations.append(configuration)
print(
"VERIFIER_SELECTED id=\(verifier.rawValue) "
+ "accepted=\(report.gate.accepted) "
+ "precision=\(report.acceptanceMetrics.precision)"
)
}
let generatedAt = ISO8601DateFormatter().string(from: Date())
let report = TrainingReport(
generatedAt: generatedAt,
baseCorpusPath: baseCorpusURL.path,
silverDirectoryPath: silverDirectoryURL.path,
selectionPolicy:
"Thresholds and top-1/top-2 margins are calibrated without acceptance data. "
+ "Automatic routing requires >=95% exact-route precision and >=100 routed "
+ "positives for every specialized label in both English and Simplified Chinese. "
+ "Without human gold, passing values remain consensus-relative.",
verifiers: reports
)
try writeJSON(report, to: reportURL)
let manifestData = try Data(contentsOf: baseManifestURL)
guard var manifest = try JSONSerialization.jsonObject(
with: manifestData
) as? [String: Any] else {
throw NSError(
domain: "VerifierTraining",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "Invalid base manifest"]
)
}
for classifier in manifest["classifiers"] as? [[String: Any]] ?? [] {
guard let modelFile = classifier["modelFile"] as? String else { continue }
let source = baseManifestURL.deletingLastPathComponent()
.appendingPathComponent(modelFile)
let destination = outputDirectoryURL.appendingPathComponent(modelFile)
if fileManager.fileExists(atPath: destination.path) {
try fileManager.removeItem(at: destination)
}
try fileManager.copyItem(at: source, to: destination)
}
manifest["schemaVersion"] = 3
manifest["verifiers"] = configurations
manifest["verifierGeneratedAt"] = generatedAt
manifest["verifierLabelPolicy"] = "multi-model-consensus-without-human-gold"
let outputManifestURL = outputDirectoryURL.appendingPathComponent(
"clipboard-semantic-models.json"
)
let outputManifestData = try JSONSerialization.data(
withJSONObject: manifest,
options: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
)
try outputManifestData.write(to: outputManifestURL, options: .atomic)
print("VERIFIER_REPORT \(reportURL.path)")
}
do {
try main()
} catch {
fputs("Verifier training failed: \(error)\n", stderr)
exit(1)
}