feat(research): streamline template details and enable bot versioning
Deploy production / deploy (push) Successful in 1m35s
Deploy production / deploy (push) Successful in 1m35s
This commit is contained in:
@@ -133,6 +133,20 @@ class CreateTemplate(Contract):
|
||||
return self
|
||||
|
||||
|
||||
class CreateTemplateVersion(CreateTemplate):
|
||||
template_id: RunId
|
||||
expected_version: int = Field(ge=1)
|
||||
|
||||
|
||||
class TemplateRead(Contract):
|
||||
template_id: RunId
|
||||
version: int | None = Field(default=None, ge=1)
|
||||
|
||||
|
||||
class TemplateSearch(Page):
|
||||
q: str = Field(default="", max_length=200)
|
||||
|
||||
|
||||
class CatalogSearch(Contract):
|
||||
filters: CatalogFilters
|
||||
dataset_id: str | None = Field(default=None, min_length=1, max_length=200)
|
||||
|
||||
@@ -81,6 +81,8 @@ class ResearchAccess(SuperResearchAccess):
|
||||
"metadata_only": True, "actual_platform_allowance": None,
|
||||
"templates": {
|
||||
"create_with": "create_research_template", "required_scope": "research:write",
|
||||
"version_with": "create_research_template_version",
|
||||
"read_with": "get_research_template", "search_with": "search_research_templates",
|
||||
"authored_by": "caller", "max_source_items": 20,
|
||||
"source_items_with": "get_backtest_results", "starts_backtests": False,
|
||||
"web_url": f"{self.public_origin}/#templates",
|
||||
@@ -285,6 +287,30 @@ class ResearchAccess(SuperResearchAccess):
|
||||
return await self.remember("create_research_template", args, digest, result,
|
||||
business_id=result["template_id"])
|
||||
|
||||
async def template(self, args):
|
||||
"""Read the exact saved template revision without executing research."""
|
||||
from ..research.assets import Assets
|
||||
|
||||
return await Assets(self.db).get(args.template_id, args.version, "template")
|
||||
|
||||
async def templates(self, args):
|
||||
"""Search the same template library used by the browser."""
|
||||
from ..research.assets import Assets
|
||||
|
||||
return await Assets(self.db).list("template", **args.model_dump())
|
||||
|
||||
async def create_template_version(self, args):
|
||||
"""Append an idempotent, optimistic revision with refreshed source evidence."""
|
||||
from .templates import create_template
|
||||
|
||||
operation = "create_research_template_version"
|
||||
previous, digest = await self.previous(operation, args)
|
||||
if previous:
|
||||
return previous.response
|
||||
result = await create_template(self.db, args, self.principal, asset_id=args.template_id)
|
||||
result["web_url"] = f"{self.public_origin}/#templates"
|
||||
return await self.remember(operation, args, digest, result, business_id=args.template_id)
|
||||
|
||||
async def previous(self, operation, args):
|
||||
# PostgreSQL row lock is shared with HTTP start and catalog/job creation.
|
||||
account = await self.db.scalar(select(Account).where(Account.id == self.principal.account_id).with_for_update())
|
||||
|
||||
@@ -10,23 +10,27 @@ from ..research.workspace_contracts import AssetWrite
|
||||
from .queries import item_summary
|
||||
|
||||
|
||||
async def create_template(db, args, principal):
|
||||
"""Save a new asset inside the caller's account-locked, idempotent transaction.
|
||||
async def create_template(db, args, principal, *, asset_id=None):
|
||||
"""Save a new asset or revision inside the caller's account-locked, idempotent transaction.
|
||||
|
||||
Args contain the external model's TemplateSpec and local source item IDs.
|
||||
Return the versioned asset and theoretical combination count. Raise
|
||||
ResearchError for name conflicts or missing/incomplete research evidence.
|
||||
Stored evidence proves provenance, not profitability or platform eligibility;
|
||||
schema validation does not validate every expanded FASTEXPR combination.
|
||||
Optional asset_id selects a version update guarded by args.expected_version;
|
||||
a stale version raises HTTP 409 and cannot overwrite a historical revision.
|
||||
"""
|
||||
from .service import ResearchError
|
||||
|
||||
existing = await db.scalar(select(ResearchAsset).where(
|
||||
ResearchAsset.kind == "template", ResearchAsset.name == args.template.name,
|
||||
ResearchAsset.id != asset_id if asset_id else True,
|
||||
).order_by(ResearchAsset.id).limit(1))
|
||||
if existing:
|
||||
raise ResearchError("TEMPLATE_NAME_CONFLICT", "模板名称已存在,请使用新名称;此工具不覆盖已有模板",
|
||||
affected_items=[{"template_id": existing.id, "version": existing.version}])
|
||||
previous = await Assets(db).get(asset_id, expected_kind="template") if asset_id else None
|
||||
rows = (await db.execute(select(BacktestItem, BacktestResult).outerjoin(
|
||||
BacktestResult, BacktestResult.item_id == BacktestItem.id,
|
||||
).where(BacktestItem.id.in_(args.source_item_ids)))).all()
|
||||
@@ -48,12 +52,16 @@ async def create_template(db, args, principal):
|
||||
"admin_id": principal.admin_id,
|
||||
"source_items": [item_summary(*found[item_id]) for item_id in args.source_item_ids],
|
||||
}
|
||||
if previous:
|
||||
provenance["parent_template"] = {"id": asset_id, "version": args.expected_version}
|
||||
asset = await Assets(db).save(AssetWrite(
|
||||
kind="template", content=args.template.model_dump(mode="json"),
|
||||
), provenance=provenance)
|
||||
version=args.expected_version if asset_id else None,
|
||||
), asset_id=asset_id, provenance=provenance)
|
||||
return {
|
||||
**asset, "template_id": asset["id"],
|
||||
"combination_count": str(math.prod(len(v.values) for v in args.template.variables.values())),
|
||||
"combination_count": (str(math.prod(len(v.values) for v in args.template.variables.values()))
|
||||
if all(v.values for v in args.template.variables.values()) else None),
|
||||
"validation": {"structure": "valid", "source_evidence": "recorded",
|
||||
"expanded_candidates": "not_validated", "platform_semantics": "unknown"},
|
||||
"next_step": "在模板工坊选择固定输入及模拟设置,展开并核验候选,再确认批量回测。",
|
||||
|
||||
Reference in New Issue
Block a user