Files
wehub-resource-sync 2c632336aa
CI / Viewer CI (push) Successful in 13m37s
CI / Core CI (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:38 +08:00

396 lines
14 KiB
Python

"""
Copy-edit workflows for existing records.
"""
from __future__ import annotations
import logging
import shutil
from pathlib import Path
from typing import Any, Callable, Optional
from agent.cost import max_cost_usd_from_env
from agent.defaults import resolve_max_turns
from agent.prompts import DESIGNER_PROMPT_NAME, normalize_sdk_package
from agent.run_context import (
RunExecutionOutcome,
_build_single_run_context,
_default_model_id,
_first_string,
_infer_provider_from_model_id,
_optional_max_cost_usd,
_optional_string,
_resolve_runtime_record_author,
)
from agent.single_run import ExecuteSingleRun, execute_single_run
from agent.tools import build_initial_user_content
from storage.records import RecordStore
from storage.repo import StorageRepo
from storage.revisions import (
INITIAL_REVISION_ID,
active_inputs_dir,
active_model_path,
active_prompt_path,
active_provenance_path,
active_revision_id,
revision_relative_path,
sha256_file,
)
from storage.runs import RunStore
logger = logging.getLogger(__name__)
EDIT_RUNTIME_GUIDANCE = """This is an edit request for an existing Articraft asset.
The active parent model.py has already been staged as the editable starting file for this run.
Treat that staged code as the current asset source, preserve the parts that still satisfy the original object, and make the smallest coherent set of changes needed for the new user request.
Do not assume prior conversation history is available; use the staged code, parent metadata, and this edit prompt as the source of truth."""
EXTERNAL_AGENT_DOC_PROMPT_NAME = "EXTERNAL_AGENT_DATA.md"
def _system_prompt_for_internal_edit(
prompting: dict[str, Any], parent_record: dict[str, Any]
) -> str:
stored_prompt = _first_string(prompting.get("system_prompt_file"), DESIGNER_PROMPT_NAME)
creator = parent_record.get("creator")
is_external_parent = isinstance(creator, dict) and creator.get("mode") == "external_agent"
if is_external_parent or stored_prompt == EXTERNAL_AGENT_DOC_PROMPT_NAME:
return DESIGNER_PROMPT_NAME
return stored_prompt
def _parent_input_refs(
repo: StorageRepo,
*,
parent_record_id: str,
parent_revision_id: str,
parent_record: dict[str, Any],
) -> list[dict[str, str]]:
seen: set[tuple[str, str, str]] = set()
refs: list[dict[str, str]] = []
parent_revision = repo.read_json(
repo.layout.record_revision_metadata_path(parent_record_id, parent_revision_id),
default={},
)
inherited = (
parent_revision.get("inherited_inputs")
if isinstance(parent_revision, dict)
and isinstance(parent_revision.get("inherited_inputs"), list)
else []
)
for item in inherited:
if not isinstance(item, dict):
continue
ref_record_id = str(item.get("record_id") or "").strip()
ref_revision_id = str(item.get("revision_id") or "").strip()
ref_path = str(item.get("path") or "").strip()
if not ref_record_id or not ref_revision_id or not ref_path:
continue
key = (ref_record_id, ref_revision_id, ref_path)
if key in seen:
continue
seen.add(key)
ref = {
"record_id": ref_record_id,
"revision_id": ref_revision_id,
"path": ref_path,
}
digest = str(item.get("sha256") or "").strip()
if digest:
ref["sha256"] = digest
refs.append(ref)
inputs_dir = active_inputs_dir(repo, parent_record_id, record=parent_record)
if not inputs_dir.is_dir():
return refs
for path in sorted(item for item in inputs_dir.rglob("*") if item.is_file()):
try:
relative = path.relative_to(inputs_dir)
except ValueError:
continue
ref_path = revision_relative_path(
parent_revision_id,
f"inputs/{relative.as_posix()}",
)
key = (parent_record_id, parent_revision_id, ref_path)
if key in seen:
continue
seen.add(key)
ref: dict[str, str] = {
"record_id": parent_record_id,
"revision_id": parent_revision_id,
"path": ref_path,
}
digest = sha256_file(path)
if digest:
ref["sha256"] = digest
refs.append(ref)
return refs
async def edit_record(
*,
repo_root: Path,
parent_record_id: str,
edit_prompt: str,
data_root: Path | None = None,
image_path: Path | None = None,
provider: str | None = None,
model_id: str | None = None,
thinking_level: str | None = None,
max_turns: int | None = None,
sdk_package: str | None = None,
max_cost_usd: float | None = None,
record_id: str | None = None,
label: str | None = None,
tags: Optional[list[str]] = None,
display_enabled: Optional[bool] = None,
execute_single_run_func: ExecuteSingleRun = execute_single_run,
resolve_record_author_func: Callable[[Path], str | None] = _resolve_runtime_record_author,
) -> RunExecutionOutcome:
normalized_prompt = edit_prompt.strip()
if not normalized_prompt:
return RunExecutionOutcome(
exit_code=1,
run_id="",
record_id=record_id or parent_record_id,
status="failed",
message="Edit prompt is required.",
)
resolved_repo_root = repo_root.resolve()
storage_repo = StorageRepo(resolved_repo_root, data_root=data_root)
storage_repo.ensure_layout()
record_author = resolve_record_author_func(resolved_repo_root)
record_store = RecordStore(storage_repo)
run_store = RunStore(storage_repo)
parent_record = record_store.load_record(parent_record_id)
if not isinstance(parent_record, dict):
return RunExecutionOutcome(
exit_code=1,
run_id="",
record_id=parent_record_id,
status="failed",
message=f"Record not found: {parent_record_id}",
)
parent_revision_id = active_revision_id(
storage_repo,
parent_record_id,
record=parent_record,
)
parent_model_path = active_model_path(storage_repo, parent_record_id, record=parent_record)
if not parent_model_path.is_file():
return RunExecutionOutcome(
exit_code=1,
run_id="",
record_id=parent_record_id,
status="failed",
message=f"Missing parent model.py: {parent_model_path}",
)
parent_prompt_path = active_prompt_path(storage_repo, parent_record_id, record=parent_record)
parent_prompt = (
parent_prompt_path.read_text(encoding="utf-8").strip()
if parent_prompt_path.exists()
else ""
)
provenance_path = active_provenance_path(storage_repo, parent_record_id, record=parent_record)
provenance = storage_repo.read_json(provenance_path)
if not isinstance(provenance, dict):
return RunExecutionOutcome(
exit_code=1,
run_id="",
record_id=parent_record_id,
status="failed",
message=f"Missing provenance.json for record {parent_record_id}",
)
generation = provenance.get("generation")
prompting = provenance.get("prompting")
sdk = provenance.get("sdk")
if (
not isinstance(generation, dict)
or not isinstance(prompting, dict)
or not isinstance(sdk, dict)
):
return RunExecutionOutcome(
exit_code=1,
run_id="",
record_id=parent_record_id,
status="failed",
message=f"Invalid provenance.json for record {parent_record_id}",
)
stored_provider = _first_string(
generation.get("provider"),
_first_string(parent_record.get("provider"), "openai"),
)
stored_model_id = _optional_string(generation.get("model_id"))
stored_openai_transport = _first_string(generation.get("openai_transport"), "http")
stored_thinking_level = _first_string(generation.get("thinking_level"), "high")
stored_max_turns = generation.get("max_turns")
openai_reasoning_summary = (
_optional_string(generation.get("openai_reasoning_summary")) or "auto"
)
system_prompt_path = _system_prompt_for_internal_edit(prompting, parent_record)
resolved_sdk_package = normalize_sdk_package(
sdk_package
if sdk_package is not None
else _first_string(sdk.get("sdk_package"), parent_record.get("sdk_package"))
)
try:
stored_max_cost_usd = _optional_max_cost_usd(
generation.get("max_cost_usd"),
label=f"generation.max_cost_usd for {parent_record_id}",
)
env_max_cost_usd = max_cost_usd_from_env()
except ValueError as exc:
return RunExecutionOutcome(
exit_code=1,
run_id="",
record_id=parent_record_id,
status="failed",
message=str(exc),
)
selected_model_override = _optional_string(model_id) or stored_model_id
selected_provider = (
_optional_string(provider)
or _infer_provider_from_model_id(selected_model_override)
or stored_provider
)
selected_thinking = _optional_string(thinking_level) or stored_thinking_level
selected_model_id = _default_model_id(
provider=selected_provider,
model_id=selected_model_override,
thinking_level=selected_thinking,
openai_transport=stored_openai_transport,
openai_reasoning_summary=openai_reasoning_summary,
)
resolved_max_turns = resolve_max_turns(
model_id=selected_model_id,
max_turns=(
max_turns
if max_turns is not None
else stored_max_turns
if isinstance(stored_max_turns, int)
else None
),
)
if max_cost_usd is not None:
resolved_max_cost_usd = max_cost_usd
elif stored_max_cost_usd is not None:
resolved_max_cost_usd = stored_max_cost_usd
else:
resolved_max_cost_usd = env_max_cost_usd
if record_id and storage_repo.layout.record_dir(record_id).exists():
return RunExecutionOutcome(
exit_code=1,
run_id="",
record_id=record_id,
status="failed",
message=f"Record already exists: {record_id}",
)
target_record_id = record_id or None
revision_id = INITIAL_REVISION_ID
existing_record = None
category_slug = _optional_string(parent_record.get("category_slug"))
inherited_label = label if label is not None else _optional_string(parent_record.get("label"))
parent_tags = parent_record.get("tags")
inherited_tags = (
list(tags)
if tags is not None
else ([str(tag) for tag in parent_tags] if isinstance(parent_tags, list) else [])
)
parent_lineage = (
parent_record.get("lineage") if isinstance(parent_record.get("lineage"), dict) else {}
)
lineage = {
"origin_record_id": _optional_string(parent_lineage.get("origin_record_id"))
or parent_record_id,
"parent_record_id": parent_record_id,
"parent_revision_id": parent_revision_id,
"edit_mode": "copy",
}
context = _build_single_run_context(
repo_root=resolved_repo_root,
prompt=normalized_prompt,
storage_repo=storage_repo,
record_id=target_record_id,
revision_id=revision_id,
)
if storage_repo.layout.record_dir(context.record_id).exists():
return RunExecutionOutcome(
exit_code=1,
run_id=context.run_id,
record_id=context.record_id,
status="failed",
message=f"Record already exists: {context.record_id}",
)
context.script_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(parent_model_path, context.script_path)
inherited_inputs = _parent_input_refs(
storage_repo,
parent_record_id=parent_record_id,
parent_revision_id=parent_revision_id,
parent_record=parent_record,
)
revision_parent = {"record_id": parent_record_id, "revision_id": parent_revision_id}
revision_seed = {
"record_id": parent_record_id,
"revision_id": parent_revision_id,
"artifact": "model.py",
}
parent_prompt_line = f"\nParent prompt: {parent_prompt}" if parent_prompt else ""
user_content = build_initial_user_content(
normalized_prompt,
image_path=image_path,
runtime_guidance_text=(
f"{EDIT_RUNTIME_GUIDANCE}\n"
f"Parent record: {parent_record_id}\n"
f"Parent revision: {parent_revision_id}"
f"{parent_prompt_line}"
),
)
outcome = await execute_single_run_func(
user_content,
prompt_text=normalized_prompt,
display_prompt=normalized_prompt,
resolved_repo_root=resolved_repo_root,
storage_repo=storage_repo,
record_store=record_store,
run_store=run_store,
image_path=image_path,
provider=selected_provider,
model_id=selected_model_override,
openai_transport=stored_openai_transport,
thinking_level=selected_thinking,
max_turns=resolved_max_turns,
system_prompt_path=system_prompt_path,
display_enabled=display_enabled,
sdk_package=resolved_sdk_package,
openai_reasoning_summary=openai_reasoning_summary,
max_cost_usd=resolved_max_cost_usd,
label=inherited_label,
tags=inherited_tags,
category_slug=category_slug,
run_mode="library_single",
context=context,
existing_record=existing_record,
record_author=record_author,
lineage=lineage,
revision_parent=revision_parent,
revision_seed=revision_seed,
inherited_inputs=inherited_inputs,
)
return outcome