treequest 0.3.1__tar.gz → 0.3.3__tar.gz

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.
Files changed (49) hide show
  1. {treequest-0.3.1 → treequest-0.3.3}/PKG-INFO +37 -14
  2. {treequest-0.3.1 → treequest-0.3.3}/README.md +35 -12
  3. {treequest-0.3.1 → treequest-0.3.3}/pyproject.toml +9 -3
  4. treequest-0.3.3/src/treequest/__init__.py +40 -0
  5. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/algos/ab_mcts_a/prob_state.py +24 -12
  6. treequest-0.3.3/src/treequest/algos/ab_mcts_m/_ab_mcts_m_imports.py +58 -0
  7. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/algos/ab_mcts_m/algo.py +49 -1
  8. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/algos/ab_mcts_m/pymc_interface.py +35 -6
  9. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/algos/tree.py +9 -1
  10. treequest-0.3.1/src/treequest/__init__.py +0 -33
  11. treequest-0.3.1/src/treequest/algos/ab_mcts_m/_ab_mcts_m_imports.py +0 -29
  12. {treequest-0.3.1 → treequest-0.3.3}/.gitignore +0 -0
  13. {treequest-0.3.1 → treequest-0.3.3}/LICENSE +0 -0
  14. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/algos/ab_mcts_a/algo.py +0 -0
  15. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/algos/ab_mcts_m/numpyro_utils.py +0 -0
  16. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/algos/base.py +0 -0
  17. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/algos/best_first_search.py +0 -0
  18. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/algos/multi_armed_bandit_ucb.py +0 -0
  19. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/algos/standard_mcts.py +0 -0
  20. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/algos/tree_of_thought_bfs.py +0 -0
  21. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/imports.py +0 -0
  22. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/py.typed +0 -0
  23. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/ranker.py +0 -0
  24. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/trial.py +0 -0
  25. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/types.py +0 -0
  26. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/__init__.py +0 -0
  27. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/algo_adapters/__init__.py +0 -0
  28. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/algo_adapters/ab_mcts_a.py +0 -0
  29. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/algo_adapters/ab_mcts_m.py +0 -0
  30. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/algo_adapters/base.py +0 -0
  31. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/algo_adapters/best_first_search.py +0 -0
  32. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/algo_adapters/multi_armed_bandit_ucb.py +0 -0
  33. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/algo_adapters/standard_mcts.py +0 -0
  34. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/algo_adapters/tree_of_thoughts_bfs.py +0 -0
  35. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/assets/colormaps.json +0 -0
  36. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/assets/d3.LICENSE.txt +0 -0
  37. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/assets/d3.v7.min.js +0 -0
  38. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/assets/d3_tree.html.jinja2 +0 -0
  39. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/build_snapshot.py +0 -0
  40. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/errors.py +0 -0
  41. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/render.py +0 -0
  42. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/renderers/__init__.py +0 -0
  43. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/renderers/color_utils.py +0 -0
  44. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/renderers/graphviz_renderer.py +0 -0
  45. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/renderers/html.py +0 -0
  46. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/renderers/json_yaml.py +0 -0
  47. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/renderers/mermaid.py +0 -0
  48. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/vis/snapshot.py +0 -0
  49. {treequest-0.3.1 → treequest-0.3.3}/src/treequest/visualization.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: treequest
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: A Tree Search Library with Flexible API for LLM Inference-Time Scaling
5
5
  Author-email: Kou Misaki <kou.misaki@sakana.ai>, Takuya Akiba <takiba@sakana.ai>
6
6
  License-File: LICENSE
@@ -57,24 +57,27 @@ import treequest as tq
57
57
  # Each node is associated with a user-definable `state`.
58
58
  State = str
59
59
 
60
+
60
61
  # 1. Define a function to be used for node generation.
61
62
  def generate(parent_state: State | None) -> tuple[State, float]:
62
63
  """Generates new states and scores based on the parent state."""
63
- if parent_state is None: # None represents the expansion from root.
64
+ if parent_state is None: # None represents the expansion from root.
64
65
  new_state = "Initial state"
65
66
  else:
66
67
  new_state = f"State after {parent_state}"
67
68
 
68
- score = random.random() # A score for the new state; It should be normalized to the [0, 1] range.
69
+ # A score for the new state; It should be normalized to the [0, 1] range.
70
+ score = random.random()
69
71
  return new_state, score
70
72
 
73
+
71
74
  # 2. Instantiate the algorithm and a search tree object.
72
75
  algo = tq.ABMCTSA()
73
76
  search_tree = algo.init_tree()
74
77
 
75
78
  # 3. Run the search with a generation budget (10 in this case).
76
79
  for _ in range(10):
77
- search_tree = algo.step(search_tree, {'Action A': generate})
80
+ search_tree = algo.step(search_tree, {"Action A": generate})
78
81
 
79
82
  # 4. Extract the best score and state.
80
83
  best_state, best_node_score = tq.top_k(search_tree, algo, k=1)[0]
@@ -82,7 +85,8 @@ print(f"Best state: {best_state}, Score: {best_node_score}")
82
85
 
83
86
  # 5. Visualize the search tree.
84
87
  output_file_basename = Path("ab_mcts_a_search_tree")
85
- tq.render(search_tree, output_file_basename, format="html") # Generates `ab_mcts_a_search_tree.html`
88
+ # Generates `ab_mcts_a_search_tree.html`
89
+ tq.render(search_tree, output_file_basename, format="html")
86
90
  ```
87
91
 
88
92
  Alternatively, you can use an ask–tell interface with batched AB-MCTS sampling steps:
@@ -92,8 +96,9 @@ import treequest as tq
92
96
 
93
97
  State = str
94
98
 
95
- def generate(parent_state: State | None) -> tuple[State, float]:
96
- ...
99
+
100
+ def generate(parent_state: State | None) -> tuple[State, float]: ...
101
+
97
102
 
98
103
  generate_fns = {"Action A": generate}
99
104
  actions = list(generate_fns.keys())
@@ -160,11 +165,13 @@ import dataclasses
160
165
 
161
166
  import treequest as tq
162
167
 
168
+
163
169
  @dataclasses.dataclass
164
170
  class State:
165
171
  llm_answer: str
166
172
  score: float
167
173
 
174
+
168
175
  def generate(parent_state: State | None) -> tuple[State, float]:
169
176
  """Generate a new node by calling an LLM."""
170
177
  if parent_state is None:
@@ -174,12 +181,14 @@ def generate(parent_state: State | None) -> tuple[State, float]:
174
181
 
175
182
  return state, state.score
176
183
 
184
+
177
185
  def initial_generation() -> State:
178
186
  """
179
187
  Call LLM API to generate an initial answer.
180
188
  """
181
189
  ...
182
190
 
191
+
183
192
  def refine_answer(llm_answer: str, score: float) -> State:
184
193
  """
185
194
  Call LLM API to refine an answer.
@@ -190,11 +199,11 @@ def refine_answer(llm_answer: str, score: float) -> State:
190
199
  algo = tq.ABMCTSM()
191
200
  search_tree = algo.init_tree()
192
201
  for i in range(20):
193
- search_tree = algo.step(search_tree, {'Action Label': generate})
202
+ search_tree = algo.step(search_tree, {"Action Label": generate})
194
203
  # Logging best node during the search.
195
204
  if (i + 1) % 5 == 0:
196
205
  best_interim_state, _ = tq.top_k(search_tree, algo, k=1)[0]
197
- print(f"Iteration {i+1}: Best state so far = {best_interim_state}")
206
+ print(f"Iteration {i + 1}: Best state so far = {best_interim_state}")
198
207
 
199
208
  best_state, _ = tq.top_k(search_tree, algo, k=1)[0]
200
209
  print(f"Best Answer: {best_state.llm_answer}, Best Score: {best_state.score}")
@@ -209,6 +218,7 @@ from functools import partial
209
218
 
210
219
  import treequest as tq
211
220
 
221
+
212
222
  def generate(llm_name: str, parent_state=None):
213
223
  """
214
224
  Call LLM API using litellm, vllm, etc., to generate a new node
@@ -216,9 +226,12 @@ def generate(llm_name: str, parent_state=None):
216
226
  ...
217
227
  return new_state, new_score
218
228
 
229
+
219
230
  llm_names = ["o4-mini", "gemini-2.5-pro"]
220
231
  # Create dict of different actions backed by different LLMs.
221
- generate_fns = {llm_name: partial(generate, llm_name=llm_name) for llm_name in llm_names}
232
+ generate_fns = {
233
+ llm_name: partial(generate, llm_name=llm_name) for llm_name in llm_names
234
+ }
222
235
 
223
236
  algo = tq.StandardMCTS()
224
237
  search_tree = algo.init_tree()
@@ -237,6 +250,7 @@ The variation is not limited to LLM types; you can use different prompts, action
237
250
  - Idempotent: calling `tell` twice on the same `trial_id` does not add extra nodes.
238
251
  - For queue-based algorithms, over-told Trials beyond possible number of childs from a parent node (e.g., `(# actions)*samples_per_action` for StandardMCTS) become `INVALID` and are not reflected.
239
252
  - Scores are expected to be normalized to the `[0, 1]` range.
253
+ - `ABMCTSM` exposes an internal `_advanced_config` for advanced knobs (e.g., bypassing the runtime check or tuning Bayesian priors). In most cases, the defaults are sufficient; only change it if you understand the impact.
240
254
 
241
255
  ## Algorithms
242
256
 
@@ -285,10 +299,12 @@ from pathlib import Path
285
299
  import treequest as tq
286
300
  from PIL import Image
287
301
 
302
+
288
303
  class State:
289
304
  text: str
290
305
  image: Image.Image
291
306
 
307
+
292
308
  def state_formatter_html(state: State) -> str:
293
309
  """Formats the state for HTML visualization."""
294
310
  buffer = BytesIO()
@@ -298,13 +314,15 @@ def state_formatter_html(state: State) -> str:
298
314
  # In case state.text cannot be trusted, we recommend e.g., html.escape(state.text) to avoid html injection.
299
315
  return f'<p>{state.text}</p><br/><img src="data:image/webp;base64,{img_str}" width=100% />'
300
316
 
317
+
301
318
  generate_fns = {
302
319
  "action_1": ... # Define your node generation functions (actions) here
303
320
  }
304
321
 
305
322
  algo = tq.ABMCTSA()
306
323
  search_tree = algo.init_tree()
307
- output_dir = Path("search_tree"); output_dir.mkdir(exist_ok=True)
324
+ output_dir = Path("search_tree")
325
+ output_dir.mkdir(exist_ok=True)
308
326
  for i in range(10):
309
327
  search_tree = algo.step(search_tree, generate_fns)
310
328
  # Rendering "latest.html" for checking search progress on the fly
@@ -313,11 +331,16 @@ for i in range(10):
313
331
  search_tree,
314
332
  output_basename=output_dir / "latest",
315
333
  format="html",
316
- state_formatter=state_formatter_html, # Use html custom formetter
334
+ state_formatter=state_formatter_html, # Use html custom formetter
317
335
  )
318
336
 
319
337
  tq.render(search_tree, output_basename=output_dir / "final", format="pdf")
320
- tq.render(search_tree, output_basename=output_dir / "final", format="html", state_formatter=state_formatter_html)
338
+ tq.render(
339
+ search_tree,
340
+ output_basename=output_dir / "final",
341
+ format="html",
342
+ state_formatter=state_formatter_html,
343
+ )
321
344
  ```
322
345
 
323
346
  > **IMPORTANT:** When using HTML format, ensure that the HTML file is securely handled, especially if the state formatter includes raw HTML content. Avoid opening untrusted HTML files in your browser. For example, XSS (cross site scripting) attacks can occur if the state includes malicious HTML/JavaScript code.
@@ -21,24 +21,27 @@ import treequest as tq
21
21
  # Each node is associated with a user-definable `state`.
22
22
  State = str
23
23
 
24
+
24
25
  # 1. Define a function to be used for node generation.
25
26
  def generate(parent_state: State | None) -> tuple[State, float]:
26
27
  """Generates new states and scores based on the parent state."""
27
- if parent_state is None: # None represents the expansion from root.
28
+ if parent_state is None: # None represents the expansion from root.
28
29
  new_state = "Initial state"
29
30
  else:
30
31
  new_state = f"State after {parent_state}"
31
32
 
32
- score = random.random() # A score for the new state; It should be normalized to the [0, 1] range.
33
+ # A score for the new state; It should be normalized to the [0, 1] range.
34
+ score = random.random()
33
35
  return new_state, score
34
36
 
37
+
35
38
  # 2. Instantiate the algorithm and a search tree object.
36
39
  algo = tq.ABMCTSA()
37
40
  search_tree = algo.init_tree()
38
41
 
39
42
  # 3. Run the search with a generation budget (10 in this case).
40
43
  for _ in range(10):
41
- search_tree = algo.step(search_tree, {'Action A': generate})
44
+ search_tree = algo.step(search_tree, {"Action A": generate})
42
45
 
43
46
  # 4. Extract the best score and state.
44
47
  best_state, best_node_score = tq.top_k(search_tree, algo, k=1)[0]
@@ -46,7 +49,8 @@ print(f"Best state: {best_state}, Score: {best_node_score}")
46
49
 
47
50
  # 5. Visualize the search tree.
48
51
  output_file_basename = Path("ab_mcts_a_search_tree")
49
- tq.render(search_tree, output_file_basename, format="html") # Generates `ab_mcts_a_search_tree.html`
52
+ # Generates `ab_mcts_a_search_tree.html`
53
+ tq.render(search_tree, output_file_basename, format="html")
50
54
  ```
51
55
 
52
56
  Alternatively, you can use an ask–tell interface with batched AB-MCTS sampling steps:
@@ -56,8 +60,9 @@ import treequest as tq
56
60
 
57
61
  State = str
58
62
 
59
- def generate(parent_state: State | None) -> tuple[State, float]:
60
- ...
63
+
64
+ def generate(parent_state: State | None) -> tuple[State, float]: ...
65
+
61
66
 
62
67
  generate_fns = {"Action A": generate}
63
68
  actions = list(generate_fns.keys())
@@ -124,11 +129,13 @@ import dataclasses
124
129
 
125
130
  import treequest as tq
126
131
 
132
+
127
133
  @dataclasses.dataclass
128
134
  class State:
129
135
  llm_answer: str
130
136
  score: float
131
137
 
138
+
132
139
  def generate(parent_state: State | None) -> tuple[State, float]:
133
140
  """Generate a new node by calling an LLM."""
134
141
  if parent_state is None:
@@ -138,12 +145,14 @@ def generate(parent_state: State | None) -> tuple[State, float]:
138
145
 
139
146
  return state, state.score
140
147
 
148
+
141
149
  def initial_generation() -> State:
142
150
  """
143
151
  Call LLM API to generate an initial answer.
144
152
  """
145
153
  ...
146
154
 
155
+
147
156
  def refine_answer(llm_answer: str, score: float) -> State:
148
157
  """
149
158
  Call LLM API to refine an answer.
@@ -154,11 +163,11 @@ def refine_answer(llm_answer: str, score: float) -> State:
154
163
  algo = tq.ABMCTSM()
155
164
  search_tree = algo.init_tree()
156
165
  for i in range(20):
157
- search_tree = algo.step(search_tree, {'Action Label': generate})
166
+ search_tree = algo.step(search_tree, {"Action Label": generate})
158
167
  # Logging best node during the search.
159
168
  if (i + 1) % 5 == 0:
160
169
  best_interim_state, _ = tq.top_k(search_tree, algo, k=1)[0]
161
- print(f"Iteration {i+1}: Best state so far = {best_interim_state}")
170
+ print(f"Iteration {i + 1}: Best state so far = {best_interim_state}")
162
171
 
163
172
  best_state, _ = tq.top_k(search_tree, algo, k=1)[0]
164
173
  print(f"Best Answer: {best_state.llm_answer}, Best Score: {best_state.score}")
@@ -173,6 +182,7 @@ from functools import partial
173
182
 
174
183
  import treequest as tq
175
184
 
185
+
176
186
  def generate(llm_name: str, parent_state=None):
177
187
  """
178
188
  Call LLM API using litellm, vllm, etc., to generate a new node
@@ -180,9 +190,12 @@ def generate(llm_name: str, parent_state=None):
180
190
  ...
181
191
  return new_state, new_score
182
192
 
193
+
183
194
  llm_names = ["o4-mini", "gemini-2.5-pro"]
184
195
  # Create dict of different actions backed by different LLMs.
185
- generate_fns = {llm_name: partial(generate, llm_name=llm_name) for llm_name in llm_names}
196
+ generate_fns = {
197
+ llm_name: partial(generate, llm_name=llm_name) for llm_name in llm_names
198
+ }
186
199
 
187
200
  algo = tq.StandardMCTS()
188
201
  search_tree = algo.init_tree()
@@ -201,6 +214,7 @@ The variation is not limited to LLM types; you can use different prompts, action
201
214
  - Idempotent: calling `tell` twice on the same `trial_id` does not add extra nodes.
202
215
  - For queue-based algorithms, over-told Trials beyond possible number of childs from a parent node (e.g., `(# actions)*samples_per_action` for StandardMCTS) become `INVALID` and are not reflected.
203
216
  - Scores are expected to be normalized to the `[0, 1]` range.
217
+ - `ABMCTSM` exposes an internal `_advanced_config` for advanced knobs (e.g., bypassing the runtime check or tuning Bayesian priors). In most cases, the defaults are sufficient; only change it if you understand the impact.
204
218
 
205
219
  ## Algorithms
206
220
 
@@ -249,10 +263,12 @@ from pathlib import Path
249
263
  import treequest as tq
250
264
  from PIL import Image
251
265
 
266
+
252
267
  class State:
253
268
  text: str
254
269
  image: Image.Image
255
270
 
271
+
256
272
  def state_formatter_html(state: State) -> str:
257
273
  """Formats the state for HTML visualization."""
258
274
  buffer = BytesIO()
@@ -262,13 +278,15 @@ def state_formatter_html(state: State) -> str:
262
278
  # In case state.text cannot be trusted, we recommend e.g., html.escape(state.text) to avoid html injection.
263
279
  return f'<p>{state.text}</p><br/><img src="data:image/webp;base64,{img_str}" width=100% />'
264
280
 
281
+
265
282
  generate_fns = {
266
283
  "action_1": ... # Define your node generation functions (actions) here
267
284
  }
268
285
 
269
286
  algo = tq.ABMCTSA()
270
287
  search_tree = algo.init_tree()
271
- output_dir = Path("search_tree"); output_dir.mkdir(exist_ok=True)
288
+ output_dir = Path("search_tree")
289
+ output_dir.mkdir(exist_ok=True)
272
290
  for i in range(10):
273
291
  search_tree = algo.step(search_tree, generate_fns)
274
292
  # Rendering "latest.html" for checking search progress on the fly
@@ -277,11 +295,16 @@ for i in range(10):
277
295
  search_tree,
278
296
  output_basename=output_dir / "latest",
279
297
  format="html",
280
- state_formatter=state_formatter_html, # Use html custom formetter
298
+ state_formatter=state_formatter_html, # Use html custom formetter
281
299
  )
282
300
 
283
301
  tq.render(search_tree, output_basename=output_dir / "final", format="pdf")
284
- tq.render(search_tree, output_basename=output_dir / "final", format="html", state_formatter=state_formatter_html)
302
+ tq.render(
303
+ search_tree,
304
+ output_basename=output_dir / "final",
305
+ format="html",
306
+ state_formatter=state_formatter_html,
307
+ )
285
308
  ```
286
309
 
287
310
  > **IMPORTANT:** When using HTML format, ensure that the HTML file is securely handled, especially if the state formatter includes raw HTML content. Avoid opening untrusted HTML files in your browser. For example, XSS (cross site scripting) attacks can occur if the state includes malicious HTML/JavaScript code.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "treequest"
3
- version = "0.3.1"
3
+ version = "0.3.3"
4
4
  description = "A Tree Search Library with Flexible API for LLM Inference-Time Scaling"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -44,16 +44,22 @@ build-backend = "hatchling.build"
44
44
  [dependency-groups]
45
45
  dev = [
46
46
  "line-profiler>=4.2.0",
47
- "mypy>=1.15.0",
47
+ # Pin ruff and mypy so that their new releases do not change CI results.
48
+ # Keep ruff in sync with .pre-commit-config.yaml.
49
+ "mypy==2.4.0",
48
50
  "pre-commit>=4.2.0",
49
51
  "pytest>=8.3.4",
50
52
  "pytest-xdist>=3.6.1",
51
- "ruff>=0.9.9",
53
+ "ruff==0.17.1",
52
54
  "tqdm>=4.64.0",
53
55
  "types-tqdm>=4.64.0",
54
56
  "types-PyYAML>=6.0.0",
55
57
  ]
56
58
 
59
+ [tool.ruff.lint]
60
+ # The rules Ruff enabled by default before 0.16, which expanded its default rule set.
61
+ select = ["E4", "E7", "E9", "F"]
62
+
57
63
  [tool.hatch.build.targets.wheel]
58
64
  packages = ["src/treequest"]
59
65
  include = [
@@ -0,0 +1,40 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ # Expose the real symbol to static type checkers / IDEs even when optional
5
+ # dependencies for ABMCTSM are not installed at runtime.
6
+ from .algos.ab_mcts_m.algo import ABMCTSM
7
+ else:
8
+ from .algos.ab_mcts_m._ab_mcts_m_imports import _import as _ab_mcts_m_import
9
+
10
+ if not _ab_mcts_m_import.is_successful():
11
+ # Create a placeholder that raises an informative error when accessed
12
+ class _ABMCTSMPlaceholder:
13
+ def __getattr__(self, name): # type: ignore
14
+ _ab_mcts_m_import.check()
15
+ raise ImportError("ABMCTSM import failed.")
16
+
17
+ def __call__(self, *args, **kwargs): # type: ignore
18
+ _ab_mcts_m_import.check()
19
+ raise ImportError("ABMCTSM import failed.")
20
+
21
+ ABMCTSM = _ABMCTSMPlaceholder() # type: ignore
22
+ else:
23
+ from .algos.ab_mcts_m.algo import ABMCTSM # type: ignore[assignment]
24
+
25
+ from .algos.ab_mcts_a.algo import ABMCTSA
26
+ from .algos.base import Algorithm
27
+ from .algos.standard_mcts import StandardMCTS
28
+ from .algos.tree_of_thought_bfs import TreeOfThoughtsBFSAlgo
29
+ from .ranker import top_k
30
+ from .vis import render
31
+
32
+ __all__ = [
33
+ "StandardMCTS",
34
+ "top_k",
35
+ "TreeOfThoughtsBFSAlgo",
36
+ "ABMCTSA",
37
+ "ABMCTSM",
38
+ "Algorithm",
39
+ "render",
40
+ ]
@@ -13,6 +13,24 @@ from treequest.algos.tree import Node
13
13
  T = TypeVar("T")
14
14
 
15
15
 
16
+ def _child_index_by_identity(parent: Node, child: Node) -> int:
17
+ """
18
+ Return the index of `child` in `parent.children` using identity comparison.
19
+
20
+ NOTE: Avoid `list.index()` here because it relies on `__eq__`, and user-defined
21
+ states may include objects (e.g., numpy arrays / torch tensors) whose equality
22
+ returns non-bool values and can raise when coerced to `bool`.
23
+ """
24
+ for i, n in enumerate(parent.children):
25
+ if n is child:
26
+ return i
27
+ raise ValueError(
28
+ "Child node not found in parent.children by identity. "
29
+ f"child.expand_idx={getattr(child, 'expand_idx', None)}, "
30
+ f"parent.expand_idx={getattr(parent, 'expand_idx', None)}"
31
+ )
32
+
33
+
16
34
  @dataclasses.dataclass
17
35
  class BetaPrior:
18
36
  """
@@ -278,18 +296,15 @@ class NodeProbState:
278
296
  assert node.parent is not None
279
297
 
280
298
  # Update for continuing with existing nodes
299
+ child_idx = _child_index_by_identity(node.parent, node)
281
300
  if self.model_selection_strategy.startswith("multiarm_bandit_"):
282
301
  # Update the node's probability
283
- self.node_probas["shared"][
284
- node.parent.children.index(node)
285
- ].tell_observation(reward)
302
+ self.node_probas["shared"][child_idx].tell_observation(reward)
286
303
  # For multiarm bandit strategies, update shared CONT distribution
287
304
  self.gen_vs_cont_probas["shared"]["CONT"].tell_observation(reward)
288
305
  else:
289
306
  # Update the node's probability
290
- self.node_probas[action][node.parent.children.index(node)].tell_observation(
291
- reward
292
- )
307
+ self.node_probas[action][child_idx].tell_observation(reward)
293
308
  # For stack strategy, update action-specific CONT distribution
294
309
  self.gen_vs_cont_probas[action]["CONT"].tell_observation(reward)
295
310
 
@@ -444,14 +459,11 @@ class NodeProbState:
444
459
 
445
460
  # Initialize the node's probability distribution with its score
446
461
  assert node.parent is not None
462
+ child_idx = _child_index_by_identity(node.parent, node)
447
463
  if model_selection_strategy == "stack":
448
- self.node_probas[action][node.parent.children.index(node)].tell_observation(
449
- node.score
450
- )
464
+ self.node_probas[action][child_idx].tell_observation(node.score)
451
465
  else:
452
- self.node_probas["shared"][
453
- node.parent.children.index(node)
454
- ].tell_observation(node.score)
466
+ self.node_probas["shared"][child_idx].tell_observation(node.score)
455
467
 
456
468
  def get_action_for_child(self, child: Node) -> str:
457
469
  """
@@ -0,0 +1,58 @@
1
+ import importlib
2
+ from pathlib import Path
3
+
4
+ from treequest.imports import try_import
5
+
6
+ _ARVIZ_IMPORT_ATTEMPTS = 3
7
+
8
+
9
+ def _import_arviz() -> None:
10
+ # TODO: Remove this workaround after arviz fixes the race in its daily warning
11
+ # https://github.com/arviz-devs/arviz/issues/2530
12
+ # arviz 0.23.3 and 0.23.4 update the warning's stamp file through a fixed
13
+ # temporary file, so importing arviz from several processes at once can raise
14
+ # FileNotFoundError. The process that won has already updated the stamp, so
15
+ # importing again normally succeeds.
16
+ for attempt in range(1, _ARVIZ_IMPORT_ATTEMPTS + 1):
17
+ try:
18
+ importlib.import_module("arviz")
19
+ return
20
+ except FileNotFoundError as e:
21
+ # The process that lost the race fails to rename the temporary file.
22
+ lost_race = Path(e.filename or "").name == "daily_warning.tmp"
23
+ if not lost_race or attempt == _ARVIZ_IMPORT_ATTEMPTS:
24
+ raise
25
+ # Leave sys.modules as it is: Python has already dropped the failed
26
+ # package, the leftover arviz.preview is safe to reuse, and another
27
+ # thread may be importing arviz by now.
28
+
29
+
30
+ with try_import() as _import:
31
+ import jax
32
+ from packaging.version import Version
33
+
34
+ # TODO: Remove this hotfix after numpyro fixes incompatibility with jax>=0.7
35
+ # https://github.com/pyro-ppl/numpyro/issues/2051
36
+ if Version(jax.__version__) >= Version("0.7.0"):
37
+ import jax.experimental.pjit as _pjit
38
+ from jax.extend.core.primitives import jit_p
39
+
40
+ _pjit.pjit_p = jit_p # type: ignore
41
+ import numpy as np
42
+ import numpyro # type: ignore
43
+ import pandas as pd # type: ignore
44
+
45
+ _import_arviz() # Import arviz before pymc does; see _import_arviz.
46
+ import pymc as pm # type: ignore
47
+ from pymc.sampling.jax import sample_numpyro_nuts # type: ignore
48
+ from xarray import DataArray
49
+
50
+ __all__ = [
51
+ "jax",
52
+ "np",
53
+ "numpyro",
54
+ "pd",
55
+ "pm",
56
+ "sample_numpyro_nuts",
57
+ "DataArray",
58
+ ]
@@ -47,6 +47,37 @@ class ABMCTSMState(Generic[StateT]):
47
47
  trial_store: TrialStore[StateT] = field(default_factory=TrialStore[StateT])
48
48
 
49
49
 
50
+ @dataclass(frozen=True)
51
+ class ABMCTSMAdvancedConfig:
52
+ """
53
+ Advanced/experimental ABMCTSM configuration.
54
+
55
+ Most users should rely on the defaults.
56
+ """
57
+
58
+ # If True, validate that rewards/scores are in [0, 1].
59
+ validate_reward_range: bool = True
60
+
61
+ # Prior sigmas for the underlying Bayesian model.
62
+ prior_mu_alpha_sigma: float = 0.2
63
+ prior_sigma_alpha_sigma: float = 0.2
64
+ prior_sigma_y_sigma: float = 0.3
65
+
66
+ def __post_init__(self) -> None:
67
+ if self.prior_mu_alpha_sigma <= 0:
68
+ raise ValueError(
69
+ f"prior_mu_alpha_sigma must be > 0, got {self.prior_mu_alpha_sigma}"
70
+ )
71
+ if self.prior_sigma_alpha_sigma <= 0:
72
+ raise ValueError(
73
+ f"prior_sigma_alpha_sigma must be > 0, got {self.prior_sigma_alpha_sigma}"
74
+ )
75
+ if self.prior_sigma_y_sigma <= 0:
76
+ raise ValueError(
77
+ f"prior_sigma_y_sigma must be > 0, got {self.prior_sigma_y_sigma}"
78
+ )
79
+
80
+
50
81
  class ABMCTSM(Algorithm[StateT, ABMCTSMState[StateT]]):
51
82
  """
52
83
  Monte Carlo Tree Search algorithm using AB-MCTS-M algorithm.
@@ -63,6 +94,7 @@ class ABMCTSM(Algorithm[StateT, ABMCTSMState[StateT]]):
63
94
  min_subtree_size_for_pruning: int = 4,
64
95
  same_score_proportion_threshold: float = 0.75,
65
96
  max_process_workers: int = os.cpu_count() or 1,
97
+ _advanced_config: ABMCTSMAdvancedConfig | None = None,
66
98
  is_worker: bool = False,
67
99
  ):
68
100
  """
@@ -79,7 +111,13 @@ class ABMCTSM(Algorithm[StateT, ABMCTSMState[StateT]]):
79
111
  min_subtree_size_for_pruning: Pruning Config, see PruningConfig class.
80
112
  same_score_proportion_threshold: Pruning Config, see PruningConfig class.
81
113
  max_process_workers: Maximum number of parallel processes used for running PyMC sampling.
114
+ _advanced_config: Advanced/experimental configuration. Defaults are recommended.
82
115
  """
116
+ self._advanced_config = (
117
+ _advanced_config
118
+ if _advanced_config is not None
119
+ else ABMCTSMAdvancedConfig()
120
+ )
83
121
  self.enable_pruning = enable_pruning
84
122
  self.pruning_config = PruningConfig(
85
123
  min_subtree_size_for_pruning=min_subtree_size_for_pruning,
@@ -94,6 +132,9 @@ class ABMCTSM(Algorithm[StateT, ABMCTSMState[StateT]]):
94
132
  enable_pruning=self.enable_pruning,
95
133
  pruning_config=self.pruning_config,
96
134
  reward_average_priors=self.reward_average_priors,
135
+ prior_mu_alpha_sigma=self._advanced_config.prior_mu_alpha_sigma,
136
+ prior_sigma_alpha_sigma=self._advanced_config.prior_sigma_alpha_sigma,
137
+ prior_sigma_y_sigma=self._advanced_config.prior_sigma_y_sigma,
97
138
  model_selection_strategy=self.model_selection_strategy,
98
139
  )
99
140
 
@@ -195,6 +236,9 @@ class ABMCTSM(Algorithm[StateT, ABMCTSMState[StateT]]):
195
236
  def _get_generation_action(
196
237
  self, state: ABMCTSMState[StateT], node: Node[StateT], actions: list[str]
197
238
  ) -> str:
239
+ if len(actions) == 1:
240
+ return actions[0]
241
+
198
242
  observations = Observation.collect_all_observations_of_descendant(
199
243
  node, state.all_observations
200
244
  )
@@ -255,6 +299,7 @@ class ABMCTSM(Algorithm[StateT, ABMCTSMState[StateT]]):
255
299
  min_subtree_size_for_pruning=self.pruning_config.min_subtree_size_for_pruning,
256
300
  same_score_proportion_threshold=self.pruning_config.same_score_proportion_threshold,
257
301
  max_process_workers=1,
302
+ _advanced_config=self._advanced_config,
258
303
  is_worker=True,
259
304
  )
260
305
 
@@ -298,7 +343,10 @@ class ABMCTSM(Algorithm[StateT, ABMCTSMState[StateT]]):
298
343
 
299
344
  # Add new node to the tree
300
345
  new_node = state.tree.add_node(
301
- result, parent_node, trial_id=finished_trial.trial_id
346
+ result,
347
+ parent_node,
348
+ trial_id=finished_trial.trial_id,
349
+ validate_score_range=self._advanced_config.validate_reward_range,
302
350
  )
303
351
 
304
352
  # Record observation
@@ -148,6 +148,9 @@ class PyMCInterface:
148
148
  enable_pruning: bool = True,
149
149
  pruning_config: Optional[PruningConfig] = None,
150
150
  reward_average_priors: Optional[float | Dict[str, float]] = None,
151
+ prior_mu_alpha_sigma: float = 0.2,
152
+ prior_sigma_alpha_sigma: float = 0.2,
153
+ prior_sigma_y_sigma: float = 0.3,
151
154
  model_selection_strategy: str = "multiarm_bandit_thompson",
152
155
  ):
153
156
  _import.check()
@@ -161,6 +164,22 @@ class PyMCInterface:
161
164
  self.reward_average_priors = (
162
165
  reward_average_priors if reward_average_priors is not None else dict()
163
166
  )
167
+ if prior_mu_alpha_sigma <= 0:
168
+ raise ValueError(
169
+ f"prior_mu_alpha_sigma must be > 0, got {prior_mu_alpha_sigma}"
170
+ )
171
+ if prior_sigma_alpha_sigma <= 0:
172
+ raise ValueError(
173
+ f"prior_sigma_alpha_sigma must be > 0, got {prior_sigma_alpha_sigma}"
174
+ )
175
+ if prior_sigma_y_sigma <= 0:
176
+ raise ValueError(
177
+ f"prior_sigma_y_sigma must be > 0, got {prior_sigma_y_sigma}"
178
+ )
179
+
180
+ self.prior_mu_alpha_sigma = prior_mu_alpha_sigma
181
+ self.prior_sigma_alpha_sigma = prior_sigma_alpha_sigma
182
+ self.prior_sigma_y_sigma = prior_sigma_y_sigma
164
183
 
165
184
  # Strategy for model selection:
166
185
  # "stack": Perform separate fits for each model (traditional approach)
@@ -491,13 +510,19 @@ class PyMCInterface:
491
510
  with pm.Model(coords=coords if not is_prior_model else None) as model:
492
511
  # Priors START
493
512
  # Overall Goodness of the model itself; mu is set to be 0.5 (50% prob of solving the problem)
494
- mu_alpha = pm.Normal("mu_alpha", mu=0.5, sigma=0.2)
513
+ mu_alpha = pm.Normal(
514
+ "mu_alpha",
515
+ mu=self.get_reward_average_prior(action=None),
516
+ sigma=self.prior_mu_alpha_sigma,
517
+ )
495
518
 
496
519
  # expresses the strength of score fluctuation across answers
497
- sigma_alpha = pm.HalfNormal("sigma_alpha", sigma=0.2)
520
+ sigma_alpha = pm.HalfNormal(
521
+ "sigma_alpha", sigma=self.prior_sigma_alpha_sigma
522
+ )
498
523
 
499
524
  # expresses the strength of score fluctuation inside answers
500
- sigma_y = pm.HalfNormal("sigma_y", sigma=0.3)
525
+ sigma_y = pm.HalfNormal("sigma_y", sigma=self.prior_sigma_y_sigma)
501
526
  # Priors END
502
527
 
503
528
  group_dims = "action" if not is_prior_model else None
@@ -543,14 +568,18 @@ class PyMCInterface:
543
568
  # Priors START
544
569
  # Overall Goodness of the model itself; mu is set to be 0.5 (50% prob of solving the problem)
545
570
  mu_alpha = pm.Normal(
546
- "mu_alpha", mu=self.get_reward_average_prior(action), sigma=0.2
571
+ "mu_alpha",
572
+ mu=self.get_reward_average_prior(action),
573
+ sigma=self.prior_mu_alpha_sigma,
547
574
  )
548
575
 
549
576
  # expresses the strength of score fluctuation across answers
550
- sigma_alpha = pm.HalfNormal("sigma_alpha", sigma=0.2)
577
+ sigma_alpha = pm.HalfNormal(
578
+ "sigma_alpha", sigma=self.prior_sigma_alpha_sigma
579
+ )
551
580
 
552
581
  # expresses the strength of score fluctuation inside answers
553
- sigma_y = pm.HalfNormal("sigma_y", sigma=0.3)
582
+ sigma_y = pm.HalfNormal("sigma_y", sigma=self.prior_sigma_y_sigma)
554
583
  # Priors END
555
584
 
556
585
  group_dims = "child_idx" if not is_prior_model else None
@@ -14,6 +14,9 @@ class Node(Generic[StateT]):
14
14
  # Scores should be in the range from 0 to 1. For root, we set it to be -1.0 as a placeholder.
15
15
  score: float = -1.0
16
16
 
17
+ # If True, validate that score is in [0, 1] for non-root nodes.
18
+ validate_score_range: bool = dataclasses.field(default=True, kw_only=True)
19
+
17
20
  # Reflects the order of expansion. The root has expand_idx = -1, followed by 0,1,2,...
18
21
  expand_idx: int = -1
19
22
 
@@ -23,7 +26,9 @@ class Node(Generic[StateT]):
23
26
 
24
27
  def __post_init__(self) -> None:
25
28
  if not self.is_root():
26
- if not (self.score <= 1.0 and self.score >= 0.0):
29
+ if self.validate_score_range and not (
30
+ self.score <= 1.0 and self.score >= 0.0
31
+ ):
27
32
  raise RuntimeError(
28
33
  f"The score value should be between 0 and 1, while {self.score} is set."
29
34
  )
@@ -117,11 +122,14 @@ class Tree(Generic[StateT]):
117
122
  state_score: StateScoreType[StateT],
118
123
  parent: Node[StateT],
119
124
  trial_id: Optional[TrialId] = None,
125
+ *,
126
+ validate_score_range: bool = True,
120
127
  ) -> Node[StateT]:
121
128
  state, score = state_score
122
129
  node = Node(
123
130
  state=state,
124
131
  score=score,
132
+ validate_score_range=validate_score_range,
125
133
  parent=parent,
126
134
  expand_idx=self.size - 1,
127
135
  trial_id=trial_id,
@@ -1,33 +0,0 @@
1
- from .algos.ab_mcts_m._ab_mcts_m_imports import _import as _ab_mcts_m_import
2
-
3
- if not _ab_mcts_m_import.is_successful():
4
- # Create a placeholder that raises an informative error when accessed
5
- class _ABMCTSMPlaceholder:
6
- def __getattr__(self, name): # type: ignore
7
- _ab_mcts_m_import.check()
8
- raise ImportError("ABMCTSM import failed.")
9
-
10
- def __call__(self, *args, **kwargs): # type: ignore
11
- _ab_mcts_m_import.check()
12
- raise ImportError("ABMCTSM import failed.")
13
-
14
- ABMCTSM = _ABMCTSMPlaceholder() # type: ignore
15
- else:
16
- from .algos.ab_mcts_m.algo import ABMCTSM # type: ignore[assignment]
17
-
18
- from .algos.ab_mcts_a.algo import ABMCTSA
19
- from .algos.base import Algorithm
20
- from .algos.standard_mcts import StandardMCTS
21
- from .algos.tree_of_thought_bfs import TreeOfThoughtsBFSAlgo
22
- from .ranker import top_k
23
- from .vis import render
24
-
25
- __all__ = [
26
- "StandardMCTS",
27
- "top_k",
28
- "TreeOfThoughtsBFSAlgo",
29
- "ABMCTSA",
30
- "ABMCTSM",
31
- "Algorithm",
32
- "render",
33
- ]
@@ -1,29 +0,0 @@
1
- from treequest.imports import try_import
2
-
3
- with try_import() as _import:
4
- import jax
5
- from packaging.version import Version
6
-
7
- # TODO: Remove this hotfix after numpyro fixes incompatibility with jax>=0.7
8
- # https://github.com/pyro-ppl/numpyro/issues/2051
9
- if Version(jax.__version__) >= Version("0.7.0"):
10
- import jax.experimental.pjit as _pjit
11
- from jax.extend.core.primitives import jit_p
12
-
13
- _pjit.pjit_p = jit_p # type: ignore
14
- import numpy as np
15
- import numpyro # type: ignore
16
- import pandas as pd # type: ignore
17
- import pymc as pm # type: ignore
18
- from pymc.sampling.jax import sample_numpyro_nuts # type: ignore
19
- from xarray import DataArray
20
-
21
- __all__ = [
22
- "jax",
23
- "np",
24
- "numpyro",
25
- "pd",
26
- "pm",
27
- "sample_numpyro_nuts",
28
- "DataArray",
29
- ]
File without changes
File without changes