-
# frozen_string_literal: true
-
-
1
require "dry/schema"
-
1
require "zeitwerk"
-
-
1
Dry::Schema.load_extensions :monads
-
-
1
Zeitwerk::Loader.new.then do |loader|
-
1
loader.inflector.inflect "api" => "API"
-
1
loader.tag = "kagi-api"
-
1
loader.push_dir "#{__dir__}/.."
-
1
loader.setup
-
end
-
-
1
module Kagi
-
# Main namespace.
-
1
module API
-
1
def self.loader registry = Zeitwerk::Registry
-
11
@loader ||= registry.loaders.each.find { |loader| loader.tag == "kagi-api" }
-
end
-
-
1
def self.new(&) = Client.new(&)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
# Provides the primary client for making API requests.
-
1
class Client
-
1
include Dependencies[:settings]
-
-
1
include Endpoints::Dependencies[
-
endpoint_enrich_news: "enrich.news",
-
endpoint_enrich_web: "enrich.web",
-
endpoint_fast: :fast,
-
endpoint_search: :search,
-
endpoint_summarize: :summarize
-
]
-
-
1
def initialize(**)
-
10
super
-
10
then: 2
else: 8
yield settings if block_given?
-
end
-
-
1
def enrich_news(**) = endpoint_enrich_news.call(**)
-
-
1
def enrich_web(**) = endpoint_enrich_web.call(**)
-
-
1
def fast(**) = endpoint_fast.call(**)
-
-
1
def search(**) = endpoint_search.call(**)
-
-
1
def summarize(**) = endpoint_summarize.call(**)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "inspectable"
-
-
1
module Kagi
-
1
module API
-
1
module Configuration
-
# Defines customizable API configuration content.
-
1
Content = Struct.new :content_type, :token, :uri do
-
1
include Inspectable[token: :redact]
-
-
1
def headers = {"Content-Type" => content_type}.compact
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "initable"
-
-
1
module Kagi
-
1
module API
-
1
module Configuration
-
# Loads configuration based on environment or falls back to defaults.
-
1
class Loader
-
1
include Initable[model: Content, environment: ENV]
-
-
1
def call
-
3
model[
-
content_type: environment.fetch("KAGI_API_CONTENT_TYPE", "application/json"),
-
token: environment["KAGI_API_TOKEN"],
-
uri: environment.fetch("KAGI_API_URI", "https://kagi.com/api/v0")
-
]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "cogger"
-
1
require "containable"
-
1
require "http"
-
-
1
module Kagi
-
1
module API
-
# Registers application dependencies.
-
1
module Container
-
1
extend Containable
-
-
2
register(:settings) { Configuration::Loader.new.call }
-
2
register(:requester) { Requester.new }
-
2
register(:logger) { Cogger.new id: "kagi-api", formatter: :json }
-
-
1
register :http do
-
1
HTTP.default_options = HTTP::Options.new features: {logging: {logger: self[:logger]}}
-
1
HTTP
-
end
-
-
1
namespace :contracts do
-
1
register :error, Contracts::Error
-
1
register :fast, Contracts::Fast
-
1
register :search, Contracts::Search
-
1
register :summary, Contracts::Summary
-
end
-
-
1
namespace :models do
-
1
register :error, Models::Error
-
1
register :fast, Models::Fast
-
1
register :search, Models::Search
-
1
register :summary, Models::Summary
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Contracts
-
# Validates error data.
-
1
Error = Dry::Schema.JSON do
-
1
required(:meta).hash Meta
-
-
1
required(:error).array(:hash) do
-
1
required(:code).filled :integer
-
1
required(:msg).filled :string
-
1
required(:ref).maybe :string
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Contracts
-
# Validates Fast GPT data.
-
1
Fast = Dry::Schema.JSON do
-
1
required(:meta).hash Meta
-
-
1
required(:data).hash do
-
1
required(:output).filled :string
-
1
required(:tokens).filled :integer
-
1
required(:references).array(Reference)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Contracts
-
# Validates meta data.
-
1
Meta = Dry::Schema.JSON do
-
1
required(:id).filled :string
-
1
required(:node).filled :string
-
1
required(:ms).filled :integer
-
2
optional(:api_balance) { filled? > int? | float? }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Contracts
-
# Validates reference data.
-
1
Reference = Dry::Schema.JSON do
-
1
required(:title).filled :string
-
1
required(:snippet).filled :string
-
1
required(:url).filled :string
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Contracts
-
# Validates search data.
-
1
Search = Dry::Schema.JSON do
-
1
required(:meta).hash Meta
-
-
1
required(:data).array(:hash) do
-
1
required(:t).filled :integer
-
1
optional(:rank).filled :integer
-
1
optional(:url).filled :string
-
1
optional(:title).filled :string
-
1
optional(:snippet).maybe :string
-
1
optional(:published).filled :time
-
-
1
optional(:thumbnail).hash do
-
1
required(:url).filled :string
-
1
optional(:width).filled :integer
-
1
optional(:height).filled :integer
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Contracts
-
# Validates summary data.
-
1
Summary = Dry::Schema.JSON do
-
1
required(:meta).hash Meta
-
-
1
required(:data).hash do
-
1
required(:output).filled :string
-
1
required(:tokens).filled :integer
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "infusible"
-
-
1
module Kagi
-
1
module API
-
1
Dependencies = Infusible[Container]
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "containable"
-
-
1
module Kagi
-
1
module API
-
1
module Endpoints
-
# Registers all endpoints.
-
1
module Container
-
1
extend Containable
-
-
1
namespace :enrich do
-
2
register(:news) { Enrich::News.new }
-
2
register(:web) { Enrich::Web.new }
-
end
-
-
2
register(:fast) { Fast.new }
-
2
register(:search) { Search.new }
-
2
register(:summarize) { Summarize.new }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "infusible"
-
-
1
module Kagi
-
1
module API
-
1
module Endpoints
-
1
Dependencies = Infusible[Container]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "inspectable"
-
1
require "pipeable"
-
-
1
module Kagi
-
1
module API
-
1
module Endpoints
-
1
module Enrich
-
# Handles news requests.
-
1
class News
-
1
include Kagi::API::Dependencies[
-
:requester,
-
contract: "contracts.search",
-
error_contract: "contracts.error",
-
model: "models.search",
-
error_model: "models.error"
-
]
-
-
1
include Dry::Monads[:result]
-
1
include Pipeable
-
1
include Inspectable[contract: :class, error_contract: :class]
-
-
1
def call(**params)
-
3
result = requester.get("enrich/news", **params)
-
-
3
in: 1
case result
-
1
in: 1
in Success then success result
-
1
else: 1
in Failure(response) then failure response
-
1
else Failure "Unable to parse HTTP response."
-
end
-
end
-
-
1
private
-
-
1
def success result
-
1
pipe result,
-
try(:parse, catch: JSON::ParserError),
-
validate(contract, as: :to_h),
-
to(model, :for)
-
end
-
-
1
def failure response
-
1
pipe(
-
response,
-
try(:parse, catch: JSON::ParserError),
-
validate(error_contract, as: :to_h),
-
to(error_model, :for),
-
1
bind { Failure it }
-
)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "inspectable"
-
1
require "pipeable"
-
-
1
module Kagi
-
1
module API
-
1
module Endpoints
-
1
module Enrich
-
# Handles web requests.
-
1
class Web
-
1
include Kagi::API::Dependencies[
-
:requester,
-
contract: "contracts.search",
-
error_contract: "contracts.error",
-
model: "models.search",
-
error_model: "models.error"
-
]
-
-
1
include Dry::Monads[:result]
-
1
include Pipeable
-
1
include Inspectable[contract: :class, error_contract: :class]
-
-
1
def call(**params)
-
3
result = requester.get("enrich/web", **params)
-
-
3
in: 1
case result
-
1
in: 1
in Success then success result
-
1
else: 1
in Failure(response) then failure response
-
1
else Failure "Unable to parse HTTP response."
-
end
-
end
-
-
1
private
-
-
1
def success result
-
1
pipe result,
-
try(:parse, catch: JSON::ParserError),
-
validate(contract, as: :to_h),
-
to(model, :for)
-
end
-
-
1
def failure response
-
1
pipe(
-
response,
-
try(:parse, catch: JSON::ParserError),
-
validate(error_contract, as: :to_h),
-
to(error_model, :for),
-
1
bind { Failure it }
-
)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "inspectable"
-
1
require "pipeable"
-
-
1
module Kagi
-
1
module API
-
1
module Endpoints
-
# Handles Fast GPT requests.
-
1
class Fast
-
1
include Kagi::API::Dependencies[
-
:requester,
-
contract: "contracts.fast",
-
error_contract: "contracts.error",
-
model: "models.fast",
-
error_model: "models.error"
-
]
-
-
1
include Dry::Monads[:result]
-
1
include Pipeable
-
1
include Inspectable[contract: :class, error_contract: :class]
-
-
1
def call(**params)
-
3
result = requester.post("fastgpt", **params)
-
-
3
in: 1
case result
-
1
in: 1
in Success then success result
-
1
else: 1
in Failure(response) then failure response
-
1
else Failure "Unable to parse HTTP response."
-
end
-
end
-
-
1
private
-
-
1
def success result
-
1
pipe result,
-
try(:parse, catch: JSON::ParserError),
-
validate(contract, as: :to_h),
-
to(model, :for)
-
end
-
-
1
def failure response
-
1
pipe(
-
response,
-
try(:parse, catch: JSON::ParserError),
-
validate(error_contract, as: :to_h),
-
to(error_model, :for),
-
1
bind { Failure it }
-
)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "inspectable"
-
1
require "pipeable"
-
-
1
module Kagi
-
1
module API
-
1
module Endpoints
-
# Handles search requests.
-
1
class Search
-
1
include Kagi::API::Dependencies[
-
:requester,
-
contract: "contracts.search",
-
error_contract: "contracts.error",
-
model: "models.search",
-
error_model: "models.error"
-
]
-
-
1
include Dry::Monads[:result]
-
1
include Pipeable
-
1
include Inspectable[contract: :class, error_contract: :class]
-
-
1
def call(**params)
-
3
result = requester.get("search", **params)
-
-
3
in: 1
case result
-
1
in: 1
in Success then success result
-
1
else: 1
in Failure(response) then failure response
-
1
else Failure "Unable to parse HTTP response."
-
end
-
end
-
-
1
private
-
-
1
def success result
-
1
pipe result,
-
try(:parse, catch: JSON::ParserError),
-
validate(contract, as: :to_h),
-
to(model, :for)
-
end
-
-
1
def failure response
-
1
pipe(
-
response,
-
try(:parse, catch: JSON::ParserError),
-
validate(error_contract, as: :to_h),
-
to(error_model, :for),
-
1
bind { Failure it }
-
)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "inspectable"
-
1
require "pipeable"
-
-
1
module Kagi
-
1
module API
-
1
module Endpoints
-
# Handles summarize requests.
-
1
class Summarize
-
1
include Kagi::API::Dependencies[
-
:requester,
-
contract: "contracts.summary",
-
error_contract: "contracts.error",
-
model: "models.summary",
-
error_model: "models.error"
-
]
-
-
1
include Dry::Monads[:result]
-
1
include Pipeable
-
1
include Inspectable[contract: :class, error_contract: :class]
-
-
1
def call(**params)
-
3
result = requester.post("summarize", **params)
-
-
3
in: 1
case result
-
1
in: 1
in Success then success result
-
1
else: 1
in Failure(response) then failure response
-
1
else Failure "Unable to parse HTTP response."
-
end
-
end
-
-
1
private
-
-
1
def success result
-
1
pipe result,
-
try(:parse, catch: JSON::ParserError),
-
validate(contract, as: :to_h),
-
to(model, :for)
-
end
-
-
1
def failure response
-
1
pipe(
-
response,
-
try(:parse, catch: JSON::ParserError),
-
validate(error_contract, as: :to_h),
-
to(error_model, :for),
-
1
bind { Failure it }
-
)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Models
-
1
module Content
-
1
ERROR_MAP = {msg: :message, ref: :reference}.freeze
-
-
# Models error data.
-
1
Error = Data.define :code, :message, :reference do
-
1
def self.for(key_map: ERROR_MAP, **attributes)
-
7
new(**attributes.transform_keys(key_map))
-
end
-
-
1
def initialize(reference: nil, **)
-
16
super
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Models
-
1
module Content
-
# Models fast data.
-
1
Fast = Data.define :output, :tokens, :references do
-
1
def self.for(**attributes)
-
6
new(**attributes, references: attributes[:references].map { Reference[**it] })
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Models
-
1
module Content
-
1
META_MAP = {ms: :duration, api_balance: :balance}.freeze
-
-
# Models meta data.
-
1
Meta = Data.define :id, :node, :duration, :balance do
-
1
def self.for(key_map: META_MAP, **attributes) = new(**attributes.transform_keys(key_map))
-
-
1
def initialize(balance: nil, **)
-
32
super
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Models
-
1
module Content
-
# Models reference data.
-
1
Reference = Data.define :title, :snippet, :url
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Models
-
1
module Content
-
1
SEARCH_MAP = {t: :type, published: :published_at}.freeze
-
-
# Models search data.
-
1
Search = Data.define :type, :rank, :title, :url, :snippet, :published_at, :thumbnail do
-
1
def self.for(key_map: SEARCH_MAP, **attributes)
-
6
new(
-
**attributes.transform_keys(key_map),
-
6
then: 1
else: 5
thumbnail: (Thumbnail[**attributes[:thumbnail]] if attributes.key? :thumbnail)
-
)
-
end
-
-
# rubocop:todo Metrics/ParameterLists
-
1
def initialize(
-
rank: nil,
-
title: nil,
-
url: nil,
-
snippet: nil,
-
published_at: nil,
-
thumbnail: nil,
-
**attributes
-
)
-
14
super
-
end
-
# rubocop:enable Metrics/ParameterLists
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Models
-
1
module Content
-
# Models summary data.
-
1
Summary = Data.define :output, :tokens
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Models
-
1
module Content
-
# Models thumbnail data.
-
1
Thumbnail = Data.define :url, :width, :height do
-
1
def initialize url:, width: nil, height: nil
-
4
super
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Models
-
# Models the API error.
-
1
Error = Data.define :meta, :error do
-
1
def self.for(**attributes)
-
6
new(
-
**attributes.merge!(
-
meta: Content::Meta.for(**attributes[:meta]),
-
6
error: attributes[:error].map { Content::Error.for(**it) }
-
)
-
)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Models
-
# Models the fast payload.
-
1
Fast = Data.define :meta, :data do
-
1
def self.for(**attributes)
-
2
new(
-
**attributes.merge!(
-
meta: Content::Meta.for(**attributes[:meta]),
-
data: Content::Fast.for(**attributes[:data])
-
)
-
)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Models
-
# Models the search payload.
-
1
Search = Data.define :meta, :data do
-
1
def self.for(**attributes)
-
4
new(
-
**attributes.merge!(
-
meta: Content::Meta.for(**attributes[:meta]),
-
5
data: attributes[:data].map { Content::Search.for(**it) }
-
)
-
)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
1
module Models
-
# Models the API payload.
-
1
Summary = Data.define :meta, :data do
-
1
def self.for(**attributes)
-
2
new(
-
**attributes.merge!(
-
meta: Content::Meta.for(**attributes[:meta]),
-
data: Content::Summary[**attributes[:data]]
-
)
-
)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Kagi
-
1
module API
-
# The low-level object for making basic HTTP requests.
-
1
class Requester
-
1
include Dependencies[:settings, :http]
-
1
include Dry::Monads[:result]
-
-
1
def initialize(**)
-
4
super
-
4
then: 1
else: 3
yield settings if block_given?
-
end
-
-
1
def get(path, **params) = call(__method__, path, params:)
-
-
1
def post(path, **json) = call(__method__, path, json:)
-
-
1
private
-
-
1
attr_reader :settings
-
-
1
def call verb, path, **options
-
3
http.auth("Bot #{settings.token}")
-
.headers(settings.headers)
-
.public_send(verb, "#{settings.uri}/#{path}", options)
-
3
then: 2
else: 1
.then { |response| response.status.success? ? Success(response) : Failure(response) }
-
end
-
end
-
end
-
end