feat: integrate chatbox research with datasets and backtests

This commit is contained in:
yuxuanhui
2026-09-08 12:43:00 +08:00
parent 43336ad960
commit aef8e1d310
37 changed files with 1421 additions and 42 deletions
+1
View File
@@ -50,6 +50,7 @@ class Source(Contract):
template_input_id: str | None = Field(default=None, max_length=200)
research_id: str | None = Field(default=None, max_length=200)
parent_run_id: str | None = Field(default=None, max_length=36)
hypothesis: str | None = Field(default=None, max_length=2000)
class DraftInput(Contract):
+18 -1
View File
@@ -3,6 +3,8 @@
from fastapi import APIRouter, Depends, Query, Request
from ..business import Business
from ..research.contracts import ResearchPreviewInput
from ..research.service import ResearchBuilder
from ..security import require_auth
from .contracts import (
ControlInput,
@@ -75,6 +77,13 @@ async def preview(body: PreviewInput, request: Request):
return await Business(db).backtests.preview(body)
@router.post("/research-previews", status_code=201, response_model=PreviewOutput)
async def research_preview(body: ResearchPreviewInput, request: Request):
"""Prepare typed field bindings for any research producer; never start a simulation."""
async with request.app.state.sessions.begin() as db:
return await ResearchBuilder(db, Business(db).backtests).prepare(body)
@router.get("/previews/{preview_id}", response_model=PreviewOutput)
async def get_preview(
preview_id: str, request: Request, limit: int = Query(25, ge=1, le=100), offset: int = Query(0, ge=0)
@@ -97,9 +106,17 @@ async def runs(
limit: int = Query(25, ge=1, le=100),
offset: int = Query(0, ge=0),
source: str | None = Query(None, max_length=100),
reference: str | None = Query(None, max_length=200),
research_id: str | None = Query(None, max_length=200),
):
async with request.app.state.sessions() as db:
return await Business(db).backtests.runs(limit, offset, source)
return await Business(db).backtests.runs(limit, offset, source, reference, research_id)
@router.get("/sources", response_model=list[str])
async def sources(request: Request):
async with request.app.state.sessions() as db:
return await Business(db).backtests.sources()
@router.get("/runs/{run_id}", response_model=RunOutput)
+26 -6
View File
@@ -161,9 +161,22 @@ class Backtests:
{k: getattr(row, k) for k in ("id", "version", "name", "source", "candidates", "updated_at")}
)
async def preview(self, body):
async def preview(self, body, *, preserve_source=False):
"""Fix inputs; new chatbox candidates inherit trusted generating-run provenance.
Existing draft references and server-side subsets/reruns retain their
producer. ai_context separately identifies whoever starts the execution.
"""
if body.inline:
data = body.inline.model_dump(mode="json")
if self.ai_context and not preserve_source:
data["source"] = {
**data["source"],
"kind": "chatbox",
"reference": self.ai_context["conversation_id"],
"research_id": self.ai_context["ai_run_id"],
"parent_run_id": None,
}
else:
draft = await self.db.scalar(
select(BacktestDraft).where(BacktestDraft.id == body.draft_id).with_for_update()
@@ -316,10 +329,11 @@ class Backtests:
await self.db.flush()
return await self.run(run.id)
async def runs(self, limit=25, offset=0, source=None):
async def runs(self, limit=25, offset=0, source=None, reference=None, research_id=None):
query = select(BacktestRun)
if source:
query = query.where(BacktestRun.source["kind"].as_string() == source)
for key, value in (("kind", source), ("reference", reference), ("research_id", research_id)):
if value:
query = query.where(BacktestRun.source[key].as_string() == value)
total = await self.db.scalar(select(func.count()).select_from(query.subquery()))
rows = (
await self.db.scalars(
@@ -333,6 +347,10 @@ class Backtests:
"offset": offset,
}
async def sources(self):
kinds = await self.db.scalars(select(BacktestRun.source["kind"].as_string()).distinct())
return sorted({kind for kind in kinds if kind} | {"chatbox", "manual"})
async def run(self, run_id):
row = await self.db.get(BacktestRun, run_id)
if not row:
@@ -562,7 +580,8 @@ class Backtests:
for r in selected
],
)
)
),
preserve_source=True,
)
async def attach_reference(self, attempt_id, body):
@@ -603,5 +622,6 @@ class Backtests:
if not candidates:
raise HTTPException(422, "至少保留一条候选")
return await self.preview(
PreviewInput(inline=DraftInput(name=parent.name, source=parent.source, candidates=candidates))
PreviewInput(inline=DraftInput(name=parent.name, source=parent.source, candidates=candidates)),
preserve_source=True,
)