Files
Sybil-2/ios/Packages/Sybil/Sources/Sybil/SybilModels.swift

608 lines
16 KiB
Swift

import Foundation
public enum Provider: String, Codable, CaseIterable, Hashable, Sendable {
case openai
case anthropic
case xai
case hermesAgent = "hermes-agent"
public var displayName: String {
switch self {
case .openai: return "OpenAI"
case .anthropic: return "Anthropic"
case .xai: return "xAI"
case .hermesAgent: return "Hermes Agent"
}
}
}
public enum MessageRole: String, Codable, Hashable, Sendable {
case system
case user
case assistant
case tool
}
public struct ChatAttachment: Codable, Hashable, Identifiable, Sendable {
public enum Kind: String, Codable, Hashable, Sendable {
case image
case text
}
public var id: String
public var kind: Kind
public var filename: String
public var mimeType: String
public var sizeBytes: Int
public var dataUrl: String?
public var text: String?
public var truncated: Bool?
public init(
id: String,
kind: Kind,
filename: String,
mimeType: String,
sizeBytes: Int,
dataUrl: String? = nil,
text: String? = nil,
truncated: Bool? = nil
) {
self.id = id
self.kind = kind
self.filename = filename
self.mimeType = mimeType
self.sizeBytes = sizeBytes
self.dataUrl = dataUrl
self.text = text
self.truncated = truncated
}
public static func image(
id: String = UUID().uuidString,
filename: String,
mimeType: String,
sizeBytes: Int,
dataUrl: String
) -> ChatAttachment {
ChatAttachment(
id: id,
kind: .image,
filename: filename,
mimeType: mimeType,
sizeBytes: sizeBytes,
dataUrl: dataUrl
)
}
public static func text(
id: String = UUID().uuidString,
filename: String,
mimeType: String,
sizeBytes: Int,
text: String,
truncated: Bool
) -> ChatAttachment {
ChatAttachment(
id: id,
kind: .text,
filename: filename,
mimeType: mimeType,
sizeBytes: sizeBytes,
text: text,
truncated: truncated
)
}
var jsonValue: JSONValue {
var object: [String: JSONValue] = [
"kind": .string(kind.rawValue),
"id": .string(id),
"filename": .string(filename),
"mimeType": .string(mimeType),
"sizeBytes": .number(Double(sizeBytes))
]
if let dataUrl {
object["dataUrl"] = .string(dataUrl)
}
if let text {
object["text"] = .string(text)
}
if let truncated {
object["truncated"] = .bool(truncated)
}
return .object(object)
}
static func attachments(from metadata: JSONValue?) -> [ChatAttachment] {
guard let metadataObject = metadata?.objectValue,
let values = metadataObject["attachments"]?.arrayValue
else {
return []
}
return values.compactMap { value in
guard let object = value.objectValue,
let kindRaw = object["kind"]?.stringValue,
let kind = Kind(rawValue: kindRaw),
let id = object["id"]?.stringValue,
let filename = object["filename"]?.stringValue,
let mimeType = object["mimeType"]?.stringValue,
let sizeNumber = object["sizeBytes"]?.numberValue
else {
return nil
}
return ChatAttachment(
id: id,
kind: kind,
filename: filename,
mimeType: mimeType,
sizeBytes: Int(sizeNumber),
dataUrl: object["dataUrl"]?.stringValue,
text: object["text"]?.stringValue,
truncated: object["truncated"]?.boolValue
)
}
}
}
public struct ChatSummary: Codable, Identifiable, Hashable, Sendable {
public var id: String
public var title: String?
public var createdAt: Date
public var updatedAt: Date
public var initiatedProvider: Provider?
public var initiatedModel: String?
public var lastUsedProvider: Provider?
public var lastUsedModel: String?
}
public struct SearchSummary: Codable, Identifiable, Hashable, Sendable {
public var id: String
public var title: String?
public var query: String?
public var createdAt: Date
public var updatedAt: Date
}
public struct Message: Codable, Identifiable, Hashable, Sendable {
public var id: String
public var createdAt: Date
public var role: MessageRole
public var content: String
public var name: String?
public var metadata: JSONValue? = nil
public var attachments: [ChatAttachment] {
ChatAttachment.attachments(from: metadata)
}
public var toolCallMetadata: ToolCallMetadata? {
guard role == .tool,
let object = metadata?.objectValue,
object["kind"]?.stringValue == "tool_call"
else { return nil }
return ToolCallMetadata(
toolCallId: object["toolCallId"]?.stringValue,
toolName: object["toolName"]?.stringValue ?? name,
status: object["status"]?.stringValue,
summary: object["summary"]?.stringValue,
durationMs: object["durationMs"]?.numberValue.map(Int.init),
error: object["error"]?.stringValue,
resultPreview: object["resultPreview"]?.stringValue
)
}
public var isToolCallLog: Bool {
toolCallMetadata != nil
}
}
public struct ToolCallMetadata: Hashable, Sendable {
public var toolCallId: String?
public var toolName: String?
public var status: String?
public var summary: String?
public var durationMs: Int?
public var error: String?
public var resultPreview: String?
}
public enum JSONValue: Codable, Hashable, Sendable {
case string(String)
case number(Double)
case bool(Bool)
case object([String: JSONValue])
case array([JSONValue])
case null
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self = .null
return
}
if let value = try? container.decode(Bool.self) {
self = .bool(value)
return
}
if let value = try? container.decode(Double.self) {
self = .number(value)
return
}
if let value = try? container.decode(String.self) {
self = .string(value)
return
}
if let value = try? container.decode([String: JSONValue].self) {
self = .object(value)
return
}
if let value = try? container.decode([JSONValue].self) {
self = .array(value)
return
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON value")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case let .string(value):
try container.encode(value)
case let .number(value):
try container.encode(value)
case let .bool(value):
try container.encode(value)
case let .object(value):
try container.encode(value)
case let .array(value):
try container.encode(value)
case .null:
try container.encodeNil()
}
}
public var stringValue: String? {
if case let .string(value) = self {
return value
}
return nil
}
public var numberValue: Double? {
if case let .number(value) = self {
return value
}
return nil
}
public var objectValue: [String: JSONValue]? {
if case let .object(value) = self {
return value
}
return nil
}
public var arrayValue: [JSONValue]? {
if case let .array(value) = self {
return value
}
return nil
}
public var boolValue: Bool? {
if case let .bool(value) = self {
return value
}
return nil
}
}
public struct ChatDetail: Codable, Identifiable, Hashable, Sendable {
public var id: String
public var title: String?
public var createdAt: Date
public var updatedAt: Date
public var initiatedProvider: Provider?
public var initiatedModel: String?
public var lastUsedProvider: Provider?
public var lastUsedModel: String?
public var messages: [Message]
}
public struct SearchResultItem: Codable, Identifiable, Hashable, Sendable {
public var id: String
public var createdAt: Date
public var rank: Int
public var title: String?
public var url: String
public var publishedDate: String?
public var author: String?
public var text: String?
public var highlights: [String]?
public var highlightScores: [Double]?
public var score: Double?
public var favicon: String?
public var image: String?
}
public struct SearchCitation: Codable, Hashable, Sendable {
public var id: String?
public var url: String?
public var title: String?
public var publishedDate: String?
public var author: String?
public var text: String?
}
public struct SearchDetail: Codable, Identifiable, Hashable, Sendable {
public var id: String
public var title: String?
public var query: String?
public var createdAt: Date
public var updatedAt: Date
public var requestId: String?
public var latencyMs: Int?
public var error: String?
public var answerText: String?
public var answerRequestId: String?
public var answerCitations: [SearchCitation]?
public var answerError: String?
public var results: [SearchResultItem]
}
public struct ActiveRunsResponse: Codable, Hashable, Sendable {
public var chats: [String]
public var searches: [String]
public init(chats: [String] = [], searches: [String] = []) {
self.chats = chats
self.searches = searches
}
}
public struct SearchRunRequest: Codable, Sendable {
public var query: String?
public var title: String?
public var type: String?
public var numResults: Int?
public var includeDomains: [String]?
public var excludeDomains: [String]?
public init(
query: String? = nil,
title: String? = nil,
type: String? = nil,
numResults: Int? = nil,
includeDomains: [String]? = nil,
excludeDomains: [String]? = nil
) {
self.query = query
self.title = title
self.type = type
self.numResults = numResults
self.includeDomains = includeDomains
self.excludeDomains = excludeDomains
}
}
public struct CompletionRequestMessage: Codable, Sendable {
public var role: MessageRole
public var content: String
public var name: String?
public var attachments: [ChatAttachment]?
public init(role: MessageRole, content: String, name: String? = nil, attachments: [ChatAttachment]? = nil) {
self.role = role
self.content = content
self.name = name
self.attachments = attachments
}
}
public struct CompletionStreamMeta: Codable, Sendable {
public var chatId: String
public var callId: String
public var provider: Provider
public var model: String
}
public struct CompletionStreamDelta: Codable, Sendable {
public var text: String
}
public struct CompletionStreamDone: Codable, Sendable {
public var text: String
}
public struct CompletionStreamToolCall: Codable, Sendable {
public var toolCallId: String
public var name: String
public var status: String
public var summary: String
public var args: [String: JSONValue]
public var startedAt: String
public var completedAt: String
public var durationMs: Int
public var error: String?
public var resultPreview: String?
}
public struct StreamErrorPayload: Codable, Sendable {
public var message: String
}
public enum CompletionStreamEvent: Sendable {
case meta(CompletionStreamMeta)
case toolCall(CompletionStreamToolCall)
case delta(CompletionStreamDelta)
case done(CompletionStreamDone)
case error(StreamErrorPayload)
case ignored
}
public struct SearchResultsPayload: Codable, Sendable {
public var requestId: String?
public var results: [SearchResultItem]
}
public struct SearchErrorPayload: Codable, Sendable {
public var error: String
}
public struct SearchAnswerPayload: Codable, Sendable {
public var answerText: String?
public var answerRequestId: String?
public var answerCitations: [SearchCitation]?
}
public struct SearchDonePayload: Codable, Sendable {
public var search: SearchDetail
}
public enum SearchStreamEvent: Sendable {
case searchResults(SearchResultsPayload)
case searchError(SearchErrorPayload)
case answer(SearchAnswerPayload)
case answerError(SearchErrorPayload)
case done(SearchDonePayload)
case error(StreamErrorPayload)
case ignored
}
public struct ProviderModelInfo: Codable, Hashable, Sendable {
public var models: [String]
public var loadedAt: Date?
public var error: String?
}
public struct ModelCatalogResponse: Codable, Hashable, Sendable {
public var providers: [Provider: ProviderModelInfo]
enum CodingKeys: String, CodingKey {
case providers
}
public init(providers: [Provider: ProviderModelInfo]) {
self.providers = providers
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let rawProviders = try container.decode([String: ProviderModelInfo].self, forKey: .providers)
var mapped: [Provider: ProviderModelInfo] = [:]
for (key, value) in rawProviders {
if let provider = Provider(rawValue: key) {
mapped[provider] = value
}
}
self.providers = mapped
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let raw = Dictionary(uniqueKeysWithValues: providers.map { ($0.key.rawValue, $0.value) })
try container.encode(raw, forKey: .providers)
}
}
struct AuthSession: Codable {
var authenticated: Bool
var mode: String
}
struct ChatListResponse: Codable {
var chats: [ChatSummary]
}
struct SearchListResponse: Codable {
var searches: [SearchSummary]
}
struct ChatDetailResponse: Codable {
var chat: ChatDetail
}
struct SearchDetailResponse: Codable {
var search: SearchDetail
}
struct ChatCreateResponse: Codable {
var chat: ChatSummary
}
struct SearchCreateResponse: Codable {
var search: SearchSummary
}
struct DeleteResponse: Codable {
var deleted: Bool
}
struct SuggestTitleBody: Codable {
var chatId: String
var content: String
}
enum APIError: LocalizedError {
case invalidBaseURL
case httpError(statusCode: Int, message: String)
case networkError(message: String)
case decodingError(message: String)
case invalidResponse
case noResponseStream
var errorDescription: String? {
switch self {
case .invalidBaseURL:
return "Invalid API URL"
case let .httpError(_, message):
return message
case let .networkError(message):
return message
case let .decodingError(message):
return message
case .invalidResponse:
return "Unexpected server response"
case .noResponseStream:
return "No response stream"
}
}
}
extension DateFormatter {
static let sybilDisplayDate: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = .autoupdatingCurrent
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter
}()
static let sybilShortTime: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = .autoupdatingCurrent
formatter.dateStyle = .none
formatter.timeStyle = .short
return formatter
}()
}
extension Date {
var sybilRelativeLabel: String {
let formatter = RelativeDateTimeFormatter()
formatter.locale = .autoupdatingCurrent
formatter.unitsStyle = .abbreviated
return formatter.localizedString(for: self, relativeTo: Date())
}
var sybilShortTimeLabel: String {
DateFormatter.sybilShortTime.string(from: self)
}
}