78 lines
3.7 KiB
Swift
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)
|
|
}
|
|
}
|