alpha-engine-lib 0.36.0__tar.gz → 0.37.0__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.
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/PKG-INFO +2 -2
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/pyproject.toml +2 -2
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/__init__.py +1 -1
- alpha_engine_lib-0.37.0/src/alpha_engine_lib/anthropic_payload.py +217 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/pipeline_status/registry.py +4 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib.egg-info/PKG-INFO +2 -2
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib.egg-info/SOURCES.txt +2 -0
- alpha_engine_lib-0.37.0/tests/test_anthropic_payload.py +273 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/README.md +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/setup.cfg +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/agent_schemas.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/alerts.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/arcticdb.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/collector_results.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/cost.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/dates.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/decision_capture.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/ec2_spot.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/email_sender.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/eval_artifacts.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/logging.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/model_pricing.yaml +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/pillars.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/pipeline_status/__init__.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/pipeline_status/read.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/pipeline_status/templates.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/preflight.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/rag/__init__.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/rag/db.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/rag/embeddings.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/rag/migrations/0001_content_tsv.sql +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/rag/rerank.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/rag/retrieval.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/rag/schema.sql +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/reconcile.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/secrets.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/sources/__init__.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/sources/protocols.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/ssm_dispatcher.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/ssm_log_capture.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/telegram.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/trading_calendar.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/transparency.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/transparency_inventory.yaml +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/universe.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib.egg-info/dependency_links.txt +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib.egg-info/requires.txt +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib.egg-info/top_level.txt +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_agent_schemas.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_alerts.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_arcticdb.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_collector_results.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_cost.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_dates.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_decision_capture.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_ec2_spot.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_email_sender.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_eval_artifacts.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_logging.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_pillars.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_pipeline_status_read.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_pipeline_status_registry.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_pipeline_status_templates.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_preflight.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_rag.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_rag_rerank.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_rag_retrieval_hybrid.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_reconcile.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_secrets.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_sources_protocols.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_ssm_dispatcher.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_ssm_log_capture.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_telegram.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_trading_calendar.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_transparency.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_universe.py +0 -0
- {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_version_pin.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpha-engine-lib
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Shared utilities for the Alpha Engine modules: preflight, structured logging with secret-redaction, ArcticDB universe access, NYSE-calendar dates + freshness predicates, decision capture, cost telemetry, RAG, agent output schemas, SSM-backed secrets, Telegram alerts + SNS fan-out, EC2 spot-launch resilience, SSM log-capture chokepoint, SSM send-command + poll chokepoint, and Step-Functions execution-state projection. Full surface documented in README.
|
|
3
|
+
Version: 0.37.0
|
|
4
|
+
Summary: Shared utilities for the Alpha Engine modules: preflight, structured logging with secret-redaction, ArcticDB universe access, NYSE-calendar dates + freshness predicates, decision capture, cost telemetry, Anthropic payload chokepoint, RAG, agent output schemas, SSM-backed secrets, Telegram alerts + SNS fan-out, EC2 spot-launch resilience, SSM log-capture chokepoint, SSM send-command + poll chokepoint, and Step-Functions execution-state projection. Full surface documented in README.
|
|
5
5
|
Author: Brian McMahon
|
|
6
6
|
License: Proprietary
|
|
7
7
|
Requires-Python: >=3.9
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "alpha-engine-lib"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Shared utilities for the Alpha Engine modules: preflight, structured logging with secret-redaction, ArcticDB universe access, NYSE-calendar dates + freshness predicates, decision capture, cost telemetry, RAG, agent output schemas, SSM-backed secrets, Telegram alerts + SNS fan-out, EC2 spot-launch resilience, SSM log-capture chokepoint, SSM send-command + poll chokepoint, and Step-Functions execution-state projection. Full surface documented in README."
|
|
7
|
+
version = "0.37.0"
|
|
8
|
+
description = "Shared utilities for the Alpha Engine modules: preflight, structured logging with secret-redaction, ArcticDB universe access, NYSE-calendar dates + freshness predicates, decision capture, cost telemetry, Anthropic payload chokepoint, RAG, agent output schemas, SSM-backed secrets, Telegram alerts + SNS fan-out, EC2 spot-launch resilience, SSM log-capture chokepoint, SSM send-command + poll chokepoint, and Step-Functions execution-state projection. Full surface documented in README."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
# EC2 still runs Python 3.9 on the always-on micro instance (boto3 drops
|
|
11
11
|
# 3.9 support 2026-04-29, so upgrade is on the near-term roadmap). All
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Anthropic ``messages.create()`` payload-construction chokepoint.
|
|
3
|
+
|
|
4
|
+
Consolidation substrate for the raw-Anthropic-SDK call shape that
|
|
5
|
+
multiple consumer repos now ship. First adopter is morning-signal
|
|
6
|
+
(``src/morning_signal/claude.py``); alpha-engine-research is the future
|
|
7
|
+
second raw-SDK adopter once the LangChain wrappers retire. Per the
|
|
8
|
+
``[[feedback_lift_invariants_to_chokepoint_after_second_recurrence]]``
|
|
9
|
+
discipline and the alpha-engine SOTA sub-sub-rule (mirror a pattern
|
|
10
|
+
across repos → lift to lib), this module bakes the known-good payload
|
|
11
|
+
shape + invariant validation into one place.
|
|
12
|
+
|
|
13
|
+
**Why this exists.** 2026-05-26 morning-signal incident: the 5/25-night
|
|
14
|
+
PR #33 (prompt caching + ``web_search max_uses`` cap) shipped on top
|
|
15
|
+
of the historical ``{role: "assistant", content: prefill}`` opener-pin.
|
|
16
|
+
The combination of ``web_search`` (any server-side tool) with a
|
|
17
|
+
trailing assistant message is rejected by the Anthropic API with HTTP
|
|
18
|
+
400::
|
|
19
|
+
|
|
20
|
+
"This model does not support assistant message prefill.
|
|
21
|
+
The conversation must end with a user message."
|
|
22
|
+
|
|
23
|
+
Two consecutive cron firings (5/25 PM at 00:00 UTC, 5/26 AM at 12:00
|
|
24
|
+
UTC) failed silently before the operator noticed. The producer-side
|
|
25
|
+
``_validate_request_payload`` chokepoint in morning-signal was the
|
|
26
|
+
local fix; this module is the lib lift so the next raw-SDK consumer
|
|
27
|
+
inherits the invariant without re-discovering it the hard way.
|
|
28
|
+
|
|
29
|
+
**Composes with:**
|
|
30
|
+
|
|
31
|
+
- :mod:`alpha_engine_lib.cost` — :func:`cost.metadata_from_anthropic_message`
|
|
32
|
+
is the canonical adapter for converting a returned ``Message`` into
|
|
33
|
+
a ``ModelMetadata`` cost-telemetry record. This module is the
|
|
34
|
+
outbound counterpart (request side); ``cost`` is the inbound side
|
|
35
|
+
(response side).
|
|
36
|
+
|
|
37
|
+
**Public surface:**
|
|
38
|
+
|
|
39
|
+
- :data:`SERVER_TOOL_PREFIXES` — type-prefix tuple for Anthropic
|
|
40
|
+
server-side tool definitions that share the "tool loop ends on
|
|
41
|
+
user message" constraint.
|
|
42
|
+
- :data:`DEFAULT_WEB_SEARCH_MAX_USES` — runaway-cost insurance cap
|
|
43
|
+
default; lifted from morning-signal PR #33.
|
|
44
|
+
- :func:`build_messages_payload` — construct the kwargs dict to splat
|
|
45
|
+
into ``client.messages.create(**payload)``. Always validates before
|
|
46
|
+
returning.
|
|
47
|
+
- :func:`validate_payload` — pure invariant check against a constructed
|
|
48
|
+
payload. Raises :exc:`ValueError` on known-incompatible shapes.
|
|
49
|
+
- :func:`build_web_search_tool` — convenience builder for the
|
|
50
|
+
``web_search_20250305`` tool spec with the runaway-cost cap default.
|
|
51
|
+
- :exc:`PayloadInvariantError` — subclass of ``ValueError`` raised by
|
|
52
|
+
:func:`validate_payload`. Distinct type so callers can catch payload
|
|
53
|
+
bugs separately from other ValueErrors.
|
|
54
|
+
|
|
55
|
+
**Anti-pattern this module forbids:** combining any server-side tool
|
|
56
|
+
(``web_search_*``, ``computer_use_*``, ``bash_*``, ``text_editor_*``)
|
|
57
|
+
with a conversation whose final ``messages[-1].role == "assistant"``.
|
|
58
|
+
The tool-loop semantics require the conversation to alternate ending
|
|
59
|
+
on a user / tool_result turn so the model can decide whether to emit
|
|
60
|
+
another tool_use block before final text.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
from __future__ import annotations
|
|
64
|
+
|
|
65
|
+
from typing import Any
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Anthropic server-side tool type prefixes. Each of these tool types
|
|
69
|
+
# triggers Anthropic's server-side tool-use loop, which requires the
|
|
70
|
+
# conversation to end on a user (or tool_result) turn so the model can
|
|
71
|
+
# decide whether to emit another tool_use block before final text.
|
|
72
|
+
# Combining any of these with a trailing assistant message (prefill)
|
|
73
|
+
# returns HTTP 400 "This model does not support assistant message
|
|
74
|
+
# prefill." Verified against the 2026-05-26 morning-signal incident.
|
|
75
|
+
SERVER_TOOL_PREFIXES: tuple[str, ...] = (
|
|
76
|
+
"web_search_",
|
|
77
|
+
"computer_use_",
|
|
78
|
+
"bash_",
|
|
79
|
+
"text_editor_",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Runaway-cost insurance on ``web_search_20250305``. Anthropic bills
|
|
83
|
+
# ``web_search`` at $10/1k requests; an uncapped spec lets a malformed
|
|
84
|
+
# prompt or model-loop bug rack up unbounded fees. 20 sits above
|
|
85
|
+
# morning-signal's empirical typical (~15 across the 9-segment briefing)
|
|
86
|
+
# so it functions as insurance not throttling. Lifted from
|
|
87
|
+
# morning-signal PR #33.
|
|
88
|
+
DEFAULT_WEB_SEARCH_MAX_USES: int = 20
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class PayloadInvariantError(ValueError):
|
|
92
|
+
"""Raised by :func:`validate_payload` on a known-incompatible
|
|
93
|
+
Anthropic ``messages.create()`` request shape. Subclass of
|
|
94
|
+
:class:`ValueError` so existing ``except ValueError`` callers still
|
|
95
|
+
catch it; distinct type so a caller that cares specifically about
|
|
96
|
+
payload bugs can catch this without swallowing other ValueErrors.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _has_server_tool(tools: list[dict] | None) -> bool:
|
|
101
|
+
if not tools:
|
|
102
|
+
return False
|
|
103
|
+
return any(
|
|
104
|
+
any(t.get("type", "").startswith(p) for p in SERVER_TOOL_PREFIXES)
|
|
105
|
+
for t in tools
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def validate_payload(payload: dict[str, Any]) -> None:
|
|
110
|
+
"""Raise :exc:`PayloadInvariantError` on a known-incompatible
|
|
111
|
+
Anthropic ``messages.create()`` payload shape.
|
|
112
|
+
|
|
113
|
+
Currently enforced invariants:
|
|
114
|
+
|
|
115
|
+
1. **Server-tool ⊥ assistant-prefill.** If ``payload["tools"]``
|
|
116
|
+
contains any type with a :data:`SERVER_TOOL_PREFIXES` prefix
|
|
117
|
+
AND ``payload["messages"][-1]["role"] == "assistant"``,
|
|
118
|
+
Anthropic returns HTTP 400. Surfaced 2026-05-26.
|
|
119
|
+
|
|
120
|
+
The validator is a producer-side chokepoint: failing here at
|
|
121
|
+
construction time means the bug class can't reach a production
|
|
122
|
+
cron firing.
|
|
123
|
+
"""
|
|
124
|
+
messages = payload.get("messages") or []
|
|
125
|
+
tools = payload.get("tools") or []
|
|
126
|
+
|
|
127
|
+
if _has_server_tool(tools):
|
|
128
|
+
last_role = messages[-1]["role"] if messages else None
|
|
129
|
+
if last_role == "assistant":
|
|
130
|
+
raise PayloadInvariantError(
|
|
131
|
+
"Anthropic payload invariant violated: server-side tools "
|
|
132
|
+
"(types prefixed with any of "
|
|
133
|
+
f"{SERVER_TOOL_PREFIXES}) cannot be combined with a "
|
|
134
|
+
"trailing assistant message (prefill). The API rejects "
|
|
135
|
+
"this with HTTP 400 'This model does not support "
|
|
136
|
+
"assistant message prefill. The conversation must end "
|
|
137
|
+
"with a user message.' Either drop the prefill or drop "
|
|
138
|
+
"the server tool."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def build_web_search_tool(
|
|
143
|
+
*,
|
|
144
|
+
max_uses: int = DEFAULT_WEB_SEARCH_MAX_USES,
|
|
145
|
+
name: str = "web_search",
|
|
146
|
+
) -> dict[str, Any]:
|
|
147
|
+
"""Build the ``web_search_20250305`` tool spec with the runaway-cost
|
|
148
|
+
cap. ``max_uses`` defaults to :data:`DEFAULT_WEB_SEARCH_MAX_USES`.
|
|
149
|
+
"""
|
|
150
|
+
return {
|
|
151
|
+
"type": "web_search_20250305",
|
|
152
|
+
"name": name,
|
|
153
|
+
"max_uses": max_uses,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def build_messages_payload(
|
|
158
|
+
*,
|
|
159
|
+
model: str,
|
|
160
|
+
system_prompt: str,
|
|
161
|
+
user_content: str,
|
|
162
|
+
max_tokens: int,
|
|
163
|
+
tools: list[dict] | None = None,
|
|
164
|
+
cache_system: bool = True,
|
|
165
|
+
extra: dict[str, Any] | None = None,
|
|
166
|
+
) -> dict[str, Any]:
|
|
167
|
+
"""Construct a validated kwargs dict for ``client.messages.create()``.
|
|
168
|
+
|
|
169
|
+
Returns a dict the caller splats into the SDK:
|
|
170
|
+
|
|
171
|
+
payload = build_messages_payload(...)
|
|
172
|
+
response = client.messages.create(**payload)
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
model: Anthropic model identifier (e.g. ``"claude-sonnet-4-5"``).
|
|
176
|
+
system_prompt: The static system-prompt text. Sent as a single
|
|
177
|
+
``system`` block; when ``cache_system=True`` (default) the
|
|
178
|
+
block carries ``cache_control: {"type": "ephemeral"}`` so
|
|
179
|
+
the prefix is cached at the 0.1× cache-read rate on every
|
|
180
|
+
tool-loop re-read within one ``messages.create()`` call.
|
|
181
|
+
user_content: The dynamic per-call user-message content
|
|
182
|
+
(typically date + edition + any per-call instructions).
|
|
183
|
+
Lives in the user message rather than the cached system
|
|
184
|
+
block so the static prefix stays per-call cacheable.
|
|
185
|
+
max_tokens: ``max_tokens`` for the call.
|
|
186
|
+
tools: Optional list of tool specs. May include server-side
|
|
187
|
+
tools (``web_search_20250305`` etc.) — :func:`validate_payload`
|
|
188
|
+
enforces the server-tool ⊥ prefill invariant.
|
|
189
|
+
cache_system: When ``True`` (default) attach ephemeral
|
|
190
|
+
``cache_control`` to the ``system`` block. Pass ``False``
|
|
191
|
+
for one-shot calls where caching has no return.
|
|
192
|
+
extra: Optional dict merged into the result (e.g. ``stop_sequences``,
|
|
193
|
+
``temperature``, ``metadata``). Validation runs AFTER the
|
|
194
|
+
merge so any extras that affect ``messages`` / ``tools``
|
|
195
|
+
are checked too.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Validated kwargs dict. Raises :exc:`PayloadInvariantError` on a
|
|
199
|
+
known-incompatible shape.
|
|
200
|
+
"""
|
|
201
|
+
system_block: dict[str, Any] = {"type": "text", "text": system_prompt}
|
|
202
|
+
if cache_system:
|
|
203
|
+
system_block["cache_control"] = {"type": "ephemeral"}
|
|
204
|
+
|
|
205
|
+
payload: dict[str, Any] = {
|
|
206
|
+
"model": model,
|
|
207
|
+
"max_tokens": max_tokens,
|
|
208
|
+
"system": [system_block],
|
|
209
|
+
"messages": [{"role": "user", "content": user_content}],
|
|
210
|
+
}
|
|
211
|
+
if tools:
|
|
212
|
+
payload["tools"] = list(tools)
|
|
213
|
+
if extra:
|
|
214
|
+
payload.update(extra)
|
|
215
|
+
|
|
216
|
+
validate_payload(payload)
|
|
217
|
+
return payload
|
{alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/pipeline_status/registry.py
RENAMED
|
@@ -242,6 +242,10 @@ STATE_TO_ARCHIVE_PAGE: Final[dict[str, Union[ArchivePageRef, ArtifactReason]]] =
|
|
|
242
242
|
reason="Counterfactual artifact written to backtest/{date}/; surfaced "
|
|
243
243
|
"inline in Backtester evaluator report (page 21)."
|
|
244
244
|
),
|
|
245
|
+
"AggregateCosts": ArchivePageRef(
|
|
246
|
+
page="23_LLM_Cost",
|
|
247
|
+
artifact_label="LLM cost telemetry (daily aggregate)",
|
|
248
|
+
),
|
|
245
249
|
"PredictorTraining": ArchivePageRef(
|
|
246
250
|
page="20_Predictor_Training_Archive",
|
|
247
251
|
artifact_label="Predictor training summary",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpha-engine-lib
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Shared utilities for the Alpha Engine modules: preflight, structured logging with secret-redaction, ArcticDB universe access, NYSE-calendar dates + freshness predicates, decision capture, cost telemetry, RAG, agent output schemas, SSM-backed secrets, Telegram alerts + SNS fan-out, EC2 spot-launch resilience, SSM log-capture chokepoint, SSM send-command + poll chokepoint, and Step-Functions execution-state projection. Full surface documented in README.
|
|
3
|
+
Version: 0.37.0
|
|
4
|
+
Summary: Shared utilities for the Alpha Engine modules: preflight, structured logging with secret-redaction, ArcticDB universe access, NYSE-calendar dates + freshness predicates, decision capture, cost telemetry, Anthropic payload chokepoint, RAG, agent output schemas, SSM-backed secrets, Telegram alerts + SNS fan-out, EC2 spot-launch resilience, SSM log-capture chokepoint, SSM send-command + poll chokepoint, and Step-Functions execution-state projection. Full surface documented in README.
|
|
5
5
|
Author: Brian McMahon
|
|
6
6
|
License: Proprietary
|
|
7
7
|
Requires-Python: >=3.9
|
{alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib.egg-info/SOURCES.txt
RENAMED
|
@@ -3,6 +3,7 @@ pyproject.toml
|
|
|
3
3
|
src/alpha_engine_lib/__init__.py
|
|
4
4
|
src/alpha_engine_lib/agent_schemas.py
|
|
5
5
|
src/alpha_engine_lib/alerts.py
|
|
6
|
+
src/alpha_engine_lib/anthropic_payload.py
|
|
6
7
|
src/alpha_engine_lib/arcticdb.py
|
|
7
8
|
src/alpha_engine_lib/collector_results.py
|
|
8
9
|
src/alpha_engine_lib/cost.py
|
|
@@ -44,6 +45,7 @@ src/alpha_engine_lib/sources/__init__.py
|
|
|
44
45
|
src/alpha_engine_lib/sources/protocols.py
|
|
45
46
|
tests/test_agent_schemas.py
|
|
46
47
|
tests/test_alerts.py
|
|
48
|
+
tests/test_anthropic_payload.py
|
|
47
49
|
tests/test_arcticdb.py
|
|
48
50
|
tests/test_collector_results.py
|
|
49
51
|
tests/test_cost.py
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for ``alpha_engine_lib.anthropic_payload``.
|
|
3
|
+
|
|
4
|
+
Pins the institutional-chokepoint contract for raw-Anthropic-SDK
|
|
5
|
+
payload construction. Surfaced as a lib lift after the 2026-05-26
|
|
6
|
+
morning-signal incident where the historical
|
|
7
|
+
``{role: "assistant", content: prefill}`` opener-pin was combined with
|
|
8
|
+
the ``web_search_20250305`` server tool, producing two consecutive
|
|
9
|
+
silent HTTP 400 cron-firing failures before the operator noticed.
|
|
10
|
+
|
|
11
|
+
* Validator MUST raise on (server-tool + trailing assistant message)
|
|
12
|
+
for every server-tool prefix in ``SERVER_TOOL_PREFIXES``.
|
|
13
|
+
* Validator MUST NOT raise on (server-tool alone) or (prefill alone).
|
|
14
|
+
* ``build_messages_payload`` MUST return a payload that validates
|
|
15
|
+
cleanly AND has the cached system block + the user message + the
|
|
16
|
+
optional tools, in the exact shape ``messages.create()`` expects.
|
|
17
|
+
* ``build_web_search_tool`` MUST default to
|
|
18
|
+
:data:`DEFAULT_WEB_SEARCH_MAX_USES` so consumers can't silently lose
|
|
19
|
+
the runaway-cost cap.
|
|
20
|
+
|
|
21
|
+
See ``[[feedback_no_silent_fails]]`` + the alpha-engine SOTA
|
|
22
|
+
sub-sub-rule (second-adoption signal → lift to lib).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import pytest
|
|
28
|
+
|
|
29
|
+
from alpha_engine_lib.anthropic_payload import (
|
|
30
|
+
DEFAULT_WEB_SEARCH_MAX_USES,
|
|
31
|
+
SERVER_TOOL_PREFIXES,
|
|
32
|
+
PayloadInvariantError,
|
|
33
|
+
build_messages_payload,
|
|
34
|
+
build_web_search_tool,
|
|
35
|
+
validate_payload,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ── validate_payload — server-tool ⊥ assistant-prefill ───────────────────────
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.parametrize(
|
|
43
|
+
"tool_type",
|
|
44
|
+
[
|
|
45
|
+
"web_search_20250305",
|
|
46
|
+
"computer_use_20250124",
|
|
47
|
+
"bash_20250124",
|
|
48
|
+
"text_editor_20250124",
|
|
49
|
+
],
|
|
50
|
+
)
|
|
51
|
+
def test_validate_rejects_server_tool_with_trailing_assistant(tool_type):
|
|
52
|
+
"""The 2026-05-26 regression class: any server-side tool combined
|
|
53
|
+
with a trailing assistant message (prefill) returns HTTP 400. The
|
|
54
|
+
validator catches it at the producer site so the failure can never
|
|
55
|
+
reach a 5 AM cron firing."""
|
|
56
|
+
payload = {
|
|
57
|
+
"model": "claude-sonnet-4-5",
|
|
58
|
+
"max_tokens": 100,
|
|
59
|
+
"tools": [{"type": tool_type, "name": "t"}],
|
|
60
|
+
"messages": [
|
|
61
|
+
{"role": "user", "content": "hi"},
|
|
62
|
+
{"role": "assistant", "content": "Welcome"},
|
|
63
|
+
],
|
|
64
|
+
}
|
|
65
|
+
with pytest.raises(PayloadInvariantError, match="server-side tools"):
|
|
66
|
+
validate_payload(payload)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_payload_invariant_error_is_value_error():
|
|
70
|
+
"""Existing ``except ValueError`` callers MUST still catch payload
|
|
71
|
+
bugs — institutional default that subclasses of ``ValueError``
|
|
72
|
+
remain catchable as ValueError."""
|
|
73
|
+
assert issubclass(PayloadInvariantError, ValueError)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_validate_allows_server_tool_without_prefill():
|
|
77
|
+
payload = {
|
|
78
|
+
"model": "claude-sonnet-4-5",
|
|
79
|
+
"max_tokens": 100,
|
|
80
|
+
"tools": [{"type": "web_search_20250305", "name": "web_search"}],
|
|
81
|
+
"messages": [{"role": "user", "content": "hi"}],
|
|
82
|
+
}
|
|
83
|
+
validate_payload(payload)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_validate_allows_prefill_without_server_tool():
|
|
87
|
+
payload = {
|
|
88
|
+
"model": "claude-sonnet-4-5",
|
|
89
|
+
"max_tokens": 100,
|
|
90
|
+
"messages": [
|
|
91
|
+
{"role": "user", "content": "hi"},
|
|
92
|
+
{"role": "assistant", "content": "Y"},
|
|
93
|
+
],
|
|
94
|
+
}
|
|
95
|
+
validate_payload(payload)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_validate_allows_no_tools_no_prefill():
|
|
99
|
+
payload = {
|
|
100
|
+
"model": "claude-sonnet-4-5",
|
|
101
|
+
"max_tokens": 100,
|
|
102
|
+
"messages": [{"role": "user", "content": "hi"}],
|
|
103
|
+
}
|
|
104
|
+
validate_payload(payload)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_validate_treats_empty_tools_list_as_no_server_tools():
|
|
108
|
+
payload = {
|
|
109
|
+
"model": "claude-sonnet-4-5",
|
|
110
|
+
"max_tokens": 100,
|
|
111
|
+
"tools": [],
|
|
112
|
+
"messages": [
|
|
113
|
+
{"role": "user", "content": "hi"},
|
|
114
|
+
{"role": "assistant", "content": "Y"},
|
|
115
|
+
],
|
|
116
|
+
}
|
|
117
|
+
validate_payload(payload)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_validate_allows_non_server_tool_with_prefill():
|
|
121
|
+
"""Client-side tool definitions (no server-tool prefix) compose
|
|
122
|
+
fine with a trailing assistant message; only Anthropic's
|
|
123
|
+
server-side tool-use loop has the constraint."""
|
|
124
|
+
payload = {
|
|
125
|
+
"model": "claude-sonnet-4-5",
|
|
126
|
+
"max_tokens": 100,
|
|
127
|
+
"tools": [{"type": "custom_thing", "name": "x"}],
|
|
128
|
+
"messages": [
|
|
129
|
+
{"role": "user", "content": "hi"},
|
|
130
|
+
{"role": "assistant", "content": "Y"},
|
|
131
|
+
],
|
|
132
|
+
}
|
|
133
|
+
validate_payload(payload)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_server_tool_prefixes_is_immutable_tuple():
|
|
137
|
+
"""Constant MUST be a tuple, not a list — defends against
|
|
138
|
+
consumers patching the prefix set at runtime, which would silently
|
|
139
|
+
expand the validator's blast radius."""
|
|
140
|
+
assert isinstance(SERVER_TOOL_PREFIXES, tuple)
|
|
141
|
+
assert "web_search_" in SERVER_TOOL_PREFIXES
|
|
142
|
+
assert "computer_use_" in SERVER_TOOL_PREFIXES
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ── build_web_search_tool ────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_build_web_search_tool_defaults():
|
|
149
|
+
spec = build_web_search_tool()
|
|
150
|
+
assert spec["type"] == "web_search_20250305"
|
|
151
|
+
assert spec["name"] == "web_search"
|
|
152
|
+
assert spec["max_uses"] == DEFAULT_WEB_SEARCH_MAX_USES == 20
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_build_web_search_tool_max_uses_override():
|
|
156
|
+
spec = build_web_search_tool(max_uses=5)
|
|
157
|
+
assert spec["max_uses"] == 5
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_build_web_search_tool_custom_name():
|
|
161
|
+
spec = build_web_search_tool(name="custom_search")
|
|
162
|
+
assert spec["name"] == "custom_search"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ── build_messages_payload ───────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_build_messages_payload_shape_with_tools():
|
|
169
|
+
payload = build_messages_payload(
|
|
170
|
+
model="claude-sonnet-4-5",
|
|
171
|
+
system_prompt="static prompt",
|
|
172
|
+
user_content="dynamic preamble",
|
|
173
|
+
max_tokens=100,
|
|
174
|
+
tools=[build_web_search_tool()],
|
|
175
|
+
)
|
|
176
|
+
assert payload["model"] == "claude-sonnet-4-5"
|
|
177
|
+
assert payload["max_tokens"] == 100
|
|
178
|
+
# system block cached by default
|
|
179
|
+
assert payload["system"] == [
|
|
180
|
+
{
|
|
181
|
+
"type": "text",
|
|
182
|
+
"text": "static prompt",
|
|
183
|
+
"cache_control": {"type": "ephemeral"},
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
# single user message; no assistant prefill (would conflict with web_search)
|
|
187
|
+
assert payload["messages"] == [
|
|
188
|
+
{"role": "user", "content": "dynamic preamble"}
|
|
189
|
+
]
|
|
190
|
+
assert payload["tools"][0]["type"] == "web_search_20250305"
|
|
191
|
+
assert payload["tools"][0]["max_uses"] == 20
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_build_messages_payload_without_tools_omits_tools_key():
|
|
195
|
+
"""Anthropic SDK rejects ``tools=[]`` vs ``tools`` missing
|
|
196
|
+
differently in some model snapshots; safer to omit the key entirely
|
|
197
|
+
when there are no tools."""
|
|
198
|
+
payload = build_messages_payload(
|
|
199
|
+
model="claude-sonnet-4-5",
|
|
200
|
+
system_prompt="p",
|
|
201
|
+
user_content="u",
|
|
202
|
+
max_tokens=10,
|
|
203
|
+
)
|
|
204
|
+
assert "tools" not in payload
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_build_messages_payload_cache_system_false_omits_cache_control():
|
|
208
|
+
payload = build_messages_payload(
|
|
209
|
+
model="claude-sonnet-4-5",
|
|
210
|
+
system_prompt="p",
|
|
211
|
+
user_content="u",
|
|
212
|
+
max_tokens=10,
|
|
213
|
+
cache_system=False,
|
|
214
|
+
)
|
|
215
|
+
assert "cache_control" not in payload["system"][0]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_build_messages_payload_extra_kwargs_pass_through():
|
|
219
|
+
payload = build_messages_payload(
|
|
220
|
+
model="claude-sonnet-4-5",
|
|
221
|
+
system_prompt="p",
|
|
222
|
+
user_content="u",
|
|
223
|
+
max_tokens=10,
|
|
224
|
+
extra={"temperature": 0.7, "stop_sequences": ["\n\n"]},
|
|
225
|
+
)
|
|
226
|
+
assert payload["temperature"] == 0.7
|
|
227
|
+
assert payload["stop_sequences"] == ["\n\n"]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_build_messages_payload_validates_extra_that_breaks_invariant():
|
|
231
|
+
"""Validation runs AFTER the extra-merge so an ``extra`` dict that
|
|
232
|
+
smuggles in an assistant prefill alongside a server tool still
|
|
233
|
+
trips the invariant. This is the load-bearing guarantee — callers
|
|
234
|
+
cannot bypass the chokepoint by routing fields through ``extra``."""
|
|
235
|
+
with pytest.raises(PayloadInvariantError):
|
|
236
|
+
build_messages_payload(
|
|
237
|
+
model="claude-sonnet-4-5",
|
|
238
|
+
system_prompt="p",
|
|
239
|
+
user_content="u",
|
|
240
|
+
max_tokens=10,
|
|
241
|
+
tools=[build_web_search_tool()],
|
|
242
|
+
extra={
|
|
243
|
+
"messages": [
|
|
244
|
+
{"role": "user", "content": "hi"},
|
|
245
|
+
{"role": "assistant", "content": "Y"},
|
|
246
|
+
]
|
|
247
|
+
},
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_build_messages_payload_morning_signal_replication():
|
|
252
|
+
"""The exact production shape used by morning-signal post-fix.
|
|
253
|
+
Pins the canonical raw-SDK consumer pattern so a future repo
|
|
254
|
+
landing on this lib module gets a working template."""
|
|
255
|
+
opener = "Welcome to Morning Signal."
|
|
256
|
+
payload = build_messages_payload(
|
|
257
|
+
model="claude-sonnet-4-5",
|
|
258
|
+
system_prompt="# Morning Signal production prompt (~1.3K tokens of static text)",
|
|
259
|
+
user_content=(
|
|
260
|
+
"Today is Tuesday, May 26, 2026. This is the MORNING edition of Morning Signal. "
|
|
261
|
+
"Generate today's morning episode per the system prompt.\n\n"
|
|
262
|
+
f"Your response MUST begin verbatim with this exact line, "
|
|
263
|
+
f"with no preamble or acknowledgement before it:\n\n{opener}"
|
|
264
|
+
),
|
|
265
|
+
max_tokens=4096,
|
|
266
|
+
tools=[build_web_search_tool(max_uses=20)],
|
|
267
|
+
)
|
|
268
|
+
# Validator already ran inside build_messages_payload — assert the
|
|
269
|
+
# shape matches what messages.create() expects post-fix.
|
|
270
|
+
assert payload["system"][0]["cache_control"] == {"type": "ephemeral"}
|
|
271
|
+
assert payload["tools"][0]["max_uses"] == 20
|
|
272
|
+
assert len(payload["messages"]) == 1 # no assistant prefill
|
|
273
|
+
assert opener in payload["messages"][0]["content"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/collector_results.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/decision_capture.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/pipeline_status/__init__.py
RENAMED
|
File without changes
|
{alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/pipeline_status/read.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/sources/__init__.py
RENAMED
|
File without changes
|
{alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/sources/protocols.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/trading_calendar.py
RENAMED
|
File without changes
|
|
File without changes
|
{alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/transparency_inventory.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib.egg-info/requires.txt
RENAMED
|
File without changes
|
{alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|