Skip to content

Commit f14e94c

Browse files
authored
Merge pull request lichess-org#81 from FriedrichtenHagen/add_external_engine_analysis_post_endpoints
Add external engine analysis post endpoints
2 parents d908722 + 9739c29 commit f14e94c

File tree

7 files changed

+196
-24
lines changed

7 files changed

+196
-24
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ To be released
1919
``client.studies.get_by_user`` to get the metadata of all studies
2020
of a given user.
2121
* Added ``bots.handle_draw_offer`` and ``bots.handle_takeback_offer`` to handle draw and takeback offers
22+
* Added ``client.external_engine.analyse``, ``client.external_engine.acquire_request``, ``client.external_engine.answer_request`` to handle analysis with an external engine
23+
2224

2325
Thanks to all the contributors who helped to this release:
2426
- @hsheth2

README.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ Most of the API is available:
148148
client.external_engine.create
149149
client.external_engine.update
150150
client.external_engine.delete
151+
client.external_engine.analyse
152+
client.external_engine.acquire_request
153+
client.external_engine.answer_request
151154
152155
client.fide.get_player
153156
client.fide.search_players

berserk/clients/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ def __init__(
9393
*,
9494
tablebase_url: str | None = None,
9595
explorer_url: str | None = None,
96+
external_engine_url: str | None = None,
9697
):
9798
session = session or requests.Session()
9899
super().__init__(session, base_url)
@@ -116,5 +117,7 @@ def __init__(
116117
self.tablebase = Tablebase(session, tablebase_url)
117118
self.opening_explorer = OpeningExplorer(session, explorer_url)
118119
self.bulk_pairings = BulkPairings(session, base_url)
119-
self.external_engine = ExternalEngine(session, base_url)
120+
self.external_engine = ExternalEngine(
121+
session, base_url=base_url, external_engine_url=external_engine_url
122+
)
120123
self.fide = Fide(session)

berserk/clients/external_engine.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
11
from __future__ import annotations
22

3-
from typing import List, cast
3+
from typing import List, cast, Iterator
44

55
from .base import BaseClient
6+
import requests
7+
from ..formats import NDJSON, TEXT
8+
from ..types.common import UciVariant
9+
from ..types.external_engine import ExternalEngineRequest, EngineAnalysisOutput
10+
11+
EXTERNAL_ENGINE_URL = "https://engine.lichess.ovh"
612

713

814
class ExternalEngine(BaseClient):
915
"""Client for external engine related endpoints."""
1016

17+
def __init__(
18+
self,
19+
session: requests.Session,
20+
*,
21+
base_url: str | None = None,
22+
external_engine_url: str | None = None,
23+
):
24+
"""Create a subclient for the endpoints that use a different base url."""
25+
super().__init__(session, base_url)
26+
self._external_client = BaseClient(
27+
session, external_engine_url or EXTERNAL_ENGINE_URL
28+
)
29+
1130
def get(self) -> List[ExternalEngine]:
1231
"""Lists all external engines that have been registered for the user, and the credentials required to use them.
1332
@@ -112,3 +131,85 @@ def delete(self, engine_id: str) -> None:
112131
"""
113132
path = f"/api/external-engine/{engine_id}"
114133
self._r.request("DELETE", path)
134+
135+
def analyse(
136+
self,
137+
engine_id: str,
138+
client_secret: str,
139+
session_id: str,
140+
threads: int,
141+
hash_table_size: int,
142+
pri_num_variations: int,
143+
variant: UciVariant,
144+
initial_fen: str,
145+
moves: List[str],
146+
movetime: int | None = None,
147+
depth: int | None = None,
148+
nodes: int | None = None,
149+
) -> Iterator[EngineAnalysisOutput]:
150+
"""
151+
Analyse with external engine
152+
153+
Request analysis from an external engine. Response content is streamed as newline delimited JSON.
154+
The properties are based on the UCI specification.
155+
Analysis stops when the client goes away, the requested limit is reached, or the provider goes away.
156+
157+
:param engine_id: external engine id
158+
:param client_secret: engine credentials
159+
:param session_id: Arbitary string that identifies the analysis session. Providers may wish to clear the hash table between sessions.
160+
:param threads: Number of threads to use for analysis.
161+
:param hash_table_size: Hash table size to use for analysis, in MiB.
162+
:param pri_num_variations: Requested number of principal variations. (1-5)
163+
:param variant: uci variant
164+
:param initial_fen: Initial position of the game.
165+
:param moves: List of moves played from the initial position, in UCI notation.
166+
:param movetime: Amount of time to analyse the position, in milliseconds.
167+
:param depth: Analysis target depth
168+
:param nodes: Number of nodes to analyse in the position
169+
"""
170+
path = f"/api/external-engine/{engine_id}/analyse"
171+
payload = {
172+
"clientSecret": client_secret,
173+
"work": {
174+
"sessionId": session_id,
175+
"threads": threads,
176+
"hash": hash_table_size,
177+
"multiPv": pri_num_variations,
178+
"variant": variant,
179+
"initialFen": initial_fen,
180+
"moves": moves,
181+
"movetime": movetime,
182+
"depth": depth,
183+
"nodes": nodes,
184+
},
185+
}
186+
187+
for response in self._external_client._r.post(
188+
path=path,
189+
payload=payload,
190+
stream=True,
191+
fmt=NDJSON,
192+
):
193+
yield cast(EngineAnalysisOutput, response)
194+
195+
def acquire_request(self, provider_secret: str) -> ExternalEngineRequest:
196+
"""Wait for an analysis request to any of the external engines that have been registered with the given secret.
197+
:param provider_secret: provider credentials
198+
:return: the requested analysis
199+
"""
200+
path = "/api/external-engine/work"
201+
payload = {"providerSecret": provider_secret}
202+
return cast(
203+
ExternalEngineRequest,
204+
self._external_client._r.post(path=path, payload=payload),
205+
)
206+
207+
def answer_request(self, engine_id: str) -> str:
208+
"""Submit a stream of analysis as UCI output.
209+
The server may close the connection at any time, indicating that the requester has gone away and analysis
210+
should be stopped.
211+
:param engine_id: engine ID
212+
:return: the requested analysis
213+
"""
214+
path = f"/api/external-engine/work/{engine_id}"
215+
return self._external_client._r.post(path=path, fmt=TEXT)

berserk/types/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
)
1818

1919
from .bulk_pairings import BulkPairing, BulkPairingGame
20+
from .external_engine import ExternalEngine
2021
from .challenges import ChallengeJson
21-
from .common import ClockConfig, ExternalEngine, LightUser, OnlineLightUser, VariantKey
22+
from .common import ClockConfig, LightUser, OnlineLightUser, VariantKey
2223
from .fide import FidePlayer
2324
from .puzzles import PuzzleData, PuzzleRace
2425
from .opening_explorer import (

berserk/types/common.py

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,6 @@ class ClockConfig(TypedDict):
1212
increment: int
1313

1414

15-
class ExternalEngine(TypedDict):
16-
# Engine ID
17-
id: str
18-
# Engine display name
19-
name: str
20-
# Secret token that can be used to request analysis
21-
clientSecret: str
22-
# User this engine has been registered for
23-
userId: str
24-
# Max number of available threads
25-
maxThreads: int
26-
# Max available hash table size, in MiB
27-
maxHash: int
28-
# Estimated depth of normal search
29-
defaultDepth: int
30-
# List of supported chess variants
31-
variants: str
32-
# Arbitrary data that engine provider can use for identification or bookkeeping
33-
providerData: NotRequired[str]
34-
35-
3615
Color: TypeAlias = Literal["white", "black"]
3716

3817
GameType: TypeAlias = Literal[
@@ -47,6 +26,17 @@ class ExternalEngine(TypedDict):
4726
"fromPosition",
4827
]
4928

29+
UciVariant = Literal[
30+
"chess",
31+
"crazyhouse",
32+
"antichess",
33+
"atomic",
34+
"horde",
35+
"kingofthehill",
36+
"racingkings",
37+
"3check",
38+
]
39+
5040
Speed = Literal[
5141
"ultraBullet", "bullet", "blitz", "rapid", "classical", "correspondence"
5242
]

berserk/types/external_engine.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from typing import List
2+
3+
from typing_extensions import TypedDict, NotRequired
4+
from .common import UciVariant
5+
6+
7+
class ExternalEngine(TypedDict):
8+
# Engine ID
9+
id: str
10+
# Engine display name
11+
name: str
12+
# Secret token that can be used to request analysis
13+
clientSecret: str
14+
# User this engine has been registered for
15+
userId: str
16+
# Max number of available threads
17+
maxThreads: int
18+
# Max available hash table size, in MiB
19+
maxHash: int
20+
# Estimated depth of normal search
21+
defaultDepth: int
22+
# List of supported chess variants
23+
variants: list[UciVariant]
24+
# Arbitrary data that engine provider can use for identification or bookkeeping
25+
providerData: NotRequired[str]
26+
27+
28+
class ExternalEngineWork(TypedDict):
29+
# Arbitrary string that identifies the analysis session. Providers may clear the hash table between sessions
30+
sessionId: str
31+
# Number of threads to use for analysis
32+
threads: int
33+
# Hash table size to use for analysis, in MiB
34+
hash: int
35+
# Requested number of principle variations
36+
multiPv: List[int]
37+
# Uci variant
38+
variant: str
39+
# Initial position of the game
40+
initialFen: str
41+
# List of moves played from the initial position, in UCI notation
42+
moves: List[str]
43+
# Request an infinite search (rather than roughly aiming for defaultDepth)
44+
infinite: NotRequired[bool]
45+
46+
47+
class ExternalEngineRequest(TypedDict):
48+
id: str
49+
work: ExternalEngineWork
50+
engine: ExternalEngine
51+
52+
53+
class PrincipleVariationAnalysis(TypedDict):
54+
# Current search depth of the pv
55+
depth: int
56+
# Variation in UCI notation
57+
moves: List[str]
58+
# Evaluation in centi-pawns, from White's point of view
59+
cp: NotRequired[int]
60+
# Evaluation in signed moves to mate, from White's point of view
61+
mate: NotRequired[int]
62+
63+
64+
class EngineAnalysisOutput(TypedDict):
65+
# Number of milliseconds the search has been going on
66+
time: int
67+
# Current search depth
68+
depth: int
69+
# Number of nodes visited so far
70+
nodes: int
71+
# Information about up to 5 pvs, with the primary pv at index 0
72+
pvs: List[PrincipleVariationAnalysis]

0 commit comments

Comments
 (0)