Packages
API client library for any MediaWiki-based site, such as Wikipedia or Wikimedia Commons. Provides the Action API, realtime feed ingestion, and ORES scoring.
Current section
Files
Jump to
Current section
Files
lib/lift_wing.ex
defmodule Wiki.LiftWing do
@moduledoc """
This module provides an adapter for the [Lift Wing API](https://api.wikimedia.org/wiki/Lift_Wing_API)
for machine learning predictions.
See this page for a catalog of models, https://wikitech.wikimedia.org/wiki/Machine_Learning/LiftWing\#Current_Inference_Services
The server currently rate-limits requests to 50,000 per hour per IP address.
## Examples
```elixir
Wiki.LiftWing.new()
|> Wiki.LiftWing.request(wiki: "enwiki", model: "articlequality", rev_id: 12345)
# {:ok, %{
# "enwiki" => %{
# "models" => %{"articlequality" => %{"version" => "0.9.2"}},
# "scores" => %{
# "12345" => %{
# "articlequality" => %{
# "score" => %{
# "prediction" => "Stub",
# "probability" => %{
# "B" => 0.04109666150848109,
# "C" => 0.02060738177009099,
# "FA" => 0.0029400688910391592,
# "GA" => 0.004970401857774162,
# "Start" => 0.17362493306327959,
# "Stub" => 0.7567605529093352
# }
# }
# }
# }
# }
# }
# }}
```
Bad requests
```elixir
Wiki.LiftWing.new()
|> Wiki.LiftWing.request(wiki: "enwiki", model: "unknown", rev_id: 12345)
# {:error, %Wiki.Error{message: "Error received with HTTP status 404"}}
Wiki.LiftWing.new()
|> Wiki.LiftWing.request(wiki: "enwiki", model: "articlequality", rev_id: -1)
# {:error,
# %Wiki.Error{
# message: "The MW API does not have any info related to the rev-id provided as input (-1), therefore it is not possible to extract features properly. One possible cause is the deletion of the page related to the revision id. Please contact the ML-Team if you need more info."
# }}
```
"""
alias Wiki.{Error, Util}
@type client_option ::
{:adapter, module()}
| {:debug, true}
| {:endpoint, binary()}
| {:user_agent, binary()}
@typedoc """
- `:adapter` - Override the HTTP adapter
- `:debug` - Turn on verbose logging by setting to `true`
- `:endpoint` - Override the base URL to query
- `:user_agent` - Override the user-agent header string
"""
@type client_options :: [client_option()]
@default_adapter Tesla.Adapter.Hackney
@default_endpoint "https://api.wikimedia.org/service/lw/inference/v1/models/"
@doc """
Create a new Lift Wing client.
## Arguments
- `opts` - Configuration options that can change client behavior
## Return value
Returns an opaque client object, which should be passed to `request/2`.
"""
@spec new(client_options()) :: Tesla.Client.t()
def new(opts \\ []) do
endpoint = opts[:endpoint] || @default_endpoint
client(endpoint, opts)
end
@doc """
Make a Lift Wing request.
## Arguments
- `client` - Client object as returned by `new/1`.
- `params` - Keyword list of query parameters,
- `:wiki` - Wiki database name, eg. "enwiki"
- `:model` - Model name, eg. "articlequality"
- Remaining parameters are passed in the POST body, for example `:rev_id`
"""
@spec request(Tesla.Client.t(), keyword | map) :: {:ok, map} | {:error, any()}
def request(client, params) do
with {:ok, wiki} <- Keyword.fetch(params, :wiki),
{:ok, model} <- Keyword.fetch(params, :model),
body_params = Keyword.drop(params, [:wiki, :model]),
{:ok, response} <-
Tesla.post(
client,
"/#{wiki}-#{model}:predict",
Jason.encode!(Enum.into(body_params, %{}))
),
{:ok, result} <- validate(response) do
{:ok, result.body}
end
end
@doc """
Assertive variant of `request`.
"""
@spec request!(Tesla.Client.t(), keyword | map) :: map
def request!(client, params) do
case request(client, params) do
{:ok, result} -> result
{:error, error = %Error{}} -> raise error
{:error, error} -> raise %Error{message: "#{inspect(error)}"}
end
end
defp validate(result) do
with nil <- validate_body_type(result.body),
nil <- validate_api_errors(result.body),
nil <- validate_http_status(result.status) do
{:ok, result}
end
end
defp validate_http_status(status) do
case status do
status when status >= 200 and status < 300 -> nil
status -> {:error, %Error{message: "Error received with HTTP status #{status}"}}
end
end
defp validate_body_type(body) do
with body when is_map(body) <- body,
body when body != %{} <- body do
nil
else
_ -> {:error, %Error{message: "Empty response"}}
end
end
defp validate_api_errors(body) do
case body["error"] do
nil -> nil
error -> {:error, %Error{message: summarize_error(error)}}
end
end
defp summarize_error(error) when is_binary(error),
do: error
# TODO: learn more about these cases
defp summarize_error(_),
do: "unknown"
@spec client(binary(), client_options()) :: Tesla.Client.t()
defp client(url, opts) do
adapter = opts[:adapter] || @default_adapter
user_agent = opts[:user_agent] || Util.default_user_agent()
([
{Tesla.Middleware.BaseUrl, url},
{Tesla.Middleware.Headers,
[
{"user-agent", user_agent}
]},
Tesla.Middleware.FollowRedirects,
Tesla.Middleware.JSON
] ++
if(opts[:debug], do: [Tesla.Middleware.Logger], else: []))
|> Tesla.client(adapter)
end
end