selfevals 0.2.2__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.
- selfevals/.agents/skills/error-analysis/SKILL.md +149 -0
- selfevals/__init__.py +19 -0
- selfevals/_errors.py +44 -0
- selfevals/_internal/__init__.py +0 -0
- selfevals/_internal/hashing.py +23 -0
- selfevals/_internal/ids.py +65 -0
- selfevals/_internal/time.py +17 -0
- selfevals/analysis/__init__.py +23 -0
- selfevals/analysis/bundle.py +162 -0
- selfevals/analysis/hypothesis.py +26 -0
- selfevals/analysis/ingest.py +185 -0
- selfevals/analysis/schemas.py +119 -0
- selfevals/analysis/staging.py +34 -0
- selfevals/api/__init__.py +24 -0
- selfevals/api/__main__.py +47 -0
- selfevals/api/app.py +351 -0
- selfevals/api/broker.py +210 -0
- selfevals/api/broker_bridge.py +29 -0
- selfevals/api/queries.py +447 -0
- selfevals/api/schemas.py +151 -0
- selfevals/api/sse.py +114 -0
- selfevals/cli/__init__.py +15 -0
- selfevals/cli/_friendly.py +180 -0
- selfevals/cli/_help.py +55 -0
- selfevals/cli/analyze_commands.py +169 -0
- selfevals/cli/commands.py +615 -0
- selfevals/cli/main.py +409 -0
- selfevals/decision/__init__.py +34 -0
- selfevals/decision/matrix.py +185 -0
- selfevals/examples/__init__.py +8 -0
- selfevals/examples/evals/datasets/pingpong.jsonl +2 -0
- selfevals/examples/evals/experiments/example_pingpong.yaml +58 -0
- selfevals/examples/pingpong.py +21 -0
- selfevals/graders/__init__.py +46 -0
- selfevals/graders/base.py +54 -0
- selfevals/graders/calibration.py +145 -0
- selfevals/graders/deterministic.py +143 -0
- selfevals/graders/llm_judge.py +187 -0
- selfevals/graders/registry.py +66 -0
- selfevals/optimization/__init__.py +47 -0
- selfevals/optimization/aggregator.py +246 -0
- selfevals/optimization/loop.py +432 -0
- selfevals/optimization/proposers.py +202 -0
- selfevals/py.typed +0 -0
- selfevals/repo/__init__.py +28 -0
- selfevals/repo/loader.py +276 -0
- selfevals/reporter/__init__.py +21 -0
- selfevals/reporter/_metrics.py +114 -0
- selfevals/reporter/compare.py +221 -0
- selfevals/reporter/json_report.py +105 -0
- selfevals/reporter/markdown.py +232 -0
- selfevals/runner/__init__.py +42 -0
- selfevals/runner/adapters.py +268 -0
- selfevals/runner/executor.py +234 -0
- selfevals/runner/otlp_receiver.py +343 -0
- selfevals/runner/otlp_to_recorder.py +180 -0
- selfevals/runner/sandbox.py +46 -0
- selfevals/schemas/__init__.py +213 -0
- selfevals/schemas/_base.py +82 -0
- selfevals/schemas/annotation.py +55 -0
- selfevals/schemas/dataset.py +111 -0
- selfevals/schemas/enums.py +324 -0
- selfevals/schemas/eval_case.py +189 -0
- selfevals/schemas/experiment.py +367 -0
- selfevals/schemas/failure_mode.py +76 -0
- selfevals/schemas/fleet.py +111 -0
- selfevals/schemas/grader_card.py +112 -0
- selfevals/schemas/iteration.py +219 -0
- selfevals/schemas/registry.py +125 -0
- selfevals/schemas/tool.py +43 -0
- selfevals/schemas/trace.py +384 -0
- selfevals/schemas/workspace.py +69 -0
- selfevals/sdk/__init__.py +24 -0
- selfevals/sdk/auto_instrument.py +165 -0
- selfevals/sdk/context.py +45 -0
- selfevals/sdk/exporter.py +50 -0
- selfevals/sdk/facade.py +203 -0
- selfevals/skills/__init__.py +61 -0
- selfevals/storage/__init__.py +53 -0
- selfevals/storage/errors.py +66 -0
- selfevals/storage/filesystem.py +137 -0
- selfevals/storage/interface.py +135 -0
- selfevals/storage/migrations/__init__.py +80 -0
- selfevals/storage/migrations/m0001_initial.py +57 -0
- selfevals/storage/seed.py +199 -0
- selfevals/storage/sqlite.py +232 -0
- selfevals/trace/__init__.py +31 -0
- selfevals/trace/otel_importer.py +455 -0
- selfevals/trace/payload_router.py +106 -0
- selfevals/trace/recorder.py +540 -0
- selfevals/version.py +1 -0
- selfevals-0.2.2.dist-info/METADATA +283 -0
- selfevals-0.2.2.dist-info/RECORD +96 -0
- selfevals-0.2.2.dist-info/WHEEL +4 -0
- selfevals-0.2.2.dist-info/entry_points.txt +2 -0
- selfevals-0.2.2.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Ingest an AnalysisResult: persist assignments, candidates, hypotheses.
|
|
2
|
+
|
|
3
|
+
This is the push half of the handshake (design §4). It enforces the two
|
|
4
|
+
invariants that keep the taxonomy trustworthy:
|
|
5
|
+
|
|
6
|
+
1. Each assignment targets exactly one of an existing `mode_id` (classify) or
|
|
7
|
+
a `new_mode_slug` (propose) — the XOR, validated on the wire model and
|
|
8
|
+
re-checked here against what actually exists.
|
|
9
|
+
2. Classify-don't-rename: an assignment may reference an existing mode but can
|
|
10
|
+
never edit its title/definition. New modes arrive only as candidates.
|
|
11
|
+
Renaming is a separate human action. This is what keeps mode identity
|
|
12
|
+
stable across analysis runs.
|
|
13
|
+
|
|
14
|
+
New modes are created idempotent on slug (a repeat slug updates the existing
|
|
15
|
+
candidate's examples rather than duplicating). Hypotheses are stored as
|
|
16
|
+
`Proposal` seeds linked to the experiment; they are not auto-run.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
from selfevals.analysis.schemas import AnalysisResult
|
|
25
|
+
from selfevals.schemas.enums import FailureModeStatus
|
|
26
|
+
from selfevals.schemas.failure_mode import FailureMode, FailureModeExample
|
|
27
|
+
from selfevals.schemas.trace import GraderResult, Trace
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from selfevals.storage.interface import ObjectStoreInterface, WorkspaceScope
|
|
31
|
+
from selfevals.storage.sqlite import SQLiteStorage
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AnalysisIngestError(ValueError):
|
|
35
|
+
"""Raised when an AnalysisResult cannot be applied (unknown ids, etc.)."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class IngestSummary:
|
|
40
|
+
created_candidates: list[str] = field(default_factory=list) # fm ids
|
|
41
|
+
updated_candidates: list[str] = field(default_factory=list) # fm ids (slug re-seen)
|
|
42
|
+
assignments_applied: int = 0
|
|
43
|
+
hypotheses_recorded: int = 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def ingest_result(
|
|
47
|
+
storage: SQLiteStorage,
|
|
48
|
+
*,
|
|
49
|
+
workspace_id: str,
|
|
50
|
+
experiment_id: str,
|
|
51
|
+
result: AnalysisResult,
|
|
52
|
+
proposed_by: str = "agent:unknown",
|
|
53
|
+
object_store: ObjectStoreInterface | None = None,
|
|
54
|
+
) -> IngestSummary:
|
|
55
|
+
"""Apply an AnalysisResult to the workspace. Best-effort transactional:
|
|
56
|
+
everything is validated before any write, so a bad result rejects whole."""
|
|
57
|
+
summary = IngestSummary()
|
|
58
|
+
|
|
59
|
+
with storage.open(workspace_id) as scope:
|
|
60
|
+
existing = [fm for fm in scope.list_entities(FailureMode) if isinstance(fm, FailureMode)]
|
|
61
|
+
by_id = {fm.id: fm for fm in existing}
|
|
62
|
+
by_slug = {fm.slug: fm for fm in existing}
|
|
63
|
+
for a in result.assignments:
|
|
64
|
+
if a.mode_id is not None and a.mode_id not in by_id:
|
|
65
|
+
raise AnalysisIngestError(f"assignment references unknown mode_id {a.mode_id!r}")
|
|
66
|
+
proposed_slugs = {p.slug for p in result.proposed_modes}
|
|
67
|
+
for a in result.assignments:
|
|
68
|
+
# An assignment can name a new slug only if it is declared in
|
|
69
|
+
# proposed_modes (so it has a definition) or already known.
|
|
70
|
+
if (
|
|
71
|
+
a.new_mode_slug is not None
|
|
72
|
+
and a.new_mode_slug not in proposed_slugs
|
|
73
|
+
and a.new_mode_slug not in by_slug
|
|
74
|
+
):
|
|
75
|
+
raise AnalysisIngestError(
|
|
76
|
+
f"assignment proposes new_mode_slug {a.new_mode_slug!r} "
|
|
77
|
+
"but it is neither in proposed_modes nor already known"
|
|
78
|
+
)
|
|
79
|
+
slug_to_mode: dict[str, FailureMode] = dict(by_slug)
|
|
80
|
+
for p in result.proposed_modes:
|
|
81
|
+
if p.slug in by_slug:
|
|
82
|
+
# Slug re-seen: keep the existing mode (classify-don't-rename),
|
|
83
|
+
# only note that the agent re-proposed it.
|
|
84
|
+
summary.updated_candidates.append(by_slug[p.slug].id)
|
|
85
|
+
slug_to_mode[p.slug] = by_slug[p.slug]
|
|
86
|
+
continue
|
|
87
|
+
parent_id = by_slug[p.parent_slug].id if p.parent_slug in by_slug else None
|
|
88
|
+
mode = FailureMode(
|
|
89
|
+
id=FailureMode.make_id(),
|
|
90
|
+
workspace_id=workspace_id,
|
|
91
|
+
slug=p.slug,
|
|
92
|
+
title=p.title,
|
|
93
|
+
definition=p.definition,
|
|
94
|
+
status=FailureModeStatus.CANDIDATE,
|
|
95
|
+
parent_mode_id=parent_id,
|
|
96
|
+
proposed_by=proposed_by,
|
|
97
|
+
)
|
|
98
|
+
scope.put_entity(mode)
|
|
99
|
+
slug_to_mode[p.slug] = mode
|
|
100
|
+
by_id[mode.id] = mode
|
|
101
|
+
summary.created_candidates.append(mode.id)
|
|
102
|
+
for a in result.assignments:
|
|
103
|
+
resolved_id = (
|
|
104
|
+
a.mode_id if a.mode_id is not None else slug_to_mode[a.new_mode_slug].id # type: ignore[index]
|
|
105
|
+
)
|
|
106
|
+
trace = scope.get_entity(Trace, a.trace_id)
|
|
107
|
+
assert isinstance(trace, Trace)
|
|
108
|
+
_stamp_mode_on_trace(trace, resolved_id, grader="error_analysis")
|
|
109
|
+
scope.put_entity(trace)
|
|
110
|
+
|
|
111
|
+
# Append example evidence to the mode (payload-route the quote).
|
|
112
|
+
mode = by_id[resolved_id]
|
|
113
|
+
quote_pointer = None
|
|
114
|
+
quote_hash = None
|
|
115
|
+
if a.quote and object_store is not None:
|
|
116
|
+
from selfevals.trace.payload_router import PayloadRouter
|
|
117
|
+
|
|
118
|
+
router = PayloadRouter(object_store, workspace_id=workspace_id)
|
|
119
|
+
routed = router.route_value(f"fm_example:{a.trace_id}", a.quote)
|
|
120
|
+
quote_pointer = routed.pointer
|
|
121
|
+
quote_hash = routed.content_hash
|
|
122
|
+
mode.examples.append(
|
|
123
|
+
FailureModeExample(
|
|
124
|
+
trace_id=a.trace_id,
|
|
125
|
+
quote_pointer=quote_pointer,
|
|
126
|
+
quote_hash=quote_hash,
|
|
127
|
+
note=a.open_note,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
scope.put_entity(mode)
|
|
131
|
+
summary.assignments_applied += 1
|
|
132
|
+
summary.hypotheses_recorded = _record_hypotheses(
|
|
133
|
+
scope, workspace_id=workspace_id, experiment_id=experiment_id, result=result
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return summary
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _stamp_mode_on_trace(trace: Trace, mode_id: str, *, grader: str) -> None:
|
|
140
|
+
"""Add `mode_id` to the trace's grader results without duplicating.
|
|
141
|
+
|
|
142
|
+
If an error-analysis GraderResult already exists, extend it; otherwise add
|
|
143
|
+
one carrying the trace's worst label so the link has context.
|
|
144
|
+
"""
|
|
145
|
+
for gr in trace.grader_results:
|
|
146
|
+
if gr.grader == grader:
|
|
147
|
+
if mode_id not in gr.failure_modes:
|
|
148
|
+
gr.failure_modes = [*gr.failure_modes, mode_id]
|
|
149
|
+
return
|
|
150
|
+
worst = "fail"
|
|
151
|
+
for gr in trace.grader_results:
|
|
152
|
+
if gr.label in {"error", "fail", "partial"}:
|
|
153
|
+
worst = gr.label
|
|
154
|
+
break
|
|
155
|
+
trace.grader_results.append(GraderResult(grader=grader, label=worst, failure_modes=[mode_id]))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _record_hypotheses(
|
|
159
|
+
scope: WorkspaceScope,
|
|
160
|
+
*,
|
|
161
|
+
workspace_id: str,
|
|
162
|
+
experiment_id: str,
|
|
163
|
+
result: AnalysisResult,
|
|
164
|
+
) -> int:
|
|
165
|
+
"""Persist hypotheses as HypothesisRecord seeds for the proposer.
|
|
166
|
+
|
|
167
|
+
Kept as a thin entity so the proposer (and a future llm_proposer) can
|
|
168
|
+
consult them. We do not run them here.
|
|
169
|
+
"""
|
|
170
|
+
from selfevals.analysis.hypothesis import HypothesisRecord
|
|
171
|
+
|
|
172
|
+
count = 0
|
|
173
|
+
for h in result.hypotheses:
|
|
174
|
+
scope.put_entity(
|
|
175
|
+
HypothesisRecord(
|
|
176
|
+
id=HypothesisRecord.make_id(),
|
|
177
|
+
workspace_id=workspace_id,
|
|
178
|
+
experiment_id=experiment_id,
|
|
179
|
+
targets_mode_slug=h.targets_mode_slug,
|
|
180
|
+
statement=h.statement,
|
|
181
|
+
suggested_parameters=dict(h.suggested_parameters),
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
count += 1
|
|
185
|
+
return count
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Wire schemas for the error-analysis handshake.
|
|
2
|
+
|
|
3
|
+
These are the contract between selfevals and an external coding agent (the
|
|
4
|
+
`error-analysis` skill). selfevals emits an `AnalysisBundle` (pull) and ingests
|
|
5
|
+
an `AnalysisResult` (push). Get these right and any agent can honour the
|
|
6
|
+
protocol. See docs/spec/error_analysis_design.md §4.
|
|
7
|
+
|
|
8
|
+
They are plain `SelfEvalsModel`s (not entities) — transport shapes, not stored
|
|
9
|
+
rows. The persistence happens by translating an `AnalysisResult` into
|
|
10
|
+
`FailureMode` entities and `GraderResult` updates in `ingest.py`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from pydantic import Field, model_validator
|
|
18
|
+
|
|
19
|
+
from selfevals.schemas._base import NonEmptyStr, SelfEvalsModel
|
|
20
|
+
|
|
21
|
+
ANALYSIS_SCHEMA_VERSION = "1.0.0"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TaxonomyEntry(SelfEvalsModel):
|
|
25
|
+
"""A live failure mode the agent must classify AGAINST (never rename)."""
|
|
26
|
+
|
|
27
|
+
id: NonEmptyStr
|
|
28
|
+
slug: NonEmptyStr
|
|
29
|
+
title: str
|
|
30
|
+
definition: str
|
|
31
|
+
status: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BundleGrade(SelfEvalsModel):
|
|
35
|
+
label: str
|
|
36
|
+
score: float | None = None
|
|
37
|
+
deterministic_modes: list[str] = Field(default_factory=list)
|
|
38
|
+
judge_reason: str | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BundleMessage(SelfEvalsModel):
|
|
42
|
+
role: str
|
|
43
|
+
content: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BundleErrorSpan(SelfEvalsModel):
|
|
47
|
+
kind: str
|
|
48
|
+
name: str
|
|
49
|
+
error: str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BundleTrace(SelfEvalsModel):
|
|
53
|
+
"""One failed trace the agent needs to code."""
|
|
54
|
+
|
|
55
|
+
trace_id: NonEmptyStr
|
|
56
|
+
run_id: NonEmptyStr
|
|
57
|
+
thread_id: str | None = None
|
|
58
|
+
eval_case_id: str | None = None
|
|
59
|
+
grade: BundleGrade
|
|
60
|
+
transcript: list[BundleMessage] = Field(default_factory=list)
|
|
61
|
+
first_error_span: BundleErrorSpan | None = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AnalysisBundle(SelfEvalsModel):
|
|
65
|
+
schema_version: str = ANALYSIS_SCHEMA_VERSION
|
|
66
|
+
workspace_id: NonEmptyStr
|
|
67
|
+
experiment_id: NonEmptyStr
|
|
68
|
+
iteration: int | None = None
|
|
69
|
+
taxonomy: list[TaxonomyEntry] = Field(default_factory=list)
|
|
70
|
+
traces: list[BundleTrace] = Field(default_factory=list)
|
|
71
|
+
instructions_ref: str = "skill://error-analysis"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Assignment(SelfEvalsModel):
|
|
75
|
+
"""Trace → failure mode. Either an existing `mode_id` (classify) XOR a
|
|
76
|
+
`new_mode_slug` (propose). Never both, never neither — enforced here and
|
|
77
|
+
again transactionally in ingest."""
|
|
78
|
+
|
|
79
|
+
trace_id: NonEmptyStr
|
|
80
|
+
mode_id: str | None = None
|
|
81
|
+
new_mode_slug: str | None = None
|
|
82
|
+
open_note: str | None = None
|
|
83
|
+
quote: str | None = None
|
|
84
|
+
confidence: float | None = Field(default=None, ge=0.0, le=1.0)
|
|
85
|
+
|
|
86
|
+
@model_validator(mode="after")
|
|
87
|
+
def _exactly_one_target(self) -> Assignment:
|
|
88
|
+
has_id = self.mode_id is not None
|
|
89
|
+
has_slug = self.new_mode_slug is not None
|
|
90
|
+
if has_id == has_slug:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"assignment must set exactly one of mode_id (classify) or "
|
|
93
|
+
"new_mode_slug (propose) — never both, never neither"
|
|
94
|
+
)
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ProposedMode(SelfEvalsModel):
|
|
99
|
+
"""A new candidate mode discovered during axial coding."""
|
|
100
|
+
|
|
101
|
+
slug: NonEmptyStr
|
|
102
|
+
title: NonEmptyStr
|
|
103
|
+
definition: NonEmptyStr
|
|
104
|
+
parent_slug: str | None = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Hypothesis(SelfEvalsModel):
|
|
108
|
+
"""A testable change targeting a mode, fed to the proposer (not auto-run)."""
|
|
109
|
+
|
|
110
|
+
targets_mode_slug: NonEmptyStr
|
|
111
|
+
statement: NonEmptyStr
|
|
112
|
+
suggested_parameters: dict[str, Any] = Field(default_factory=dict)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class AnalysisResult(SelfEvalsModel):
|
|
116
|
+
schema_version: str = ANALYSIS_SCHEMA_VERSION
|
|
117
|
+
assignments: list[Assignment] = Field(default_factory=list)
|
|
118
|
+
proposed_modes: list[ProposedMode] = Field(default_factory=list)
|
|
119
|
+
hypotheses: list[Hypothesis] = Field(default_factory=list)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""AnalysisStagingRecord — selfevals's advisory "this run is worth coding" marker.
|
|
2
|
+
|
|
3
|
+
When an experiment opts into error analysis (`error_analysis.enabled`) and an
|
|
4
|
+
iteration's fail rate clears the configured trigger, the loop persists one of
|
|
5
|
+
these. It records *that the trigger fired* — the experiment, the iteration, the
|
|
6
|
+
observed fail rate, and a human-readable reason — so a human or scheduler knows
|
|
7
|
+
an `analyze pull` is worth doing.
|
|
8
|
+
|
|
9
|
+
selfevals never invokes an agent or an LLM off the back of this. Staging is a
|
|
10
|
+
signal, not an action: `analyze pull` stays a pure read you can run anytime; the
|
|
11
|
+
marker just tells you *when it pays off*. See docs/spec/error_analysis_design.md §9.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import ClassVar
|
|
17
|
+
|
|
18
|
+
from pydantic import Field
|
|
19
|
+
|
|
20
|
+
from selfevals.schemas._base import BaseEntity, NonEmptyStr
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AnalysisStagingRecord(BaseEntity):
|
|
24
|
+
_id_prefix: ClassVar[str] = "stg"
|
|
25
|
+
|
|
26
|
+
experiment_id: NonEmptyStr
|
|
27
|
+
iteration: int = Field(ge=0)
|
|
28
|
+
fail_rate: float = Field(ge=0.0, le=1.0)
|
|
29
|
+
threshold: float = Field(ge=0.0, le=1.0)
|
|
30
|
+
scope: str
|
|
31
|
+
"""`failed_only` or `all` — what `analyze pull` should bundle for this run."""
|
|
32
|
+
reason: NonEmptyStr
|
|
33
|
+
consumed: bool = False
|
|
34
|
+
"""Set once an `analyze pull` has acted on this staging, so it isn't re-flagged."""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""HTTP bridge between the SQLite-backed storage and the web UI.
|
|
2
|
+
|
|
3
|
+
Read-only for MVP plus two writes: create workspace, queue experiment
|
|
4
|
+
spec. FastAPI is an optional extra (`pip install selfevals[web]`);
|
|
5
|
+
importing this package does not import FastAPI eagerly so that the
|
|
6
|
+
default install path stays slim.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from fastapi import FastAPI
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_app(*, db_path: str | None = None) -> FastAPI:
|
|
18
|
+
"""Build the FastAPI app. Defers the FastAPI import to call time."""
|
|
19
|
+
from selfevals.api.app import build_app
|
|
20
|
+
|
|
21
|
+
return build_app(db_path=db_path)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ["create_app"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""`python -m selfevals.api` — run the FastAPI app via uvicorn."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main(argv: list[str] | None = None) -> int:
|
|
11
|
+
parser = argparse.ArgumentParser(prog="selfevals-api")
|
|
12
|
+
parser.add_argument("--host", default="127.0.0.1")
|
|
13
|
+
parser.add_argument("--port", type=int, default=8000)
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"--db",
|
|
16
|
+
default=os.environ.get("SELFEVALS_DB", "./selfevals.sqlite"),
|
|
17
|
+
help="Path to the SQLite database file.",
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--reload",
|
|
21
|
+
action="store_true",
|
|
22
|
+
help="Enable uvicorn auto-reload (dev only).",
|
|
23
|
+
)
|
|
24
|
+
args = parser.parse_args(argv)
|
|
25
|
+
os.environ["SELFEVALS_DB"] = args.db
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
import uvicorn
|
|
29
|
+
except ImportError as exc:
|
|
30
|
+
print(
|
|
31
|
+
"error: uvicorn is not installed. Install with: pip install selfevals[web]",
|
|
32
|
+
file=sys.stderr,
|
|
33
|
+
)
|
|
34
|
+
raise SystemExit(2) from exc
|
|
35
|
+
|
|
36
|
+
uvicorn.run(
|
|
37
|
+
"selfevals.api.app:build_app",
|
|
38
|
+
host=args.host,
|
|
39
|
+
port=args.port,
|
|
40
|
+
reload=args.reload,
|
|
41
|
+
factory=True,
|
|
42
|
+
)
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__": # pragma: no cover
|
|
47
|
+
raise SystemExit(main())
|