alphaengine 0.9.1__tar.gz → 0.9.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.
- {alphaengine-0.9.1 → alphaengine-0.9.3}/PKG-INFO +4 -1
- {alphaengine-0.9.1 → alphaengine-0.9.3}/README.md +3 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/_version.py +9 -1
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/cli.py +170 -30
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/client/agent.py +214 -19
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/client/session.py +19 -11
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/commands.py +37 -1
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/tui/app.py +71 -19
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/tui/dispatch.py +88 -2
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_agent.py +60 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_client.py +25 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_tui.py +71 -1
- {alphaengine-0.9.1 → alphaengine-0.9.3}/.github/workflows/ci.yml +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/.github/workflows/publish.yml +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/.gitignore +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/AGENTS.md +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/LICENSE +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/SECURITY.md +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/docs/assets/banner.png +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/docs/assets/data_boundary.png +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/docs/assets/session.png +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/docs/figure_contract.md +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/docs/workflows/evaluate_process.json +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/docs/workflows/research_week.json +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/docs/workflows/stress_study.json +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/pyproject.toml +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/scripts/gen_docs.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/__init__.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/__main__.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/agent/__init__.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/agent/answer.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/agent/driver.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/auth.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/book.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/charts.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/client/__init__.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/client/executor.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/complete.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/connectors/__init__.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/__init__.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/active.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/allocate.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/backtest.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/covariance.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/cross_section.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/factors.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/pairs.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/panel.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/performance.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/process.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/profile.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/risk.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/screen.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/series_shapes.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/signals.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/stress.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/technical.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/trace.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/validation.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/core/walkforward.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/demo.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/demo_book.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/demo_returns.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/demo_signal.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/demo_universe.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/events.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/loaders.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/model.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/py.typed +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/repl.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/study/__init__.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/study/report.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/study/schema.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/sweep/__init__.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/sweep/runner.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/src/alphaengine/tui/__init__.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_answer.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_book.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_cli.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_commands.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_events.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_executor_ops.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_goldens.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_loaders.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_math_audit.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_model.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_process.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_quant_maths.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_screen.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_signals.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_smoke.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_sweep.py +0 -0
- {alphaengine-0.9.1 → alphaengine-0.9.3}/tests/test_trace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: alphaengine
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.3
|
|
4
4
|
Summary: Validated research tooling for investment strategies: deflation, overfitting detection, and honest trial counts.
|
|
5
5
|
Project-URL: Homepage, https://github.com/quantOSC/alphaengine
|
|
6
6
|
Project-URL: Documentation, https://github.com/quantOSC/alphaengine#readme
|
|
@@ -333,6 +333,7 @@ w = hrp_weights(cov, names=names) # no matrix inverse; weights sum to one
|
|
|
333
333
|
|---|---|---|
|
|
334
334
|
| `book [<name> \| status]` | show or load sleeves on the multi-strategy book | session |
|
|
335
335
|
| `load <file \| module \| universe>` | a CSV, a project module, or a portal universe | session |
|
|
336
|
+
| `thesis [<name> \| clear]` | choose a portal thesis for the next run | session |
|
|
336
337
|
| `universe <name>` | same as load: a universe registered in the portal | session |
|
|
337
338
|
| `data <file>` | same as load: a local CSV or parquet | session |
|
|
338
339
|
| `project <module>` | same as load: a module with data and backtest_fn | session |
|
|
@@ -363,6 +364,7 @@ w = hrp_weights(cov, names=names) # no matrix inverse; weights sum to one
|
|
|
363
364
|
| `--project MODULE` | a module exposing `data` and `backtest_fn` |
|
|
364
365
|
| `--data FILE` | a local CSV: wide, long, or a single series |
|
|
365
366
|
| `--universe NAME` | a universe registered in the portal, with its stored closes |
|
|
367
|
+
| `--thesis NAME` | a thesis on this account; the run is attached to it |
|
|
366
368
|
| `--symbol TICKER` | one name out of a loaded universe; its closes become the return series |
|
|
367
369
|
| `--label TEXT` | what to call the artifact this run produces |
|
|
368
370
|
| `--input K=V` | a workflow input; repeatable |
|
|
@@ -398,6 +400,7 @@ alphaengine run size_position --data returns.csv
|
|
|
398
400
|
alphaengine run validate_study --project research.momentum
|
|
399
401
|
alphaengine models
|
|
400
402
|
alphaengine trace
|
|
403
|
+
alphaengine run screen_universe --thesis momentum
|
|
401
404
|
alphaengine logout
|
|
402
405
|
alphaengine version
|
|
403
406
|
```
|
|
@@ -288,6 +288,7 @@ w = hrp_weights(cov, names=names) # no matrix inverse; weights sum to one
|
|
|
288
288
|
|---|---|---|
|
|
289
289
|
| `book [<name> \| status]` | show or load sleeves on the multi-strategy book | session |
|
|
290
290
|
| `load <file \| module \| universe>` | a CSV, a project module, or a portal universe | session |
|
|
291
|
+
| `thesis [<name> \| clear]` | choose a portal thesis for the next run | session |
|
|
291
292
|
| `universe <name>` | same as load: a universe registered in the portal | session |
|
|
292
293
|
| `data <file>` | same as load: a local CSV or parquet | session |
|
|
293
294
|
| `project <module>` | same as load: a module with data and backtest_fn | session |
|
|
@@ -318,6 +319,7 @@ w = hrp_weights(cov, names=names) # no matrix inverse; weights sum to one
|
|
|
318
319
|
| `--project MODULE` | a module exposing `data` and `backtest_fn` |
|
|
319
320
|
| `--data FILE` | a local CSV: wide, long, or a single series |
|
|
320
321
|
| `--universe NAME` | a universe registered in the portal, with its stored closes |
|
|
322
|
+
| `--thesis NAME` | a thesis on this account; the run is attached to it |
|
|
321
323
|
| `--symbol TICKER` | one name out of a loaded universe; its closes become the return series |
|
|
322
324
|
| `--label TEXT` | what to call the artifact this run produces |
|
|
323
325
|
| `--input K=V` | a workflow input; repeatable |
|
|
@@ -353,6 +355,7 @@ alphaengine run size_position --data returns.csv
|
|
|
353
355
|
alphaengine run validate_study --project research.momentum
|
|
354
356
|
alphaengine models
|
|
355
357
|
alphaengine trace
|
|
358
|
+
alphaengine run screen_universe --thesis momentum
|
|
356
359
|
alphaengine logout
|
|
357
360
|
alphaengine version
|
|
358
361
|
```
|
|
@@ -80,4 +80,12 @@ is what a changed figure costs. The API may still move underneath it.
|
|
|
80
80
|
# 0.9.1 IS THE SESSION, NOT THE MATHS. No computed value moved. A signed-in
|
|
81
81
|
# account loads its one stored universe from the portal. `enter model key`
|
|
82
82
|
# takes whichever provider key is pasted. The left pane lists the commands.
|
|
83
|
-
|
|
83
|
+
#
|
|
84
|
+
# 0.9.2 IS THE SESSION, NOT THE MATHS. No computed value moved. A command
|
|
85
|
+
# shows that it is working, and a run writes each step as it happens. A thesis
|
|
86
|
+
# on the account can be pinned, and the run opened after that carries its id.
|
|
87
|
+
#
|
|
88
|
+
# 0.9.3 IS THE SESSION, NOT THE MATHS. No computed value moved. The math pane
|
|
89
|
+
# is narrower. A sentence is shown the workflow it is choosing and what a step
|
|
90
|
+
# already measured, and a finished step is not run again.
|
|
91
|
+
__version__ = "0.9.3"
|
|
@@ -301,6 +301,19 @@ QUESTION_VERBS: dict[str, str] = {
|
|
|
301
301
|
#: hand-maintained copies of this map is precisely the drift `commands.py`
|
|
302
302
|
#: exists to prevent.
|
|
303
303
|
QUESTION_FOR_WORKFLOW: dict[str, str] = {w: v for v, w in QUESTION_VERBS.items()}
|
|
304
|
+
#: What each workflow answers, in a reader's words. The server publishes a name.
|
|
305
|
+
#: A name is not a choice. These sentences describe the choice and say nothing
|
|
306
|
+
#: about the order of steps inside it. The model sees the same sentences.
|
|
307
|
+
_WORKFLOW_ANSWERS: dict[str, str] = {
|
|
308
|
+
"diagnose_data": "can I trust this data",
|
|
309
|
+
"screen_universe": "what in my universe is worth a look",
|
|
310
|
+
"evaluate_signal": "does this signal carry information",
|
|
311
|
+
"validate_study": "does this result hold up once you count the tries",
|
|
312
|
+
"stress_study": "where does it break",
|
|
313
|
+
"check_overlap": "is it new, or my book again",
|
|
314
|
+
"size_position": "how much of it should I hold",
|
|
315
|
+
"monitor_sleeve": "has anything crossed a line",
|
|
316
|
+
}
|
|
304
317
|
#: The held-back-rows marker. Probed like the rest: cp1252 renders a literal
|
|
305
318
|
#: ellipsis as mojibake in exactly the place a user reads their result.
|
|
306
319
|
ELLIPSIS = "…" if _UNICODE_OK else "..."
|
|
@@ -644,7 +657,8 @@ class Working:
|
|
|
644
657
|
self._started = time.monotonic()
|
|
645
658
|
sink = _SINK.get()
|
|
646
659
|
if sink is not None:
|
|
647
|
-
|
|
660
|
+
# The session paints this on the status line. It is not a trace line.
|
|
661
|
+
sink("\x1e" + self.plain)
|
|
648
662
|
return self
|
|
649
663
|
if not _tty():
|
|
650
664
|
return self
|
|
@@ -900,18 +914,6 @@ def cmd_workflows(args: argparse.Namespace) -> int:
|
|
|
900
914
|
say("The server offers no workflows.")
|
|
901
915
|
return 0
|
|
902
916
|
|
|
903
|
-
# WHAT EACH ONE ANSWERS, in the reader's words. The server publishes a name,
|
|
904
|
-
# a version and an input contract -- correctly, since anything more would be
|
|
905
|
-
# workflow knowledge crossing the wire -- but a name is not a choice. These
|
|
906
|
-
# sentences live here, on the client, for exactly that reason: they describe
|
|
907
|
-
# what a reader is picking between and disclose nothing about how it works.
|
|
908
|
-
ANSWERS = {
|
|
909
|
-
"screen_universe": "what in my universe is worth a look",
|
|
910
|
-
"validate_study": "does this result hold up once you count the tries",
|
|
911
|
-
"size_position": "how much of it should I hold",
|
|
912
|
-
"monitor_sleeve": "has anything crossed a line",
|
|
913
|
-
}
|
|
914
|
-
|
|
915
917
|
width = max(len(str(r.get("name"))) for r in rows)
|
|
916
918
|
say("")
|
|
917
919
|
for r in rows:
|
|
@@ -921,7 +923,7 @@ def cmd_workflows(args: argparse.Namespace) -> int:
|
|
|
921
923
|
# the same question disagree. Same reasoning as a trial count that says
|
|
922
924
|
# whether it was derived or asserted.
|
|
923
925
|
repro = green("reproducible") if r.get("reproducible") else yellow("may differ")
|
|
924
|
-
say(f" {bold(name.ljust(width))} {
|
|
926
|
+
say(f" {bold(name.ljust(width))} {_workflow_answer(r)}")
|
|
925
927
|
say(f" {' ' * width} {dim('needs ' + needs)} {DOT} {repro} {dim(str(r.get('version')))}")
|
|
926
928
|
say("")
|
|
927
929
|
say(dim(" alphaengine run <name> --data prices.csv ") + dim(f"{DOT} `commands run` for the flags"))
|
|
@@ -1619,12 +1621,12 @@ def _drive(run: Any, *, quiet: bool = False) -> None:
|
|
|
1619
1621
|
# with no why sent people to the traceback that no longer
|
|
1620
1622
|
# prints. The message came from the executor and was written
|
|
1621
1623
|
# to be read.
|
|
1622
|
-
say(f" {red(CROSS)} {bold(op)}")
|
|
1623
|
-
say(f" {NEST} {took}")
|
|
1624
|
+
say(f" step {n:<2} {red(CROSS)} {bold(op)}")
|
|
1624
1625
|
why = getattr(run, "last_error", None)
|
|
1625
1626
|
if why:
|
|
1626
1627
|
for line in str(why).splitlines():
|
|
1627
|
-
say(red(f"
|
|
1628
|
+
say(red(f" {line}"))
|
|
1629
|
+
say(f" {took}")
|
|
1628
1630
|
if failures[key] >= 2:
|
|
1629
1631
|
run.status = "abandoned"
|
|
1630
1632
|
run.stopped = {
|
|
@@ -1639,11 +1641,13 @@ def _drive(run: Any, *, quiet: bool = False) -> None:
|
|
|
1639
1641
|
# `emit.*` seal rather than measure) and printing a blank line
|
|
1640
1642
|
# for it would read as a stall.
|
|
1641
1643
|
found = _found(seen, run)
|
|
1642
|
-
say(f" {
|
|
1643
|
-
|
|
1644
|
+
say(f" step {n:<2} {bold(op)}")
|
|
1645
|
+
if found:
|
|
1646
|
+
say(f" {found}")
|
|
1647
|
+
say(f" {took}")
|
|
1644
1648
|
rejected = getattr(run, "trace_rejected", None)
|
|
1645
1649
|
if rejected:
|
|
1646
|
-
say(yellow(f"
|
|
1650
|
+
say(yellow(f" portal did not accept the series trace: {rejected}"))
|
|
1647
1651
|
|
|
1648
1652
|
if run.status != "open":
|
|
1649
1653
|
return
|
|
@@ -1708,11 +1712,28 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1708
1712
|
# named one explicitly. Without this, a sweep received `grid={}`.
|
|
1709
1713
|
if "grid" not in inputs and project_grid():
|
|
1710
1714
|
inputs["grid"] = project_grid()
|
|
1715
|
+
thesis_id = None
|
|
1716
|
+
named_thesis = getattr(args, "thesis", None)
|
|
1717
|
+
if named_thesis:
|
|
1718
|
+
chosen, candidates = match_thesis(session.theses(), str(named_thesis))
|
|
1719
|
+
if chosen is None:
|
|
1720
|
+
if candidates:
|
|
1721
|
+
names = ", ".join(thesis_label(r) for r in candidates)
|
|
1722
|
+
say(yellow(f"Which thesis? {names}"))
|
|
1723
|
+
else:
|
|
1724
|
+
say(yellow(f"No thesis named {named_thesis!r} on this account."))
|
|
1725
|
+
return 2
|
|
1726
|
+
thesis_id = thesis_id_of(chosen)
|
|
1727
|
+
if not thesis_id:
|
|
1728
|
+
say(yellow(f"{thesis_label(chosen)} has no id, so a run cannot be attached to it."))
|
|
1729
|
+
return 2
|
|
1730
|
+
say(dim(f" thesis {thesis_label(chosen)}"))
|
|
1711
1731
|
run = session.open(
|
|
1712
1732
|
args.workflow,
|
|
1713
1733
|
data=data,
|
|
1714
1734
|
backtest_fn=backtest_fn,
|
|
1715
1735
|
workspace_id=workspace_id,
|
|
1736
|
+
thesis_id=thesis_id,
|
|
1716
1737
|
**inputs,
|
|
1717
1738
|
)
|
|
1718
1739
|
_drive(run, quiet=args.quiet)
|
|
@@ -2235,7 +2256,7 @@ def _extract_flags(line: str) -> tuple[str, list[str]]:
|
|
|
2235
2256
|
i = 0
|
|
2236
2257
|
while i < len(parts):
|
|
2237
2258
|
tok = parts[i]
|
|
2238
|
-
if tok in ("--data", "--universe", "--project", "--symbol") and i + 1 < len(parts):
|
|
2259
|
+
if tok in ("--data", "--universe", "--project", "--symbol", "--thesis") and i + 1 < len(parts):
|
|
2239
2260
|
flags += [tok, parts[i + 1]]
|
|
2240
2261
|
i += 2
|
|
2241
2262
|
continue
|
|
@@ -2357,6 +2378,58 @@ def _stored_universes(session: Any, question: str = "") -> tuple[str | None, lis
|
|
|
2357
2378
|
return (ready[0] if len(ready) == 1 else None), ready
|
|
2358
2379
|
|
|
2359
2380
|
|
|
2381
|
+
def thesis_label(row: dict[str, Any]) -> str:
|
|
2382
|
+
"""The name a person types. The id is what the run carries."""
|
|
2383
|
+
return str(row.get("name") or row.get("title") or row.get("id") or "").strip()
|
|
2384
|
+
|
|
2385
|
+
|
|
2386
|
+
def thesis_text(row: dict[str, Any]) -> str:
|
|
2387
|
+
"""The statement, under whichever key the portal sent it."""
|
|
2388
|
+
for key in ("statement", "summary", "body", "text"):
|
|
2389
|
+
value = row.get(key)
|
|
2390
|
+
if isinstance(value, str) and value.strip():
|
|
2391
|
+
return value.strip()
|
|
2392
|
+
return ""
|
|
2393
|
+
|
|
2394
|
+
|
|
2395
|
+
def thesis_id_of(row: dict[str, Any]) -> str:
|
|
2396
|
+
return str(row.get("id") or row.get("thesis_id") or "").strip()
|
|
2397
|
+
|
|
2398
|
+
|
|
2399
|
+
def match_thesis(rows: list[Any], query: str) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]:
|
|
2400
|
+
"""One thesis, or the candidates when the name matches more than one.
|
|
2401
|
+
|
|
2402
|
+
Exact name or id wins. A unique substring is enough. Several matches are
|
|
2403
|
+
returned so the caller can name them instead of guessing.
|
|
2404
|
+
"""
|
|
2405
|
+
usable = [r for r in rows if isinstance(r, dict)]
|
|
2406
|
+
q = query.strip().casefold()
|
|
2407
|
+
exact = [r for r in usable if thesis_label(r).casefold() == q or thesis_id_of(r).casefold() == q]
|
|
2408
|
+
if len(exact) == 1:
|
|
2409
|
+
return exact[0], []
|
|
2410
|
+
if len(exact) > 1:
|
|
2411
|
+
return None, exact
|
|
2412
|
+
partial = [r for r in usable if q and q in thesis_label(r).casefold()]
|
|
2413
|
+
if len(partial) == 1:
|
|
2414
|
+
return partial[0], []
|
|
2415
|
+
return None, partial
|
|
2416
|
+
|
|
2417
|
+
|
|
2418
|
+
def pop_flag(flags: list[str], name: str) -> tuple[str | None, list[str]]:
|
|
2419
|
+
"""Take one ``--name value`` out of a flag list. The rest stay in order."""
|
|
2420
|
+
kept: list[str] = []
|
|
2421
|
+
value: str | None = None
|
|
2422
|
+
i = 0
|
|
2423
|
+
while i < len(flags):
|
|
2424
|
+
if flags[i] == name and i + 1 < len(flags):
|
|
2425
|
+
value = flags[i + 1]
|
|
2426
|
+
i += 2
|
|
2427
|
+
continue
|
|
2428
|
+
kept.append(flags[i])
|
|
2429
|
+
i += 1
|
|
2430
|
+
return value, kept
|
|
2431
|
+
|
|
2432
|
+
|
|
2360
2433
|
def _split_run(rest: str) -> tuple[str, list[str]]:
|
|
2361
2434
|
"""`screen_universe --universe sp100` -> ("screen_universe", [...]).
|
|
2362
2435
|
|
|
@@ -3154,6 +3227,51 @@ def _is_workflow(session: Any, name: str) -> bool:
|
|
|
3154
3227
|
return False
|
|
3155
3228
|
|
|
3156
3229
|
|
|
3230
|
+
def _workflow_answer(row: dict[str, Any]) -> str:
|
|
3231
|
+
"""What this workflow answers, from the server when it says, else the local sentence."""
|
|
3232
|
+
for key in ("answers", "purpose", "description", "summary"):
|
|
3233
|
+
value = row.get(key)
|
|
3234
|
+
if isinstance(value, str) and value.strip():
|
|
3235
|
+
return value.strip()
|
|
3236
|
+
return _WORKFLOW_ANSWERS.get(str(row.get("name") or ""), "")
|
|
3237
|
+
|
|
3238
|
+
|
|
3239
|
+
def _catalogue_option(row: dict[str, Any]) -> dict[str, Any]:
|
|
3240
|
+
"""One catalogue row, as a choice the model can tell from the others.
|
|
3241
|
+
|
|
3242
|
+
The name alone is why every question was landing on the same workflow.
|
|
3243
|
+
The sentence, what it needs, and whether it repeats are the distinction.
|
|
3244
|
+
"""
|
|
3245
|
+
name = str(row.get("name") or "?")
|
|
3246
|
+
params: dict[str, Any] = {}
|
|
3247
|
+
answers = _workflow_answer(row)
|
|
3248
|
+
if answers:
|
|
3249
|
+
params["answers"] = answers[:180]
|
|
3250
|
+
needs = [str(item) for item in (row.get("requires") or []) if str(item)]
|
|
3251
|
+
if needs:
|
|
3252
|
+
params["needs"] = ", ".join(needs[:6])
|
|
3253
|
+
agency = row.get("agency")
|
|
3254
|
+
if agency:
|
|
3255
|
+
params["agency"] = str(agency)
|
|
3256
|
+
if row.get("reproducible") is True:
|
|
3257
|
+
params["reproducible"] = "yes"
|
|
3258
|
+
elif row.get("reproducible") is False:
|
|
3259
|
+
params["reproducible"] = "no"
|
|
3260
|
+
return {"op": name, "params": params, "answers": answers}
|
|
3261
|
+
|
|
3262
|
+
|
|
3263
|
+
def _prior_line(run: Any) -> str:
|
|
3264
|
+
"""One line of what the desk already ran, so the next question is not blind."""
|
|
3265
|
+
from .client.agent import summarize_figures
|
|
3266
|
+
|
|
3267
|
+
workflow = str((getattr(run, "artifact", None) or {}).get("workflow") or "the last run")
|
|
3268
|
+
line = f"{workflow} is {getattr(run, 'status', '')}."
|
|
3269
|
+
measured = summarize_figures(getattr(run, "figures", None))
|
|
3270
|
+
if measured != "(nothing yet)":
|
|
3271
|
+
line = f"{line} {' '.join(measured.split())[:240]}"
|
|
3272
|
+
return line
|
|
3273
|
+
|
|
3274
|
+
|
|
3157
3275
|
def _ask(
|
|
3158
3276
|
session: Any,
|
|
3159
3277
|
url: str,
|
|
@@ -3162,6 +3280,10 @@ def _ask(
|
|
|
3162
3280
|
data: Any,
|
|
3163
3281
|
backtest_fn: Any,
|
|
3164
3282
|
workspace_id: str | None = None,
|
|
3283
|
+
thesis_id: str | None = None,
|
|
3284
|
+
thesis_name: str | None = None,
|
|
3285
|
+
thesis_statement: str | None = None,
|
|
3286
|
+
prior: str | None = None,
|
|
3165
3287
|
think: Callable[[str], str] | None = None,
|
|
3166
3288
|
) -> Any:
|
|
3167
3289
|
"""Natural language → a workflow chosen and driven by the user's own model.
|
|
@@ -3173,6 +3295,7 @@ def _ask(
|
|
|
3173
3295
|
without being unsafe.
|
|
3174
3296
|
"""
|
|
3175
3297
|
from .client import AgentDriver, AgentRefusal, Offline, ServerError
|
|
3298
|
+
from .client.agent import prefer_index
|
|
3176
3299
|
from .events import EventSink
|
|
3177
3300
|
from .model import NoModelConfigured, build_think
|
|
3178
3301
|
|
|
@@ -3211,23 +3334,39 @@ def _ask(
|
|
|
3211
3334
|
say(dim("The server offers no workflows, so there is nothing to choose from."))
|
|
3212
3335
|
return None
|
|
3213
3336
|
|
|
3214
|
-
say(f" {
|
|
3337
|
+
say(f" model {dim(label)}")
|
|
3338
|
+
if thesis_name:
|
|
3339
|
+
say(dim(f" thesis {thesis_name}"))
|
|
3340
|
+
if prior:
|
|
3341
|
+
say(dim(" prior " + prior.splitlines()[0][:88]))
|
|
3342
|
+
goal = request
|
|
3343
|
+
if thesis_statement:
|
|
3344
|
+
goal = f"{request}\n\nThesis:\n{thesis_statement}"
|
|
3345
|
+
if prior:
|
|
3346
|
+
goal = f"{goal}\n\nAlready on the desk:\n{prior}"
|
|
3215
3347
|
|
|
3216
3348
|
# Choosing the workflow is the same shape as choosing a step: an index into
|
|
3217
3349
|
# a list the server produced. Reusing `AgentDriver.pick` rather than writing
|
|
3218
3350
|
# a second selector keeps one place where a model's answer is validated.
|
|
3219
3351
|
driver = AgentDriver(
|
|
3220
3352
|
think,
|
|
3221
|
-
goal=
|
|
3222
|
-
on_thought=lambda why: say(f" {
|
|
3353
|
+
goal=goal,
|
|
3354
|
+
on_thought=lambda why: say(f" why {why}"),
|
|
3355
|
+
on_step=lambda line: say(f" {line}"),
|
|
3223
3356
|
sink=sink,
|
|
3224
3357
|
provider=provider or None,
|
|
3225
3358
|
model=model_name or None,
|
|
3226
3359
|
)
|
|
3227
|
-
options = [
|
|
3360
|
+
options = [_catalogue_option(w) for w in catalogue]
|
|
3228
3361
|
try:
|
|
3362
|
+
say(dim(" reading the catalogue"))
|
|
3229
3363
|
with Working(dim("reading the catalogue"), plain="reading the catalogue"):
|
|
3230
|
-
|
|
3364
|
+
matched = prefer_index(request, options)
|
|
3365
|
+
if matched is None:
|
|
3366
|
+
index = driver.pick(options)
|
|
3367
|
+
else:
|
|
3368
|
+
index = matched
|
|
3369
|
+
say(dim(" matched the question uses this workflow's own words"))
|
|
3231
3370
|
chosen = catalogue[index]
|
|
3232
3371
|
except AgentRefusal as exc:
|
|
3233
3372
|
say(red(f"The model did not choose a workflow: {exc}"))
|
|
@@ -3286,10 +3425,7 @@ def _ask(
|
|
|
3286
3425
|
return None
|
|
3287
3426
|
|
|
3288
3427
|
repro = chosen.get("reproducible")
|
|
3289
|
-
say(
|
|
3290
|
-
f" {_accent(ON)} chose {bold(name)} "
|
|
3291
|
-
+ (green("reproducible") if repro else yellow("not reproducible"))
|
|
3292
|
-
)
|
|
3428
|
+
say(f" workflow {bold(name)} " + (green("reproducible") if repro else yellow("exploratory")))
|
|
3293
3429
|
|
|
3294
3430
|
try:
|
|
3295
3431
|
run = session.open(
|
|
@@ -3297,6 +3433,7 @@ def _ask(
|
|
|
3297
3433
|
data=data,
|
|
3298
3434
|
backtest_fn=backtest_fn,
|
|
3299
3435
|
workspace_id=workspace_id,
|
|
3436
|
+
thesis_id=thesis_id,
|
|
3300
3437
|
**({"grid": project_grid()} if project_grid() else {}),
|
|
3301
3438
|
)
|
|
3302
3439
|
sink.bind(run.run_id)
|
|
@@ -3311,6 +3448,7 @@ def _ask(
|
|
|
3311
3448
|
say(red(f"The run stopped: {exc}"))
|
|
3312
3449
|
return None
|
|
3313
3450
|
|
|
3451
|
+
say(dim(" result"))
|
|
3314
3452
|
_report(run)
|
|
3315
3453
|
if run.status == "closed":
|
|
3316
3454
|
_publish_signals(session, run, workspace_id=workspace_id, label=None)
|
|
@@ -3520,6 +3658,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3520
3658
|
r = sub.add_parser("run", parents=[common], help="run one workflow to completion")
|
|
3521
3659
|
r.add_argument("workflow")
|
|
3522
3660
|
r.add_argument("--label", help="what to call the artifact")
|
|
3661
|
+
r.add_argument("--thesis", help="name or id of a thesis on this account; attached to the run")
|
|
3523
3662
|
r.add_argument("--input", action="append", help="workflow input as key=value (repeatable)")
|
|
3524
3663
|
r.add_argument("--quiet", action="store_true", help="only the result")
|
|
3525
3664
|
r.add_argument("--stream", action="store_true", help="print the answer after the citation guard")
|
|
@@ -3532,6 +3671,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3532
3671
|
for verb, workflow in QUESTION_VERBS.items():
|
|
3533
3672
|
q = sub.add_parser(verb, parents=[common], help=f"run {workflow}")
|
|
3534
3673
|
q.add_argument("--label", help="what to call the artifact")
|
|
3674
|
+
q.add_argument("--thesis", help="name or id of a thesis on this account; attached to the run")
|
|
3535
3675
|
q.add_argument("--input", action="append", help="workflow input as key=value (repeatable)")
|
|
3536
3676
|
q.add_argument("--quiet", action="store_true", help="only the result")
|
|
3537
3677
|
q.add_argument("--stream", action="store_true", help="print the answer after the citation guard")
|