eslams-core 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eslams/__init__.py +52 -0
- eslams/action_descriptors.py +404 -0
- eslams/agent/__init__.py +5 -0
- eslams/agent/server.py +54 -0
- eslams/agents.py +979 -0
- eslams/arena.py +72 -0
- eslams/arena_transport.py +842 -0
- eslams/arenas/__init__.py +163 -0
- eslams/arenas/advanced_cards.py +927 -0
- eslams/arenas/backgammon.py +279 -0
- eslams/arenas/battleship.py +163 -0
- eslams/arenas/checkers.py +270 -0
- eslams/arenas/chess.py +240 -0
- eslams/arenas/classic_cards.py +577 -0
- eslams/arenas/connect_four.py +121 -0
- eslams/arenas/control_arcade.py +911 -0
- eslams/arenas/east_asian_board.py +952 -0
- eslams/arenas/gomoku.py +154 -0
- eslams/arenas/gridworld.py +371 -0
- eslams/arenas/hex.py +163 -0
- eslams/arenas/mancala.py +182 -0
- eslams/arenas/matrix_games.py +241 -0
- eslams/arenas/modern_control.py +532 -0
- eslams/arenas/nine_mens_morris.py +271 -0
- eslams/arenas/othello.py +213 -0
- eslams/arenas/pentago.py +175 -0
- eslams/arenas/poker.py +498 -0
- eslams/arenas/strategic_games.py +836 -0
- eslams/arenas/tic_tac_toe.py +100 -0
- eslams/arenas/tile_card_games.py +760 -0
- eslams/arenas/ultimate_tic_tac_toe.py +201 -0
- eslams/artifacts.py +1825 -0
- eslams/bench.py +161 -0
- eslams/catalogue.py +179 -0
- eslams/cli.py +977 -0
- eslams/contracts/__init__.py +85 -0
- eslams/contracts/artifact.py +238 -0
- eslams/contracts/catalogue.py +118 -0
- eslams/contracts/eval_plan.py +113 -0
- eslams/contracts/json_schema.py +920 -0
- eslams/contracts/provider.py +127 -0
- eslams/contracts/publication.py +76 -0
- eslams/contracts/replay.py +160 -0
- eslams/contracts/runner_job.py +187 -0
- eslams/contracts/safety.py +74 -0
- eslams/contracts/security.py +157 -0
- eslams/contracts/versions.py +79 -0
- eslams/core_contract.py +680 -0
- eslams/eval_runtime.py +170 -0
- eslams/events.py +145 -0
- eslams/fixtures.py +102 -0
- eslams/golden.py +80 -0
- eslams/hashing.py +73 -0
- eslams/model_actions.py +215 -0
- eslams/observation_budgets.py +72 -0
- eslams/official.py +119 -0
- eslams/planning.py +155 -0
- eslams/policy.py +50 -0
- eslams/protocol.py +240 -0
- eslams/provider_preflight.py +64 -0
- eslams/providers/__init__.py +6 -0
- eslams/providers/anthropic.py +5 -0
- eslams/providers/capabilities.py +350 -0
- eslams/providers/data/models.generated.json +220111 -0
- eslams/providers/data/overrides.json +54 -0
- eslams/providers/google.py +5 -0
- eslams/providers/openai.py +5 -0
- eslams/providers/registry.py +225 -0
- eslams/public_catalogue.py +267 -0
- eslams/public_replay.py +241 -0
- eslams/publication_export.py +470 -0
- eslams/rendering.py +137 -0
- eslams/replay.py +633 -0
- eslams/replay_projection.py +171 -0
- eslams/runner.py +817 -0
- eslams/runner_health.py +70 -0
- eslams/runner_result.py +58 -0
- eslams/runner_server.py +107 -0
- eslams/runner_session.py +142 -0
- eslams/state.py +93 -0
- eslams_core/__init__.py +3 -0
- eslams_core/bench.py +8 -0
- eslams_core-0.4.0.dist-info/METADATA +820 -0
- eslams_core-0.4.0.dist-info/RECORD +87 -0
- eslams_core-0.4.0.dist-info/WHEEL +4 -0
- eslams_core-0.4.0.dist-info/entry_points.txt +2 -0
- eslams_core-0.4.0.dist-info/licenses/LICENSE +21 -0
eslams/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Public eSlams framework.
|
|
2
|
+
|
|
3
|
+
eSlams Core gives agent builders a small, strict contract:
|
|
4
|
+
|
|
5
|
+
* implement POST /act or use a local Python agent
|
|
6
|
+
* run against an eSlams arena
|
|
7
|
+
* produce a deterministic trace, replay, score, and artifact
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from eslams.agents import MockProviderAgent
|
|
11
|
+
from eslams.artifacts import (
|
|
12
|
+
ArtifactManifest,
|
|
13
|
+
ArtifactValidationReport,
|
|
14
|
+
ArtifactValidator,
|
|
15
|
+
SignatureValidationStatus,
|
|
16
|
+
extract_provider_usage,
|
|
17
|
+
extract_public_manifest,
|
|
18
|
+
extract_validation_summary,
|
|
19
|
+
open_artifact,
|
|
20
|
+
read_member,
|
|
21
|
+
write_artifact,
|
|
22
|
+
)
|
|
23
|
+
from eslams.contracts import schema_versions
|
|
24
|
+
from eslams.core_contract import core_step, engine_capabilities, prompt_package
|
|
25
|
+
from eslams.protocol import ActRequest, ActResponse
|
|
26
|
+
from eslams.runner import RunConfig, Runner
|
|
27
|
+
from eslams.state import ArenaState
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"ActRequest",
|
|
31
|
+
"ActResponse",
|
|
32
|
+
"ArenaState",
|
|
33
|
+
"ArtifactManifest",
|
|
34
|
+
"ArtifactValidationReport",
|
|
35
|
+
"ArtifactValidator",
|
|
36
|
+
"MockProviderAgent",
|
|
37
|
+
"RunConfig",
|
|
38
|
+
"Runner",
|
|
39
|
+
"SignatureValidationStatus",
|
|
40
|
+
"core_step",
|
|
41
|
+
"engine_capabilities",
|
|
42
|
+
"extract_provider_usage",
|
|
43
|
+
"extract_public_manifest",
|
|
44
|
+
"extract_validation_summary",
|
|
45
|
+
"open_artifact",
|
|
46
|
+
"prompt_package",
|
|
47
|
+
"read_member",
|
|
48
|
+
"schema_versions",
|
|
49
|
+
"write_artifact",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
__version__ = "0.4.0"
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""Public-safe action descriptors for interactive Arena transport."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from eslams.hashing import canonical_json
|
|
10
|
+
from eslams.state import ArenaState
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class ActionDescriptor:
|
|
15
|
+
token: str
|
|
16
|
+
label: str
|
|
17
|
+
short_label: str
|
|
18
|
+
verb: str
|
|
19
|
+
object: str
|
|
20
|
+
category: str
|
|
21
|
+
group: str
|
|
22
|
+
sort_key: str
|
|
23
|
+
prompt_label: str
|
|
24
|
+
confirm: bool = False
|
|
25
|
+
disabled_reason: str | None = None
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> dict[str, Any]:
|
|
28
|
+
return {
|
|
29
|
+
"token": self.token,
|
|
30
|
+
"label": self.label,
|
|
31
|
+
"short_label": self.short_label,
|
|
32
|
+
"verb": self.verb,
|
|
33
|
+
"object": self.object,
|
|
34
|
+
"category": self.category,
|
|
35
|
+
"group": self.group,
|
|
36
|
+
"sort_key": self.sort_key,
|
|
37
|
+
"prompt_label": self.prompt_label,
|
|
38
|
+
"confirm": self.confirm,
|
|
39
|
+
"disabled_reason": self.disabled_reason,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
POSITION_NAMES_3X3 = {
|
|
44
|
+
0: "top-left",
|
|
45
|
+
1: "top",
|
|
46
|
+
2: "top-right",
|
|
47
|
+
3: "left",
|
|
48
|
+
4: "center",
|
|
49
|
+
5: "right",
|
|
50
|
+
6: "bottom-left",
|
|
51
|
+
7: "bottom",
|
|
52
|
+
8: "bottom-right",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
CONTROL_LABELS = {
|
|
56
|
+
"accelerate": "Accelerate",
|
|
57
|
+
"advance": "Advance",
|
|
58
|
+
"all-in": "Go all-in",
|
|
59
|
+
"balance": "Balance",
|
|
60
|
+
"bet": "Bet",
|
|
61
|
+
"brake": "Brake",
|
|
62
|
+
"call": "Call",
|
|
63
|
+
"check": "Check",
|
|
64
|
+
"coast": "Coast",
|
|
65
|
+
"cooperate": "Cooperate",
|
|
66
|
+
"defect": "Defect",
|
|
67
|
+
"dodge-left": "Dodge left",
|
|
68
|
+
"dodge-right": "Dodge right",
|
|
69
|
+
"down": "Move down",
|
|
70
|
+
"fire": "Fire",
|
|
71
|
+
"guard": "Guard",
|
|
72
|
+
"hit": "Hit",
|
|
73
|
+
"hold": "Hold",
|
|
74
|
+
"hook": "Hook",
|
|
75
|
+
"idle": "Idle",
|
|
76
|
+
"jab": "Jab",
|
|
77
|
+
"jump": "Jump",
|
|
78
|
+
"left": "Move left",
|
|
79
|
+
"left-step": "Step left",
|
|
80
|
+
"main": "Main thrust",
|
|
81
|
+
"paper": "Paper",
|
|
82
|
+
"pass": "Pass",
|
|
83
|
+
"resign": "Resign",
|
|
84
|
+
"retreat": "Retreat",
|
|
85
|
+
"right": "Move right",
|
|
86
|
+
"right-step": "Step right",
|
|
87
|
+
"rock": "Rock",
|
|
88
|
+
"scissors": "Scissors",
|
|
89
|
+
"shoot": "Shoot",
|
|
90
|
+
"skate-left": "Skate left",
|
|
91
|
+
"skate-right": "Skate right",
|
|
92
|
+
"stand": "Stand",
|
|
93
|
+
"stay": "Stay",
|
|
94
|
+
"up": "Move up",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
SUIT_SYMBOLS = {"C": "\u2663", "D": "\u2666", "H": "\u2665", "S": "\u2660"}
|
|
98
|
+
SUIT_NAMES = {"C": "clubs", "D": "diamonds", "H": "hearts", "S": "spades"}
|
|
99
|
+
CARD_RE = re.compile(r"^(A|K|Q|J|10|[2-9]|T)([CDHS])$")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def action_token(action: Any) -> str:
|
|
103
|
+
"""Return the stable token string used by interactive transport."""
|
|
104
|
+
|
|
105
|
+
if isinstance(action, str):
|
|
106
|
+
return action
|
|
107
|
+
return canonical_json(action)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def action_descriptors(
|
|
111
|
+
*,
|
|
112
|
+
game_id: str,
|
|
113
|
+
state: ArenaState,
|
|
114
|
+
actions: list[Any],
|
|
115
|
+
) -> list[dict[str, Any]]:
|
|
116
|
+
return [
|
|
117
|
+
describe_action(game_id=game_id, state=state, action=action).to_dict()
|
|
118
|
+
for action in actions
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def describe_action(*, game_id: str, state: ArenaState, action: Any) -> ActionDescriptor:
|
|
123
|
+
token = action_token(action)
|
|
124
|
+
if isinstance(action, int):
|
|
125
|
+
return _integer_descriptor(game_id, state, action, token)
|
|
126
|
+
if isinstance(action, list):
|
|
127
|
+
return _list_descriptor(game_id, action, token)
|
|
128
|
+
if isinstance(action, str):
|
|
129
|
+
return _string_descriptor(game_id, state, action, token)
|
|
130
|
+
return _fallback_descriptor(token)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _integer_descriptor(
|
|
134
|
+
game_id: str,
|
|
135
|
+
state: ArenaState,
|
|
136
|
+
action: int,
|
|
137
|
+
token: str,
|
|
138
|
+
) -> ActionDescriptor:
|
|
139
|
+
if game_id == "connect-four":
|
|
140
|
+
label = f"Drop in column {action + 1}"
|
|
141
|
+
return _descriptor(
|
|
142
|
+
token,
|
|
143
|
+
label,
|
|
144
|
+
f"Column {action + 1}",
|
|
145
|
+
"drop",
|
|
146
|
+
f"column {action + 1}",
|
|
147
|
+
"drop_disc",
|
|
148
|
+
)
|
|
149
|
+
if game_id == "tic-tac-toe":
|
|
150
|
+
position = POSITION_NAMES_3X3.get(action, f"square {action + 1}")
|
|
151
|
+
label = f"Play {position}"
|
|
152
|
+
return _descriptor(token, label, position.title(), "play", position, "mark_square")
|
|
153
|
+
if game_id == "mancala":
|
|
154
|
+
label = f"Sow from pit {action + 1}"
|
|
155
|
+
return _descriptor(token, label, f"Pit {action + 1}", "sow", f"pit {action + 1}", "sow_pit")
|
|
156
|
+
if game_id == "goofspiel":
|
|
157
|
+
label = f"Play bid card {action}"
|
|
158
|
+
return _descriptor(token, label, str(action), "play", f"bid card {action}", "play_card")
|
|
159
|
+
if game_id == "first-price-sealed-bid-auction":
|
|
160
|
+
label = f"Bid {action}"
|
|
161
|
+
return _descriptor(token, label, str(action), "bid", str(action), "bid")
|
|
162
|
+
rows, cols = _grid_shape(state)
|
|
163
|
+
if rows and cols and 0 <= action < rows * cols:
|
|
164
|
+
row = action // cols
|
|
165
|
+
col = action % cols
|
|
166
|
+
label = f"Place at row {row + 1}, column {col + 1}"
|
|
167
|
+
return _descriptor(
|
|
168
|
+
token,
|
|
169
|
+
label,
|
|
170
|
+
f"R{row + 1}C{col + 1}",
|
|
171
|
+
"place",
|
|
172
|
+
f"row {row + 1}, column {col + 1}",
|
|
173
|
+
"place_piece",
|
|
174
|
+
)
|
|
175
|
+
label = f"Choose {action}"
|
|
176
|
+
return _descriptor(token, label, str(action), "choose", str(action), "choose")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _list_descriptor(game_id: str, action: list[Any], token: str) -> ActionDescriptor:
|
|
180
|
+
if game_id == "othello" and len(action) == 2:
|
|
181
|
+
row, col = action
|
|
182
|
+
label = f"Place at row {int(row) + 1}, column {int(col) + 1}"
|
|
183
|
+
return _descriptor(
|
|
184
|
+
token,
|
|
185
|
+
label,
|
|
186
|
+
f"R{int(row) + 1}C{int(col) + 1}",
|
|
187
|
+
"place",
|
|
188
|
+
f"row {int(row) + 1}, column {int(col) + 1}",
|
|
189
|
+
"place_piece",
|
|
190
|
+
)
|
|
191
|
+
label = "Choose " + ", ".join(str(item) for item in action)
|
|
192
|
+
return _descriptor(token, label, label.removeprefix("Choose "), "choose", token, "choose")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _string_descriptor(
|
|
196
|
+
game_id: str,
|
|
197
|
+
state: ArenaState,
|
|
198
|
+
action: str,
|
|
199
|
+
token: str,
|
|
200
|
+
) -> ActionDescriptor:
|
|
201
|
+
chess = _chess_descriptor(state, action, token)
|
|
202
|
+
if game_id == "chess" and chess is not None:
|
|
203
|
+
return chess
|
|
204
|
+
if game_id == "liars-dice" and re.match(r"^\d+x\d+$", action):
|
|
205
|
+
count, face = action.split("x", 1)
|
|
206
|
+
label = f"Bid {count} x {face}"
|
|
207
|
+
return _descriptor(token, label, f"{count}x{face}", "bid", f"{count} x {face}", "bid")
|
|
208
|
+
if game_id == "pentago":
|
|
209
|
+
pentago = _pentago_descriptor(action, token)
|
|
210
|
+
if pentago is not None:
|
|
211
|
+
return pentago
|
|
212
|
+
if "," in action and "-" not in action and all(part.isdigit() for part in action.split(",")):
|
|
213
|
+
row, col = action.split(",", 1)
|
|
214
|
+
label = f"Target row {int(row) + 1}, column {int(col) + 1}"
|
|
215
|
+
return _descriptor(
|
|
216
|
+
token,
|
|
217
|
+
label,
|
|
218
|
+
f"R{int(row) + 1}C{int(col) + 1}",
|
|
219
|
+
"target",
|
|
220
|
+
label[7:].lower(),
|
|
221
|
+
"target_cell",
|
|
222
|
+
)
|
|
223
|
+
if "-" in action and re.match(r"^[a-z]?\d+[a-z]?-?[a-z]?\d+[a-z]?$", action):
|
|
224
|
+
label = f"Move {action.replace('-', ' to ')}"
|
|
225
|
+
return _descriptor(token, label, action, "move", action, "move_piece")
|
|
226
|
+
if action.startswith("move:"):
|
|
227
|
+
obj = action.split(":", 1)[1]
|
|
228
|
+
label = f"Move {obj.replace('-', ' to ')}"
|
|
229
|
+
return _descriptor(token, label, obj, "move", obj, "move_piece")
|
|
230
|
+
colon_prefixes = ("play:", "discard:", "draw:", "call:", "bet:", "bid:", "offer:", "place:")
|
|
231
|
+
if action.startswith(colon_prefixes):
|
|
232
|
+
return _colon_descriptor(action, token)
|
|
233
|
+
if action in CONTROL_LABELS:
|
|
234
|
+
label = CONTROL_LABELS[action]
|
|
235
|
+
verb = label.split()[0].lower()
|
|
236
|
+
return _descriptor(token, label, label, verb, label.lower(), _category_for_verb(verb))
|
|
237
|
+
return _fallback_descriptor(token)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _chess_descriptor(state: ArenaState, action: str, token: str) -> ActionDescriptor | None:
|
|
241
|
+
legal_moves = state.public_state.get("legal_moves")
|
|
242
|
+
if not isinstance(legal_moves, list):
|
|
243
|
+
return None
|
|
244
|
+
detail = next(
|
|
245
|
+
(
|
|
246
|
+
item
|
|
247
|
+
for item in legal_moves
|
|
248
|
+
if isinstance(item, dict) and str(item.get("uci")) == action
|
|
249
|
+
),
|
|
250
|
+
None,
|
|
251
|
+
)
|
|
252
|
+
san = str(detail.get("san")) if isinstance(detail, dict) and detail.get("san") else None
|
|
253
|
+
src = action[:2]
|
|
254
|
+
dst = action[2:4] if len(action) >= 4 else action
|
|
255
|
+
suffix = f" ({san})" if san else ""
|
|
256
|
+
label = f"Move {src} to {dst}{suffix}"
|
|
257
|
+
short = san or f"{src}-{dst}"
|
|
258
|
+
return _descriptor(token, label, short, "move", dst, "move_piece")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _pentago_descriptor(action: str, token: str) -> ActionDescriptor | None:
|
|
262
|
+
parts = action.split(":")
|
|
263
|
+
if len(parts) != 3 or not parts[0].isdigit() or not parts[1].isdigit():
|
|
264
|
+
return None
|
|
265
|
+
cell, quadrant, direction = parts
|
|
266
|
+
direction_label = "clockwise" if direction == "cw" else "counter-clockwise"
|
|
267
|
+
label = f"Place at cell {int(cell) + 1}, rotate quadrant {int(quadrant) + 1} {direction_label}"
|
|
268
|
+
return _descriptor(
|
|
269
|
+
token,
|
|
270
|
+
label,
|
|
271
|
+
f"{int(cell) + 1} / Q{int(quadrant) + 1} {direction.upper()}",
|
|
272
|
+
"place",
|
|
273
|
+
f"cell {int(cell) + 1}, quadrant {int(quadrant) + 1}",
|
|
274
|
+
"place_and_rotate",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _colon_descriptor(action: str, token: str) -> ActionDescriptor:
|
|
279
|
+
verb, value = action.split(":", 1)
|
|
280
|
+
if verb in {"play", "discard"}:
|
|
281
|
+
if value.isdigit():
|
|
282
|
+
index_label = f"card {int(value) + 1}"
|
|
283
|
+
label = f"{verb.capitalize()} {index_label}"
|
|
284
|
+
return _descriptor(token, label, index_label.title(), verb, index_label, f"{verb}_card")
|
|
285
|
+
cards = [_format_card(part.strip()) for part in value.split(",") if part.strip()]
|
|
286
|
+
obj = _join_words(cards) if cards else value
|
|
287
|
+
label = f"{verb.capitalize()} {obj}"
|
|
288
|
+
return _descriptor(token, label, obj, verb, obj, f"{verb}_card")
|
|
289
|
+
if verb == "draw":
|
|
290
|
+
obj = "from deck" if value == "deck" else f"from {value}"
|
|
291
|
+
label = f"Draw {obj}"
|
|
292
|
+
return _descriptor(token, label, value.title(), "draw", obj, "draw_card")
|
|
293
|
+
if verb == "call":
|
|
294
|
+
suit = SUIT_NAMES.get(value, value)
|
|
295
|
+
label = f"Call {suit}"
|
|
296
|
+
return _descriptor(token, label, suit.title(), "call", suit, "call_suit")
|
|
297
|
+
if verb in {"bet", "bid"}:
|
|
298
|
+
label = f"{verb.capitalize()} {value}"
|
|
299
|
+
return _descriptor(token, label, value, verb, value, verb)
|
|
300
|
+
if verb == "offer":
|
|
301
|
+
obj = " / ".join(value.split(":"))
|
|
302
|
+
label = f"Offer {obj}"
|
|
303
|
+
return _descriptor(token, label, obj, "offer", obj, "offer")
|
|
304
|
+
if verb == "place":
|
|
305
|
+
label = f"Place at point {int(value) + 1}" if value.isdigit() else f"Place {value}"
|
|
306
|
+
short = f"Point {int(value) + 1}" if value.isdigit() else value
|
|
307
|
+
return _descriptor(token, label, short, "place", short.lower(), "place_piece")
|
|
308
|
+
return _fallback_descriptor(token)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _fallback_descriptor(token: str) -> ActionDescriptor:
|
|
312
|
+
label = _title_token(token)
|
|
313
|
+
verb = label.split()[0].lower() if label else "choose"
|
|
314
|
+
return _descriptor(
|
|
315
|
+
token,
|
|
316
|
+
label or token,
|
|
317
|
+
label or token,
|
|
318
|
+
verb,
|
|
319
|
+
label.lower() or token,
|
|
320
|
+
"action",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _descriptor(
|
|
325
|
+
token: str,
|
|
326
|
+
label: str,
|
|
327
|
+
short_label: str,
|
|
328
|
+
verb: str,
|
|
329
|
+
obj: str,
|
|
330
|
+
category: str,
|
|
331
|
+
) -> ActionDescriptor:
|
|
332
|
+
group = _group_for_category(category)
|
|
333
|
+
return ActionDescriptor(
|
|
334
|
+
token=token,
|
|
335
|
+
label=label,
|
|
336
|
+
short_label=short_label,
|
|
337
|
+
verb=verb,
|
|
338
|
+
object=obj,
|
|
339
|
+
category=category,
|
|
340
|
+
group=group,
|
|
341
|
+
sort_key=f"{category}:{_sort_fragment(token)}",
|
|
342
|
+
prompt_label=label[0].lower() + label[1:] if label else token,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _grid_shape(state: ArenaState) -> tuple[int | None, int | None]:
|
|
347
|
+
rows = state.public_state.get("rows")
|
|
348
|
+
cols = state.public_state.get("cols")
|
|
349
|
+
return (rows if isinstance(rows, int) else None, cols if isinstance(cols, int) else None)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _category_for_verb(verb: str) -> str:
|
|
353
|
+
if verb in {"move", "step", "skate"}:
|
|
354
|
+
return "move"
|
|
355
|
+
if verb in {"bet", "bid", "offer"}:
|
|
356
|
+
return verb
|
|
357
|
+
return "action"
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _group_for_category(category: str) -> str:
|
|
361
|
+
groups = {
|
|
362
|
+
"action": "Actions",
|
|
363
|
+
"bet": "Bet",
|
|
364
|
+
"bid": "Bid",
|
|
365
|
+
"call_suit": "Call a suit",
|
|
366
|
+
"choose": "Choose",
|
|
367
|
+
"discard_card": "Discard a card",
|
|
368
|
+
"draw_card": "Draw",
|
|
369
|
+
"drop_disc": "Drop a disc",
|
|
370
|
+
"mark_square": "Play a square",
|
|
371
|
+
"move": "Move",
|
|
372
|
+
"move_piece": "Move a piece",
|
|
373
|
+
"offer": "Make an offer",
|
|
374
|
+
"place_and_rotate": "Place and rotate",
|
|
375
|
+
"place_piece": "Place a piece",
|
|
376
|
+
"play_card": "Play a card",
|
|
377
|
+
"sow_pit": "Sow seeds",
|
|
378
|
+
"target_cell": "Target a cell",
|
|
379
|
+
}
|
|
380
|
+
return groups.get(category, "Actions")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _format_card(value: str) -> str:
|
|
384
|
+
match = CARD_RE.match(value)
|
|
385
|
+
if match is None:
|
|
386
|
+
return value
|
|
387
|
+
rank, suit = match.groups()
|
|
388
|
+
return f"{'10' if rank == 'T' else rank}{SUIT_SYMBOLS[suit]}"
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _join_words(values: list[str]) -> str:
|
|
392
|
+
if len(values) <= 1:
|
|
393
|
+
return values[0] if values else ""
|
|
394
|
+
if len(values) == 2:
|
|
395
|
+
return f"{values[0]} and {values[1]}"
|
|
396
|
+
return ", ".join(values[:-1]) + f", and {values[-1]}"
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _title_token(token: str) -> str:
|
|
400
|
+
return token.replace(":", " ").replace("-", " ").replace("_", " ").title()
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _sort_fragment(token: str) -> str:
|
|
404
|
+
return token.replace(":", "_").replace(",", "_").replace("[", "").replace("]", "")
|
eslams/agent/__init__.py
ADDED
eslams/agent/server.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Small FastAPI server wrapper for building /act agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI, HTTPException
|
|
8
|
+
|
|
9
|
+
from eslams.protocol import ActRequest, ActResponse, ProtocolError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentServer:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
*,
|
|
16
|
+
title: str = "eSlams Agent",
|
|
17
|
+
agent_id: str = "custom-agent",
|
|
18
|
+
version: str = "1",
|
|
19
|
+
) -> None:
|
|
20
|
+
self.title = title
|
|
21
|
+
self.agent_id = agent_id
|
|
22
|
+
self.version = version
|
|
23
|
+
self.app = FastAPI(title=title)
|
|
24
|
+
self._handler: Callable[[ActRequest], Any] | None = None
|
|
25
|
+
self.app.post("/act")(self._act)
|
|
26
|
+
self.app.get("/health")(self._health)
|
|
27
|
+
|
|
28
|
+
def act(self, func: Callable[[ActRequest], Any]) -> Callable[[ActRequest], Any]:
|
|
29
|
+
self._handler = func
|
|
30
|
+
return func
|
|
31
|
+
|
|
32
|
+
async def _health(self) -> dict[str, str]:
|
|
33
|
+
return {"status": "ok", "agent_id": self.agent_id, "version": self.version}
|
|
34
|
+
|
|
35
|
+
async def _act(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
36
|
+
if self._handler is None:
|
|
37
|
+
raise HTTPException(status_code=503, detail="No /act handler registered")
|
|
38
|
+
try:
|
|
39
|
+
request = ActRequest.from_mapping(payload)
|
|
40
|
+
value = self._handler(request)
|
|
41
|
+
if isinstance(value, ActResponse):
|
|
42
|
+
response = value
|
|
43
|
+
elif isinstance(value, dict):
|
|
44
|
+
response = ActResponse.from_mapping(value)
|
|
45
|
+
else:
|
|
46
|
+
response = ActResponse(action=value)
|
|
47
|
+
return response.to_dict()
|
|
48
|
+
except ProtocolError as exc:
|
|
49
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
50
|
+
|
|
51
|
+
def run(self, *, host: str = "0.0.0.0", port: int = 8000) -> None:
|
|
52
|
+
import uvicorn
|
|
53
|
+
|
|
54
|
+
uvicorn.run(self.app, host=host, port=port)
|