simjecture 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- conjecture_solver/__init__.py +10 -0
- conjecture_solver/__main__.py +4 -0
- conjecture_solver/action_handlers.py +459 -0
- conjecture_solver/adapters/__init__.py +7 -0
- conjecture_solver/adapters/base.py +96 -0
- conjecture_solver/adapters/fake.py +155 -0
- conjecture_solver/adapters/pic.py +196 -0
- conjecture_solver/adapters/warpx.py +1375 -0
- conjecture_solver/autonomous_research.py +1071 -0
- conjecture_solver/benchmarks/__init__.py +6 -0
- conjecture_solver/benchmarks/electrostatic_pic.py +547 -0
- conjecture_solver/benchmarks/kinetic_sufficiency.py +318 -0
- conjecture_solver/builtin_skills/warpx/SKILL.md +151 -0
- conjecture_solver/builtin_skills/warpx/agents/openai.yaml +4 -0
- conjecture_solver/builtin_skills/warpx/examples/implicit_em_smoke.py +74 -0
- conjecture_solver/builtin_skills/warpx/examples/minimal_smoke.py +54 -0
- conjecture_solver/builtin_skills/warpx/examples/openpmd_field_smoke.py +87 -0
- conjecture_solver/builtin_skills/warpx/manifest.json +11 -0
- conjecture_solver/builtin_skills/warpx/references/2d-xz-commissioning.md +145 -0
- conjecture_solver/builtin_skills/warpx/references/cpu-launch-tuning.md +54 -0
- conjecture_solver/builtin_skills/warpx/references/diagnostics.md +139 -0
- conjecture_solver/builtin_skills/warpx/references/gpu-launch-tuning.md +79 -0
- conjecture_solver/builtin_skills/warpx/references/local-cuda-deployment.md +87 -0
- conjecture_solver/builtin_skills/warpx/references/numerical-risks.md +32 -0
- conjecture_solver/builtin_skills/warpx/references/picmi-interface.md +118 -0
- conjecture_solver/builtin_skills/warpx/references/resource-scaling.md +24 -0
- conjecture_solver/builtin_skills/warpx/references/time-integration.md +58 -0
- conjecture_solver/builtin_skills/warpx/scripts/benchmark_cpu_threads.py +207 -0
- conjecture_solver/builtin_skills/warpx/scripts/benchmark_gpu.py +242 -0
- conjecture_solver/builtin_skills/warpx/scripts/bootstrap_local_cuda.sh +82 -0
- conjecture_solver/builtin_skills/warpx/scripts/probe_local_cuda.py +110 -0
- conjecture_solver/builtin_skills/warpx/scripts/reduced_energy_budget.py +195 -0
- conjecture_solver/builtin_skills/warpx/scripts/run_local_cuda.sh +39 -0
- conjecture_solver/campaign.py +406 -0
- conjecture_solver/cli.py +963 -0
- conjecture_solver/confirmation.py +287 -0
- conjecture_solver/control.py +234 -0
- conjecture_solver/deployment.py +874 -0
- conjecture_solver/discovery.py +68 -0
- conjecture_solver/domains/__init__.py +40 -0
- conjecture_solver/domains/base.py +157 -0
- conjecture_solver/domains/kinetic_sufficiency.py +521 -0
- conjecture_solver/ledger.py +238 -0
- conjecture_solver/lifecycle.py +131 -0
- conjecture_solver/literature.py +494 -0
- conjecture_solver/llm.py +228 -0
- conjecture_solver/models.py +279 -0
- conjecture_solver/mvp_agent.py +3212 -0
- conjecture_solver/mvp_claims.py +1279 -0
- conjecture_solver/mvp_control.py +322 -0
- conjecture_solver/mvp_guidance.py +172 -0
- conjecture_solver/mvp_launch.py +1296 -0
- conjecture_solver/mvp_monitor.py +1487 -0
- conjecture_solver/mvp_skills.py +538 -0
- conjecture_solver/orchestration.py +578 -0
- conjecture_solver/outbox.py +526 -0
- conjecture_solver/parameters.py +394 -0
- conjecture_solver/proposals.py +427 -0
- conjecture_solver/research_tools.py +278 -0
- conjecture_solver/schema_export.py +246 -0
- conjecture_solver/search.py +1047 -0
- conjecture_solver/semantics.py +146 -0
- conjecture_solver/tui/__init__.py +7 -0
- conjecture_solver/tui/app.py +202 -0
- conjecture_solver/tui/claim_views.py +213 -0
- conjecture_solver/tui/screens.py +1014 -0
- conjecture_solver/warpx_analysis.py +184 -0
- conjecture_solver/warpx_campaign.py +148 -0
- conjecture_solver/warpx_confirmation.py +617 -0
- simjecture-0.1.0.dist-info/METADATA +400 -0
- simjecture-0.1.0.dist-info/RECORD +75 -0
- simjecture-0.1.0.dist-info/WHEEL +4 -0
- simjecture-0.1.0.dist-info/entry_points.txt +4 -0
- simjecture-0.1.0.dist-info/licenses/LICENSE +202 -0
- simjecture-0.1.0.dist-info/licenses/NOTICE +6 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""Typed, deterministic admission gate for model-generated research proposals."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any, Literal, Protocol
|
|
9
|
+
|
|
10
|
+
from pydantic import Field, ValidationError, model_validator
|
|
11
|
+
|
|
12
|
+
from .adapters.base import CapabilityManifest, SimulatorAdapter
|
|
13
|
+
from .ledger import SQLiteEventLedger, StoredEvent
|
|
14
|
+
from .llm import CompletionResult, ModelRoute
|
|
15
|
+
from .models import (
|
|
16
|
+
ExperimentSpec,
|
|
17
|
+
HypothesisNode,
|
|
18
|
+
HypothesisOrigin,
|
|
19
|
+
ObservableSpec,
|
|
20
|
+
PropositionClass,
|
|
21
|
+
StrictModel,
|
|
22
|
+
new_id,
|
|
23
|
+
utc_now,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ProposalRequest(StrictModel):
|
|
28
|
+
schema_version: Literal["0.1.0"] = "0.1.0"
|
|
29
|
+
id: str = Field(default_factory=lambda: new_id("proposal_request"))
|
|
30
|
+
research_goal: str = Field(min_length=1)
|
|
31
|
+
allowed_model_families: tuple[str, ...] = Field(min_length=1)
|
|
32
|
+
allowed_action_types: tuple[str, ...] = Field(min_length=1)
|
|
33
|
+
allowed_proposition_classes: tuple[PropositionClass, ...] = Field(min_length=1)
|
|
34
|
+
required_coordinates: tuple[str, ...] = Field(min_length=1)
|
|
35
|
+
allowed_observable_kinds: tuple[str, ...] = Field(min_length=1)
|
|
36
|
+
available_diagnostics: tuple[str, ...] = Field(min_length=1)
|
|
37
|
+
required_diagnostics: tuple[str, ...] = ()
|
|
38
|
+
fixed_parameters: dict[str, float | int | str] = Field(default_factory=dict)
|
|
39
|
+
fixed_numerical_parameters: dict[str, float | int | str] = Field(default_factory=dict)
|
|
40
|
+
additional_constraints: tuple[str, ...] = ()
|
|
41
|
+
|
|
42
|
+
@model_validator(mode="after")
|
|
43
|
+
def required_diagnostics_must_be_available(self) -> ProposalRequest:
|
|
44
|
+
if not set(self.required_diagnostics).issubset(self.available_diagnostics):
|
|
45
|
+
raise ValueError("required diagnostics must be included in available diagnostics")
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def planted_proposal_request() -> ProposalRequest:
|
|
50
|
+
"""Human-approved envelope for the first live proposal experiment."""
|
|
51
|
+
|
|
52
|
+
return ProposalRequest(
|
|
53
|
+
id="proposal_request_kinetic_sufficiency_v1",
|
|
54
|
+
research_goal=(
|
|
55
|
+
"Propose a finite matched-pair test of whether low-order velocity moments "
|
|
56
|
+
"suffice to predict the dominant linear growth rate."
|
|
57
|
+
),
|
|
58
|
+
allowed_model_families=("linearized_1d_electrostatic_vlasov_poisson",),
|
|
59
|
+
allowed_action_types=("kinetic_sufficiency",),
|
|
60
|
+
allowed_proposition_classes=(PropositionClass.PREDICTIVE_SUFFICIENCY,),
|
|
61
|
+
required_coordinates=("density", "mean_velocity", "variance"),
|
|
62
|
+
allowed_observable_kinds=("dominant_linear_growth_rate",),
|
|
63
|
+
available_diagnostics=("dominant_linear_mode", "distribution_moments"),
|
|
64
|
+
required_diagnostics=("dominant_linear_mode", "distribution_moments"),
|
|
65
|
+
fixed_parameters={"wavenumber": 0.5},
|
|
66
|
+
additional_constraints=(
|
|
67
|
+
"use a declarative proposition, not a research question",
|
|
68
|
+
"include a typed matched-pair formal predicate",
|
|
69
|
+
"label the origin as ai or mixed",
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def pic_proposal_request() -> ProposalRequest:
|
|
75
|
+
"""Human-approved envelope for a live proposal targeting the PIC adapter."""
|
|
76
|
+
|
|
77
|
+
return ProposalRequest(
|
|
78
|
+
id="proposal_request_electrostatic_pic_v1",
|
|
79
|
+
research_goal=(
|
|
80
|
+
"Propose a finite matched-pair PIC test of whether density, mean velocity, "
|
|
81
|
+
"and variance suffice to predict the effective fundamental-mode growth rate."
|
|
82
|
+
),
|
|
83
|
+
allowed_model_families=("electrostatic_1d_pic_vlasov_poisson",),
|
|
84
|
+
allowed_action_types=("kinetic_sufficiency",),
|
|
85
|
+
allowed_proposition_classes=(PropositionClass.PREDICTIVE_SUFFICIENCY,),
|
|
86
|
+
required_coordinates=("density", "mean_velocity", "variance"),
|
|
87
|
+
allowed_observable_kinds=("effective_fundamental_growth_rate",),
|
|
88
|
+
available_diagnostics=(
|
|
89
|
+
"dominant_linear_mode",
|
|
90
|
+
"distribution_moments",
|
|
91
|
+
"energy_conservation",
|
|
92
|
+
"gauss_residual",
|
|
93
|
+
),
|
|
94
|
+
required_diagnostics=(
|
|
95
|
+
"dominant_linear_mode",
|
|
96
|
+
"distribution_moments",
|
|
97
|
+
"energy_conservation",
|
|
98
|
+
"gauss_residual",
|
|
99
|
+
),
|
|
100
|
+
fixed_parameters={
|
|
101
|
+
"wavenumber": 0.5,
|
|
102
|
+
"perturbation_amplitude": 0.01,
|
|
103
|
+
"stream_drift": 0.9,
|
|
104
|
+
},
|
|
105
|
+
fixed_numerical_parameters={
|
|
106
|
+
"grid_cells": 64,
|
|
107
|
+
"velocity_beams": 256,
|
|
108
|
+
"particles_per_beam": 64,
|
|
109
|
+
"time_step": 0.05,
|
|
110
|
+
"final_time": 20.0,
|
|
111
|
+
"diagnostic_interval": 0.5,
|
|
112
|
+
"seed": 7,
|
|
113
|
+
},
|
|
114
|
+
additional_constraints=(
|
|
115
|
+
"use exactly one hypothesis",
|
|
116
|
+
"use action_type kinetic_sufficiency",
|
|
117
|
+
"use a declarative proposition and typed matched-pair formal predicate",
|
|
118
|
+
"set numerical parameters grid_cells=64, velocity_beams=256, "
|
|
119
|
+
"particles_per_beam=64, time_step=0.05, final_time=20.0, "
|
|
120
|
+
"diagnostic_interval=0.5, and seed=7",
|
|
121
|
+
"require all four available diagnostics",
|
|
122
|
+
"label the hypothesis origin as ai or mixed",
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ProposalDraft(StrictModel):
|
|
128
|
+
schema_version: Literal["0.1.0"] = "0.1.0"
|
|
129
|
+
hypothesis: HypothesisNode
|
|
130
|
+
observable: ObservableSpec
|
|
131
|
+
experiment: ExperimentSpec
|
|
132
|
+
rationale: str = Field(min_length=1)
|
|
133
|
+
declared_unknowns: tuple[str, ...] = Field(min_length=1)
|
|
134
|
+
|
|
135
|
+
@model_validator(mode="after")
|
|
136
|
+
def forbid_model_from_claiming_human_origin(self) -> ProposalDraft:
|
|
137
|
+
if self.hypothesis.origin is HypothesisOrigin.HUMAN:
|
|
138
|
+
raise ValueError("a model-generated proposal cannot claim human origin")
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ProposalValidation(StrictModel):
|
|
143
|
+
valid: bool
|
|
144
|
+
errors: tuple[str, ...] = ()
|
|
145
|
+
warnings: tuple[str, ...] = ()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ModelCallProvenance(StrictModel):
|
|
149
|
+
request_id: str
|
|
150
|
+
model: str
|
|
151
|
+
route: ModelRoute
|
|
152
|
+
route_reason: str
|
|
153
|
+
finish_reason: str
|
|
154
|
+
prompt_template_version: Literal["proposal_v1"] = "proposal_v1"
|
|
155
|
+
prompt_hash: str
|
|
156
|
+
output_schema_hash: str
|
|
157
|
+
response_hash: str
|
|
158
|
+
usage: dict[str, Any] = Field(default_factory=dict)
|
|
159
|
+
admission_errors: tuple[str, ...] = ()
|
|
160
|
+
attempt_number: int = Field(ge=1)
|
|
161
|
+
created_at: datetime = Field(default_factory=utc_now)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class ProposalRecord(StrictModel):
|
|
165
|
+
schema_version: Literal["0.1.0"] = "0.1.0"
|
|
166
|
+
request: ProposalRequest
|
|
167
|
+
draft: ProposalDraft
|
|
168
|
+
validation: ProposalValidation
|
|
169
|
+
model_calls: tuple[ModelCallProvenance, ...] = Field(min_length=1)
|
|
170
|
+
|
|
171
|
+
@model_validator(mode="after")
|
|
172
|
+
def only_admitted_proposals_become_records(self) -> ProposalRecord:
|
|
173
|
+
if not self.validation.valid or self.validation.errors:
|
|
174
|
+
raise ValueError("a proposal record requires a successful admission decision")
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class ProposalRejected(ValueError):
|
|
179
|
+
def __init__(
|
|
180
|
+
self,
|
|
181
|
+
errors: tuple[str, ...],
|
|
182
|
+
model_calls: tuple[ModelCallProvenance, ...],
|
|
183
|
+
) -> None:
|
|
184
|
+
super().__init__("proposal rejected: " + "; ".join(errors))
|
|
185
|
+
self.errors = errors
|
|
186
|
+
self.model_calls = model_calls
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def record_admitted_proposal(
|
|
190
|
+
ledger: SQLiteEventLedger,
|
|
191
|
+
*,
|
|
192
|
+
campaign_id: str,
|
|
193
|
+
record: ProposalRecord,
|
|
194
|
+
) -> StoredEvent:
|
|
195
|
+
"""Commit a validated proposal without granting it execution authority."""
|
|
196
|
+
|
|
197
|
+
return ledger.append(
|
|
198
|
+
campaign_id=campaign_id,
|
|
199
|
+
event_type="proposal_admitted",
|
|
200
|
+
aggregate_type="proposal",
|
|
201
|
+
aggregate_id=record.request.id,
|
|
202
|
+
payload={"proposal_record": record.model_dump(mode="json")},
|
|
203
|
+
idempotency_key=f"{campaign_id}:proposal:{record.request.id}",
|
|
204
|
+
).event
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class CompletionClient(Protocol):
|
|
208
|
+
def complete(
|
|
209
|
+
self,
|
|
210
|
+
messages: list[dict[str, str]],
|
|
211
|
+
*,
|
|
212
|
+
route: ModelRoute = ModelRoute.DEFAULT,
|
|
213
|
+
escalation_reason: str | None = None,
|
|
214
|
+
max_tokens: int = 1024,
|
|
215
|
+
temperature: float = 0.0,
|
|
216
|
+
) -> CompletionResult: ...
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def validate_proposal(
|
|
220
|
+
request: ProposalRequest,
|
|
221
|
+
draft: ProposalDraft,
|
|
222
|
+
capabilities: CapabilityManifest,
|
|
223
|
+
) -> ProposalValidation:
|
|
224
|
+
errors: list[str] = []
|
|
225
|
+
warnings: list[str] = []
|
|
226
|
+
hypothesis = draft.hypothesis
|
|
227
|
+
experiment = draft.experiment
|
|
228
|
+
observable = draft.observable
|
|
229
|
+
|
|
230
|
+
if hypothesis.proposition_class not in request.allowed_proposition_classes:
|
|
231
|
+
errors.append("proposition class is outside the human-approved request")
|
|
232
|
+
if hypothesis.domain.model_family not in request.allowed_model_families:
|
|
233
|
+
errors.append("model family is outside the human-approved request")
|
|
234
|
+
if hypothesis.domain.model_family not in capabilities.supported_models:
|
|
235
|
+
errors.append("adapter does not support the proposed model family")
|
|
236
|
+
if hypothesis.coordinates != request.required_coordinates:
|
|
237
|
+
errors.append("hypothesis coordinates do not match the required canonical coordinates")
|
|
238
|
+
if not set(hypothesis.coordinates).issubset(capabilities.supported_coordinates):
|
|
239
|
+
errors.append("adapter does not support every proposed coordinate")
|
|
240
|
+
if observable.semantic_kind not in request.allowed_observable_kinds:
|
|
241
|
+
errors.append("observable semantic kind is outside the human-approved request")
|
|
242
|
+
if observable.semantic_kind not in capabilities.supported_observable_kinds:
|
|
243
|
+
errors.append("adapter does not support the observable semantic kind")
|
|
244
|
+
if experiment.action_type not in request.allowed_action_types:
|
|
245
|
+
errors.append("action type is outside the human-approved request")
|
|
246
|
+
if experiment.action_type not in capabilities.supported_actions:
|
|
247
|
+
errors.append("adapter does not support the proposed action type")
|
|
248
|
+
if hypothesis.id not in experiment.hypothesis_ids:
|
|
249
|
+
errors.append("experiment does not reference the proposed hypothesis")
|
|
250
|
+
if hypothesis.evidence_contract.primary_observable_id != observable.id:
|
|
251
|
+
errors.append("proposal observable does not match the hypothesis evidence contract")
|
|
252
|
+
if abs(hypothesis.evidence_contract.primary_tolerance - observable.tolerance) > 1e-15:
|
|
253
|
+
errors.append("proposal observable tolerance does not match the evidence contract")
|
|
254
|
+
|
|
255
|
+
requested_diagnostics = set(experiment.required_diagnostics)
|
|
256
|
+
if not requested_diagnostics.issubset(request.available_diagnostics):
|
|
257
|
+
errors.append("experiment requests diagnostics outside the approved set")
|
|
258
|
+
if not requested_diagnostics.issubset(capabilities.supported_diagnostics):
|
|
259
|
+
errors.append("adapter does not support every required diagnostic")
|
|
260
|
+
if not set(request.required_diagnostics).issubset(requested_diagnostics):
|
|
261
|
+
errors.append("experiment omitted a human-required diagnostic")
|
|
262
|
+
|
|
263
|
+
for name, fixed_value in request.fixed_parameters.items():
|
|
264
|
+
if hypothesis.domain.fixed_parameters.get(name) != fixed_value:
|
|
265
|
+
errors.append(f"hypothesis domain changed fixed parameter {name}")
|
|
266
|
+
if experiment.physical_parameters.get(name) != fixed_value:
|
|
267
|
+
errors.append(f"experiment changed fixed parameter {name}")
|
|
268
|
+
for name, fixed_value in request.fixed_numerical_parameters.items():
|
|
269
|
+
if experiment.numerical_parameters.get(name) != fixed_value:
|
|
270
|
+
errors.append(f"experiment changed fixed numerical parameter {name}")
|
|
271
|
+
|
|
272
|
+
if hypothesis.formal_predicate is None:
|
|
273
|
+
errors.append("initial AI proposals require a typed formal predicate")
|
|
274
|
+
if hypothesis.proposition_class is PropositionClass.CAUSAL_EXHAUSTIVENESS:
|
|
275
|
+
errors.append("open-world causal-exhaustiveness claims are not admitted initially")
|
|
276
|
+
if len(experiment.hypothesis_ids) > 1:
|
|
277
|
+
warnings.append("the initial slice is optimized for one hypothesis per experiment")
|
|
278
|
+
|
|
279
|
+
return ProposalValidation(valid=not errors, errors=tuple(errors), warnings=tuple(warnings))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class ProposalGenerator:
|
|
283
|
+
def __init__(
|
|
284
|
+
self,
|
|
285
|
+
*,
|
|
286
|
+
client: CompletionClient,
|
|
287
|
+
capabilities: CapabilityManifest | None = None,
|
|
288
|
+
adapter: SimulatorAdapter | None = None,
|
|
289
|
+
max_attempts: int = 2,
|
|
290
|
+
max_escalation_attempts: int = 1,
|
|
291
|
+
) -> None:
|
|
292
|
+
if max_attempts < 1 or max_escalation_attempts < 1:
|
|
293
|
+
raise ValueError("attempt limits must be positive")
|
|
294
|
+
if capabilities is None and adapter is None:
|
|
295
|
+
raise ValueError("proposal generation requires capabilities or an adapter")
|
|
296
|
+
self.client = client
|
|
297
|
+
self.adapter = adapter
|
|
298
|
+
if capabilities is not None:
|
|
299
|
+
self.capabilities = capabilities
|
|
300
|
+
else:
|
|
301
|
+
assert adapter is not None
|
|
302
|
+
self.capabilities = adapter.capabilities()
|
|
303
|
+
self.max_attempts = max_attempts
|
|
304
|
+
self.max_escalation_attempts = max_escalation_attempts
|
|
305
|
+
|
|
306
|
+
@staticmethod
|
|
307
|
+
def _initial_messages(request: ProposalRequest) -> list[dict[str, str]]:
|
|
308
|
+
schema = ProposalDraft.model_json_schema()
|
|
309
|
+
return [
|
|
310
|
+
{
|
|
311
|
+
"role": "system",
|
|
312
|
+
"content": (
|
|
313
|
+
"You propose a falsifiable computational-physics test inside a "
|
|
314
|
+
"human-approved capability envelope. Return exactly one JSON object "
|
|
315
|
+
"matching the supplied schema: no Markdown and no commentary. You may "
|
|
316
|
+
"propose only; do not claim evidence, confirmation, or job execution."
|
|
317
|
+
),
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"role": "user",
|
|
321
|
+
"content": json.dumps(
|
|
322
|
+
{
|
|
323
|
+
"request": request.model_dump(mode="json"),
|
|
324
|
+
"output_schema": schema,
|
|
325
|
+
},
|
|
326
|
+
sort_keys=True,
|
|
327
|
+
),
|
|
328
|
+
},
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
@staticmethod
|
|
332
|
+
def _provenance(
|
|
333
|
+
result: CompletionResult,
|
|
334
|
+
messages: list[dict[str, str]],
|
|
335
|
+
attempt_number: int,
|
|
336
|
+
) -> ModelCallProvenance:
|
|
337
|
+
prompt = json.dumps(messages, sort_keys=True, separators=(",", ":"))
|
|
338
|
+
output_schema = json.dumps(
|
|
339
|
+
ProposalDraft.model_json_schema(),
|
|
340
|
+
sort_keys=True,
|
|
341
|
+
separators=(",", ":"),
|
|
342
|
+
)
|
|
343
|
+
return ModelCallProvenance(
|
|
344
|
+
request_id=result.request_id,
|
|
345
|
+
model=result.model,
|
|
346
|
+
route=result.route,
|
|
347
|
+
route_reason=result.route_reason,
|
|
348
|
+
finish_reason=result.finish_reason,
|
|
349
|
+
prompt_hash=hashlib.sha256(prompt.encode()).hexdigest(),
|
|
350
|
+
output_schema_hash=hashlib.sha256(output_schema.encode()).hexdigest(),
|
|
351
|
+
response_hash=hashlib.sha256(result.content.encode()).hexdigest(),
|
|
352
|
+
usage=result.usage,
|
|
353
|
+
attempt_number=attempt_number,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def generate(
|
|
357
|
+
self,
|
|
358
|
+
request: ProposalRequest,
|
|
359
|
+
*,
|
|
360
|
+
route: ModelRoute = ModelRoute.DEFAULT,
|
|
361
|
+
escalation_reason: str | None = None,
|
|
362
|
+
max_tokens: int = 16384,
|
|
363
|
+
) -> ProposalRecord:
|
|
364
|
+
messages = self._initial_messages(request)
|
|
365
|
+
calls: list[ModelCallProvenance] = []
|
|
366
|
+
last_errors: tuple[str, ...] = ("model returned no proposal",)
|
|
367
|
+
|
|
368
|
+
attempt_limit = (
|
|
369
|
+
self.max_escalation_attempts
|
|
370
|
+
if route is ModelRoute.ESCALATION
|
|
371
|
+
else self.max_attempts
|
|
372
|
+
)
|
|
373
|
+
for attempt_number in range(1, attempt_limit + 1):
|
|
374
|
+
result = self.client.complete(
|
|
375
|
+
messages,
|
|
376
|
+
route=route,
|
|
377
|
+
escalation_reason=escalation_reason,
|
|
378
|
+
max_tokens=max_tokens,
|
|
379
|
+
temperature=0.0,
|
|
380
|
+
)
|
|
381
|
+
calls.append(self._provenance(result, messages, attempt_number))
|
|
382
|
+
if result.finish_reason != "stop":
|
|
383
|
+
last_errors = (f"incomplete completion: {result.finish_reason}",)
|
|
384
|
+
else:
|
|
385
|
+
try:
|
|
386
|
+
document = json.loads(result.content)
|
|
387
|
+
draft = ProposalDraft.model_validate(document)
|
|
388
|
+
except (json.JSONDecodeError, ValidationError) as error:
|
|
389
|
+
last_errors = (f"invalid proposal document: {error}",)
|
|
390
|
+
else:
|
|
391
|
+
validation = validate_proposal(request, draft, self.capabilities)
|
|
392
|
+
if validation.valid and self.adapter is not None:
|
|
393
|
+
adapter_validation = self.adapter.validate(draft.experiment)
|
|
394
|
+
validation = ProposalValidation(
|
|
395
|
+
valid=adapter_validation.valid,
|
|
396
|
+
errors=tuple(
|
|
397
|
+
f"adapter validation: {error}"
|
|
398
|
+
for error in adapter_validation.errors
|
|
399
|
+
),
|
|
400
|
+
warnings=(*validation.warnings, *adapter_validation.warnings),
|
|
401
|
+
)
|
|
402
|
+
if validation.valid:
|
|
403
|
+
return ProposalRecord(
|
|
404
|
+
request=request,
|
|
405
|
+
draft=draft,
|
|
406
|
+
validation=validation,
|
|
407
|
+
model_calls=tuple(calls),
|
|
408
|
+
)
|
|
409
|
+
last_errors = validation.errors
|
|
410
|
+
|
|
411
|
+
calls[-1] = calls[-1].model_copy(update={"admission_errors": last_errors})
|
|
412
|
+
|
|
413
|
+
messages.extend(
|
|
414
|
+
(
|
|
415
|
+
{"role": "assistant", "content": result.content},
|
|
416
|
+
{
|
|
417
|
+
"role": "user",
|
|
418
|
+
"content": (
|
|
419
|
+
"The deterministic admission gate rejected the proposal for: "
|
|
420
|
+
+ "; ".join(last_errors)
|
|
421
|
+
+ ". Return a corrected JSON object only."
|
|
422
|
+
),
|
|
423
|
+
},
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
raise ProposalRejected(last_errors, tuple(calls))
|