QGMGDD3MPCHENK66ZX3M3KMIGP7C6ZVXTAEABMP25UARSEJQL5LQC
// Copyright © 2024 Ryan Booker. All rights reserved.
import ArgumentParser
import Foundation
extension URL: ExpressibleByArgument {
public init?(argument: String) {
self.init(string: argument)
}
public var defaultValueDescription: String {
absoluteString
}
}
// Copyright © 2024 Ryan Booker. All rights reserved.
import ArgumentParser
import Foundation
extension Kagi {
struct Summarize: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "summarize",
abstract: "Summarize something.",
usage: "kg summarize \"Some text or a url to summarize.\""
)
@Argument(help: "Some text or a url to summarize") var query: Either<URL, String>
@Option(help: Engine.help) var engine: Engine?
@Option(help: SummaryType.help) var summaryType: SummaryType?
mutating func run() async throws {
do {
let response = try await process(query, engine: engine, summaryType: summaryType)
print(response)
} catch {
throw ValidationError(error.localizedDescription)
}
}
}
}
// MARK: - Processing
extension Kagi.Summarize {
func process(_ query: Either<URL, String>, engine: Engine?, summaryType: SummaryType?) async throws -> Kagi.Response<Summarization> {
var request = try Kagi.Endpoint.summarize.authorizedRequest
var body: [String: Any] = query.reduce(success: { ["url": $0.absoluteString] }, failure: { ["text": $0] })
if let engine {
body["engine"] = engine.rawValue
}
if let summaryType {
body["summary_type"] = summaryType.rawValue
}
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await URLSession.shared.data(for: request)
do {
return try JSONDecoder().decode(Kagi.Response.self, from: data)
} catch {
#if DEBUG
print(response)
print(error)
print("-----")
#endif
throw error
}
}
}
// MARK: - Response
extension Kagi.Summarize {
enum Engine: String, CaseIterable, CustomStringConvertible, ExpressibleByArgument {
case cecil
case agnes
case daphne
case muriel
var description: String {
switch self {
case .cecil:
"Friendly, descriptive, fast summary (default)"
case .agnes:
"Formal, technical, analytical summary"
case .daphne:
"Informal, creative, friendly summary"
case .muriel:
"Best-in-class summary using our enterprise-grade model"
}
}
static var help: ArgumentHelp? {
.init(
allCases.map { "- \($0.rawValue): \($0.description)" }.joined(separator: "\n") + "\n"
)
}
var defaultValueDescription: String {
Self.cecil.rawValue
}
}
enum SummaryType: String, CaseIterable, CustomStringConvertible, ExpressibleByArgument {
case summary
case takeaway
var description: String {
switch self {
case .summary:
"Paragraph(s) of summary prose (default)"
case .takeaway:
"Bulleted list of key points"
}
}
static var help: ArgumentHelp? {
.init(
allCases.map { "- \($0.rawValue): \($0.description)" }.joined(separator: "\n") + "\n"
)
}
var defaultValueDescription: String {
Self.summary.rawValue
}
}
struct Summarization: CustomStringConvertible, Decodable {
var output: String
var tokens: Int
var description: String {
"""
Summary:
\(output)
"""
}
}
}
// Copyright © 2024 Ryan Booker. All rights reserved.
import Foundation
extension Result {
func reduce<T>(
success successResult: (Success) -> T,
failure failureResult: (Failure) -> T
) -> T {
switch self {
case let .success(value):
successResult(value)
case let .failure(error):
failureResult(error)
}
}
static func <|> <T, E>(lhs: Result<T, E>, rhs: Result<T, E>) -> Result<T, E> {
lhs.flatMapError { _ in
rhs
}
}
}
infix operator <|> : NilCoalescingPrecedence
// Copyright © 2024 Ryan Booker. All rights reserved.
import Foundation
extension Kagi {
struct Response<T>: CustomStringConvertible, Decodable where T: CustomStringConvertible & Decodable {
let meta: Metadata
let data: Result<T, Failure>
var description: String {
data.reduce(success: \.description, failure: \.description)
}
enum CodingKeys: CodingKey {
case meta, data, error
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.meta = try container.decode(Metadata.self, forKey: .meta)
do {
let data = try container.decode(T.self, forKey: .data)
self.data = .success(data)
} catch {
let reasons = try container.decode([Failure.Reason].self, forKey: .error)
self.data = .failure(.init(reasons: reasons))
}
}
}
}
// MARK: - Repsonse types
extension Kagi.Response {
struct Failure: CustomStringConvertible, Decodable, Error {
struct Reason: CustomStringConvertible, Decodable {
let code: Int
let msg: String
var description: String {
"\(code): \(msg)"
}
}
let reasons: [Reason]
var description: String {
"""
Whoops!
\(reasons.map(\.description).joined(separator: "\n\n"))
"""
}
}
struct Metadata: Decodable {
let id: String
let node: String
let ms: Int
}
}
// The Swift Programming Language
// https://docs.swift.org/swift-book
//
// Swift Argument Parser
// https://swiftpackageindex.com/apple/swift-argument-parser/documentation
import ArgumentParser
import Foundation
@main
struct Kagi: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "kg",
abstract: "Interact with Kagi services.",
subcommands: [FastGPT.self, Summarize.self]
)
}
// Copyright © 2024 Ryan Booker. All rights reserved.
import ArgumentParser
import Foundation
extension Kagi {
struct FastGPT: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "fastgpt",
abstract: "Ask FastGPT something.",
usage: "kg fastgpt \"What is the answer to life, the universe, and everything?\""
)
@Argument(help: "A query for FastGPT") var query: String
mutating func run() async throws {
do {
let response = try await process(query)
print(response)
} catch {
throw ValidationError(error.localizedDescription)
}
}
}
}
// MARK: - Processing
extension Kagi.FastGPT {
func process(_ query: String) async throws -> Kagi.Response<Answer> {
var request = try Kagi.Endpoint.fastGPT.authorizedRequest
request.httpBody = try JSONSerialization.data(withJSONObject: ["query": query])
let (data, response) = try await URLSession.shared.data(for: request)
do {
return try JSONDecoder().decode(Kagi.Response.self, from: data)
} catch {
#if DEBUG
print(response)
print(error)
print("-----")
#endif
throw error
}
}
}
// MARK: - Response
extension Kagi.FastGPT {
struct Answer: CustomStringConvertible, Decodable {
struct Reference: CustomStringConvertible, Decodable {
var title: String
var snippet: String
var url: URL
var description: String {
"""
- \(title)
\(url)
\(snippet.trimmingCharacters(in: .whitespaces))
"""
}
}
var output: String
var tokens: Int
var references: [Reference]
var description: String {
"""
Answer:
\(output)
References:
\(references.map(\.description).joined(separator: "\n\n"))
"""
}
}
}
// Copyright © 2024 Ryan Booker. All rights reserved.
import ArgumentParser
import Foundation
extension Kagi {
enum Endpoint: String {
static let baseUrl = URL(string: "https://kagi.com/api/v0/")!
case fastGPT
case summarize
var url: URL {
URL(string: rawValue.lowercased(), relativeTo: Self.baseUrl)!
}
var authorizedRequest: URLRequest {
get throws {
guard let apiToken = ProcessInfo.processInfo.environment["FASTGPT_API_TOKEN"] else {
throw ValidationError("API key is missing. Set the environment variable FASTGPT_API_TOKEN.")
}
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bot \(apiToken)", forHTTPHeaderField: "Authorization")
request.httpMethod = "POST"
return request
}
}
}
}
// Copyright © 2024 Ryan Booker. All rights reserved.
import ArgumentParser
import Foundation
enum Either<First, Second> {
case first(First)
case second(Second)
func reduce<T>(
success firstResult: (First) -> T,
failure secondResult: (Second) -> T
) -> T {
switch self {
case let .first(value):
firstResult(value)
case let .second(error):
secondResult(error)
}
}
}
extension Either: ExpressibleByArgument
where First: ExpressibleByArgument, Second: ExpressibleByArgument {
init?(argument: String) {
if let value = First(argument: argument) {
self = .first(value)
} else if let value = Second(argument: argument) {
self = .second(value)
} else {
return nil
}
}
}
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Kagi",
platforms: [
.macOS(.v12),
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.executableTarget(
name: "kg",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]
),
]
)
{
"pins" : [
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41",
"version" : "1.3.0"
}
}
],
"version" : 2
}
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1520"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "kg"
BuildableName = "kg"
BlueprintName = "kg"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "kg"
BuildableName = "kg"
BlueprintName = "kg"
ReferencedContainer = "container:">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "summarize https://hypercritical.co/2024/01/11/i-made-this --summary-type takeaway"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "help summarize"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "fastgpt "What is the answer to life, the universe, and everything?""
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "help fastgpt"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "kg"
BuildableName = "kg"
BlueprintName = "kg"
ReferencedContainer = "container:">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>