From f3efce8a4842e132a789f2dfdfc2f9db47f50743 Mon Sep 17 00:00:00 2001 From: Anupya Pamidimukkala Date: Fri, 3 Nov 2023 17:37:24 -0400 Subject: [PATCH 1/6] add external engine analysis endpoints --- README.rst | 4 + berserk/clients/__init__.py | 5 ++ berserk/clients/external_engine_analysis.py | 86 +++++++++++++++++++++ berserk/types/__init__.py | 4 +- berserk/types/common.py | 21 ----- berserk/types/external_engine.py | 71 +++++++++++++++++ 6 files changed, 169 insertions(+), 22 deletions(-) create mode 100644 berserk/clients/external_engine_analysis.py create mode 100644 berserk/types/external_engine.py diff --git a/README.rst b/README.rst index 1d4f910..65003cb 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/berserk/clients/__init__.py b/berserk/clients/__init__.py index e7c83ac..32384f4 100644 --- a/berserk/clients/__init__.py +++ b/berserk/clients/__init__.py @@ -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 @@ -35,6 +36,7 @@ "Challenges", "Client", "ExternalEngine", + "ExternalEngineAnalysis", "Games", "Messaging", "OAuth", @@ -73,6 +75,7 @@ class Client(BaseClient): - :class:`tablebase ` - lookup endgame tablebase - :class:`bulk_pairings ` - manage bulk pairings - :class: `external_engine ` - manage external engines + - :class: `external_engine_analysis ` - manage external engine analysis :param session: request session, authenticated as needed :param base_url: base API URL to use (if other than the default) @@ -90,6 +93,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) @@ -114,3 +118,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) diff --git a/berserk/clients/external_engine_analysis.py b/berserk/clients/external_engine_analysis.py new file mode 100644 index 0000000..33edab8 --- /dev/null +++ b/berserk/clients/external_engine_analysis.py @@ -0,0 +1,86 @@ +from typing import List, Iterator, cast + +import requests + +from berserk import NDJSON +from berserk.clients import BaseClient +from berserk.formats import TEXT +from berserk.types import EngineAnalysisOutput +from berserk.types.external_engine import ExternalEngineRequest + +ENGINE_URL = "https://engine.lichess.ovh" + + +class ExternalEngineAnalysis(BaseClient): + """Client for external engine analysis related endpoints.""" + + def __init__(self, session: requests.Session, engine_url: str | None = 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: bool | None = 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, + }, + } + yield from self._r.post(path=path, payload=payload, stream=True, fmt=NDJSON) + + 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) diff --git a/berserk/types/__init__.py b/berserk/types/__init__.py index 46a132f..a7bf12c 100644 --- a/berserk/types/__init__.py +++ b/berserk/types/__init__.py @@ -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, @@ -21,6 +22,7 @@ "Challenge", "ClockConfig", "CurrentTournaments", + "EngineAnalysisOutput", "ExternalEngine", "LightUser", "OnlineLightUser", diff --git a/berserk/types/common.py b/berserk/types/common.py index 0ad2b95..4d87eab 100644 --- a/berserk/types/common.py +++ b/berserk/types/common.py @@ -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[ diff --git a/berserk/types/external_engine.py b/berserk/types/external_engine.py new file mode 100644 index 0000000..b9428ab --- /dev/null +++ b/berserk/types/external_engine.py @@ -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] From 8eaffa5940a7ae15dabd13c0145b71e3090b5854 Mon Sep 17 00:00:00 2001 From: Anupya Pamidimukkala Date: Fri, 3 Nov 2023 18:03:21 -0400 Subject: [PATCH 2/6] fix import issue --- berserk/clients/external_engine_analysis.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/berserk/clients/external_engine_analysis.py b/berserk/clients/external_engine_analysis.py index 33edab8..81d16c1 100644 --- a/berserk/clients/external_engine_analysis.py +++ b/berserk/clients/external_engine_analysis.py @@ -2,9 +2,8 @@ import requests -from berserk import NDJSON -from berserk.clients import BaseClient -from berserk.formats import TEXT +from .base import BaseClient +from berserk.formats import NDJSON, TEXT from berserk.types import EngineAnalysisOutput from berserk.types.external_engine import ExternalEngineRequest From a2a625efb902c8ee9acee062c13e4c4a522de651 Mon Sep 17 00:00:00 2001 From: Anupya Pamidimukkala Date: Tue, 7 Nov 2023 15:18:00 -0500 Subject: [PATCH 3/6] some improvements --- berserk/clients/__init__.py | 2 ++ berserk/clients/external_engine_analysis.py | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/berserk/clients/__init__.py b/berserk/clients/__init__.py index 32384f4..eddd311 100644 --- a/berserk/clients/__init__.py +++ b/berserk/clients/__init__.py @@ -83,6 +83,8 @@ class Client(BaseClient): 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__( diff --git a/berserk/clients/external_engine_analysis.py b/berserk/clients/external_engine_analysis.py index 81d16c1..5baac68 100644 --- a/berserk/clients/external_engine_analysis.py +++ b/berserk/clients/external_engine_analysis.py @@ -3,9 +3,8 @@ import requests from .base import BaseClient -from berserk.formats import NDJSON, TEXT -from berserk.types import EngineAnalysisOutput -from berserk.types.external_engine import ExternalEngineRequest +from ..formats import NDJSON, TEXT +from ..types.external_engine import ExternalEngineRequest, EngineAnalysisOutput ENGINE_URL = "https://engine.lichess.ovh" From 0c5eda920d836d468d1e59dafadc025e36d07dc2 Mon Sep 17 00:00:00 2001 From: Anupya Pamidimukkala Date: Tue, 7 Nov 2023 15:23:22 -0500 Subject: [PATCH 4/6] use Optional instead of | --- berserk/clients/external_engine_analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/berserk/clients/external_engine_analysis.py b/berserk/clients/external_engine_analysis.py index 5baac68..20760a1 100644 --- a/berserk/clients/external_engine_analysis.py +++ b/berserk/clients/external_engine_analysis.py @@ -1,4 +1,4 @@ -from typing import List, Iterator, cast +from typing import List, Iterator, cast, Optional import requests @@ -12,7 +12,7 @@ class ExternalEngineAnalysis(BaseClient): """Client for external engine analysis related endpoints.""" - def __init__(self, session: requests.Session, engine_url: str | None = None): + def __init__(self, session: requests.Session, engine_url: Optional[str] = None): super().__init__(session, engine_url or ENGINE_URL) def analyse( From 1183ef3242a6782bd6a7e87b7cd12a7a06d371fe Mon Sep 17 00:00:00 2001 From: Anupya Pamidimukkala Date: Tue, 7 Nov 2023 15:25:04 -0500 Subject: [PATCH 5/6] use Optional for infinite --- berserk/clients/external_engine_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/berserk/clients/external_engine_analysis.py b/berserk/clients/external_engine_analysis.py index 20760a1..ce396a5 100644 --- a/berserk/clients/external_engine_analysis.py +++ b/berserk/clients/external_engine_analysis.py @@ -26,7 +26,7 @@ def analyse( variant: str, initial_fen: str, moves: List[str], - infinite: bool | None = None, + infinite: Optional[bool] = None, ) -> Iterator[EngineAnalysisOutput]: """Request analysis from an external engine. From 68d08e85ef32d8370a9edf0b9fe9306aad09ec20 Mon Sep 17 00:00:00 2001 From: Anupya Pamidimukkala Date: Tue, 7 Nov 2023 15:29:08 -0500 Subject: [PATCH 6/6] fix pyright error --- berserk/clients/external_engine_analysis.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/berserk/clients/external_engine_analysis.py b/berserk/clients/external_engine_analysis.py index ce396a5..8eae1d3 100644 --- a/berserk/clients/external_engine_analysis.py +++ b/berserk/clients/external_engine_analysis.py @@ -59,7 +59,10 @@ def analyse( "moves": moves, }, } - yield from self._r.post(path=path, payload=payload, stream=True, fmt=NDJSON) + 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.