Files
ReadViewSDK/Sources/RDAIReaderView/FoundationModels/RDAIAppleFoundationModelsProvider.swift
T
2026-07-27 21:43:13 +08:00

78 lines
3.7 KiB
Swift

import Foundation
import FoundationModels
@available(iOS 26.0, *)
@Generable(description: "A factual statement grounded in one or more supplied passage identifiers.")
private struct RDAIModelStatement {
var text: String
var passageIDs: [String]
}
@available(iOS 26.0, *)
@Generable(description: "A citation-grounded book summary.")
private struct RDAIModelSummary {
var overview: String
var statements: [RDAIModelStatement]
}
@available(iOS 26.0, *)
@Generable(description: "A citation-grounded answer to a question about supplied passages.")
private struct RDAIModelAnswer {
var answer: String
var statements: [RDAIModelStatement]
}
/// Optional on-device provider. The Core service remains fully functional on
/// older systems by falling back to evidence-only retrieval.
@available(iOS 26.0, *)
public actor RDAIAppleFoundationModelsProvider: RDAIGenerativeProvider {
public let identifier = "apple.foundation-models"
public init() {}
public func availability(for capability: RDAICapability, locale: Locale?) async -> RDAICapabilityAvailability {
switch SystemLanguageModel.default.availability {
case .available: return .available
case .unavailable(.deviceNotEligible): return .unavailable(reason: .deviceNotEligible)
case .unavailable(.appleIntelligenceNotEnabled): return .unavailable(reason: .appleIntelligenceNotEnabled)
case .unavailable(.modelNotReady): return .unavailable(reason: .modelNotReady)
@unknown default: return .unavailable(reason: .unknown("system-model-unavailable"))
}
}
public func generate(_ request: RDAIGenerationRequest) async throws -> RDAIGeneratedArtifact {
guard case .available = await availability(for: capability(for: request.task), locale: request.locale) else {
throw RDAIError.modelUnavailable(.modelNotReady)
}
let context = request.passages.enumerated().map { "[\($0.element.id)] \($0.element.text)" }.joined(separator: "\n\n")
let task: String
switch request.task {
case .summary: task = "Summarize the passages in the user's language. Do not add facts."
case .answer: task = "Answer the question using only the passages. If evidence is insufficient, say so. Question: \(request.userText ?? "")"
case .characters, .relationships: throw RDAIError.insufficientEvidence
}
let session = LanguageModelSession(instructions: "You are a book reading assistant. Use only supplied passages and never reveal unread content.")
switch request.task {
case .summary:
let response = try await session.respond(to: "\(task) Each statement must include its supplied passage IDs.\n\nPassages:\n\(context)", generating: RDAIModelSummary.self)
return .summary(overview: response.content.overview, statements: response.content.statements.map(statement))
case .answer:
let response = try await session.respond(to: "\(task) Each statement must include its supplied passage IDs.\n\nPassages:\n\(context)", generating: RDAIModelAnswer.self)
return .answer(text: response.content.answer, statements: response.content.statements.map(statement))
case .characters, .relationships: throw RDAIError.insufficientEvidence
}
}
private func capability(for task: RDAIGenerationTask) -> RDAICapability {
switch task {
case .summary: return .summarization
case .answer: return .questionAnswering
case .characters, .relationships: return .characterRelationships
}
}
private func statement(_ value: RDAIModelStatement) -> RDAISourcedStatement {
RDAISourcedStatement(text: value.text, citationIdentifiers: value.passageIDs)
}
}