Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement external-engine endpoints #66

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ Most of the API is available:
client.external_engine.update
client.external_engine.delete

client.external_engine_analysis.analyse
client.external_engine_analysis.acquire_request
client.external_engine_analysis.answer_request

client.games.export
client.games.export_ongoing_by_player
client.games.export_by_player
Expand Down
7 changes: 7 additions & 0 deletions berserk/clients/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .analysis import Analysis
from .base import BaseClient
from .account import Account
from .external_engine_analysis import ExternalEngineAnalysis
from .users import Users
from .relations import Relations
from .teams import Teams
Expand Down Expand Up @@ -35,6 +36,7 @@
"Challenges",
"Client",
"ExternalEngine",
"ExternalEngineAnalysis",
"Games",
"Messaging",
"OAuth",
Expand Down Expand Up @@ -73,13 +75,16 @@ class Client(BaseClient):
- :class:`tablebase <berserk.clients.Tablebase>` - lookup endgame tablebase
- :class:`bulk_pairings <berserk.clients.BulkPairing>` - manage bulk pairings
- :class: `external_engine <berserk.clients.ExternalEngine>` - manage external engines
- :class: `external_engine_analysis <berserk.clients.ExternalEngineAnalysis>` - manage external engine analysis

:param session: request session, authenticated as needed
:param base_url: base API URL to use (if other than the default)
:param pgn_as_default: ``True`` if PGN should be the default format for game exports
when possible. This defaults to ``False`` and is used as a fallback when
``as_pgn`` is left as ``None`` for methods that support it.
:param tablebase_url: URL for tablebase lookups
:param explorer_url: URL for opening explorer
:param engine_url: URL for external engine analysis
"""

def __init__(
Expand All @@ -90,6 +95,7 @@ def __init__(
*,
tablebase_url: str | None = None,
explorer_url: str | None = None,
engine_url: str | None = None,
):
session = session or requests.Session()
super().__init__(session, base_url)
Expand All @@ -114,3 +120,4 @@ def __init__(
self.opening_explorer = OpeningExplorer(session, explorer_url)
self.bulk_pairings = BulkPairings(session, base_url)
self.external_engine = ExternalEngine(session, base_url)
self.external_engine_analysis = ExternalEngineAnalysis(session, engine_url)
87 changes: 87 additions & 0 deletions berserk/clients/external_engine_analysis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from typing import List, Iterator, cast, Optional

import requests

from .base import BaseClient
from ..formats import NDJSON, TEXT
from ..types.external_engine import ExternalEngineRequest, EngineAnalysisOutput

ENGINE_URL = "https://engine.lichess.ovh"


class ExternalEngineAnalysis(BaseClient):
"""Client for external engine analysis related endpoints."""

def __init__(self, session: requests.Session, engine_url: Optional[str] = None):
super().__init__(session, engine_url or ENGINE_URL)

def analyse(
self,
engine_id: str,
client_secret: str,
session_id: str,
threads: int,
hash_table_size: int,
num_variations: int,
variant: str,
initial_fen: str,
moves: List[str],
infinite: Optional[bool] = None,
) -> Iterator[EngineAnalysisOutput]:
"""Request analysis from an external engine.

Response content is streamed as newline delimited JSON. The properties are based on the UCI specification.
Analysis stops when the client goes away, the requested limit is reached, or the provider goes away.

:param engine_id: engine ID
:param client_secret: engine credentials
:param session_id: arbitrary string that identifies the analysis session
:param threads: number of threads to use for analysis
:param hash_table_size: hash table size to use for analysis, in MiB
:param num_variations: requested number of principle variation
:param variant: chess UCI variant
:param initial_fen: initial position of the game
:param moves: list of moves played from the initial position, in UCI notation
:param infinite: request an infinite search (rather than roughly aiming for defaultDepth)
:return: iterator over the request analysis from the external engine
"""
path = f"/api/external-engine/{engine_id}/analyse"
payload = {
"clientSecret": client_secret,
"work": {
"sessionId": session_id,
"threads": threads,
"hash": hash_table_size,
"infinite": infinite,
"multiPv": num_variations,
"variant": variant,
"initialFen": initial_fen,
"moves": moves,
},
}
for response in self._r.post(
path=path, payload=payload, stream=True, fmt=NDJSON
):
yield cast(EngineAnalysisOutput, response)

def acquire_request(self, provider_secret: str) -> ExternalEngineRequest:
"""Wait for an analysis request to any of the external engines that have been registered with the given secret.

:param provider_secret: provider credentials
:return: the requested analysis
"""
path = "/api/external-engine/work"
payload = {"providerSecret": provider_secret}
return cast(ExternalEngineRequest, self._r.post(path=path, payload=payload))

def answer_request(self, engine_id: str) -> str:
"""Submit a stream of analysis as UCI output.

The server may close the connection at any time, indicating that the requester has gone away and analysis
should be stopped.

:param engine_id: engine ID
:return: the requested analysis
"""
path = f"/api/external-engine/work/{engine_id}"
return self._r.post(path=path, fmt=TEXT)
4 changes: 3 additions & 1 deletion berserk/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from .account import AccountInformation, Perf, Preferences, Profile, StreamerInfo
from .bulk_pairings import BulkPairing, BulkPairingGame
from .challenges import Challenge
from .common import ClockConfig, ExternalEngine, LightUser, OnlineLightUser, Variant
from .common import ClockConfig, LightUser, OnlineLightUser, Variant
from .external_engine import ExternalEngine, EngineAnalysisOutput
from .puzzles import PuzzleRace
from .opening_explorer import (
OpeningExplorerRating,
Expand All @@ -21,6 +22,7 @@
"Challenge",
"ClockConfig",
"CurrentTournaments",
"EngineAnalysisOutput",
"ExternalEngine",
"LightUser",
"OnlineLightUser",
Expand Down
21 changes: 0 additions & 21 deletions berserk/types/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,6 @@ class ClockConfig(TypedDict):
increment: int


class ExternalEngine(TypedDict):
# Engine ID
id: str
# Engine display name
name: str
# Secret token that can be used to request analysis
clientSecret: str
# User this engine has been registered for
userId: str
# Max number of available threads
maxThreads: int
# Max available hash table size, in MiB
maxHash: int
# Estimated depth of normal search
defaultDepth: int
# List of supported chess variants
variants: str
# Arbitrary data that engine provider can use for identification or bookkeeping
providerData: NotRequired[str]


Color: TypeAlias = Literal["white", "black"]

GameType: TypeAlias = Literal[
Expand Down
71 changes: 71 additions & 0 deletions berserk/types/external_engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from typing import List

from typing_extensions import TypedDict, NotRequired


class ExternalEngine(TypedDict):
# Engine ID
id: str
# Engine display name
name: str
# Secret token that can be used to request analysis
clientSecret: str
# User this engine has been registered for
userId: str
# Max number of available threads
maxThreads: int
# Max available hash table size, in MiB
maxHash: int
# Estimated depth of normal search
defaultDepth: int
# List of supported chess variants
variants: str
# Arbitrary data that engine provider can use for identification or bookkeeping
providerData: NotRequired[str]


class ExternalEngineWork(TypedDict):
# Arbitrary string that identifies the analysis session. Providers may clear the hash table between sessions
sessionId: str
# Number of threads to use for analysis
threads: int
# Hash table size to use for analysis, in MiB
hash: int
# Requested number of principle variations
multiPv: List[int]
# Uci variant
variant: str
# Initial position of the game
initialFen: str
# List of moves played from the initial position, in UCI notation
moves: List[str]
# Request an infinite search (rather than roughly aiming for defaultDepth)
infinite: NotRequired[bool]


class ExternalEngineRequest(TypedDict):
id: str
work: ExternalEngineWork
engine: ExternalEngine


class PrincipleVariationAnalysis(TypedDict):
# Current search depth of the pv
depth: int
# Variation in UCI notation
moves: List[str]
# Evaluation in centi-pawns, from White's point of view
cp: NotRequired[int]
# Evaluation in signed moves to mate, from White's point of view
mate: NotRequired[int]


class EngineAnalysisOutput(TypedDict):
# Number of milliseconds the search has been going on
time: int
# Current search depth
depth: int
# Number of nodes visited so far
nodes: int
# Information about up to 5 pvs, with the primary pv at index 0
pvs: List[PrincipleVariationAnalysis]