refactor: preserve auto resource compatibility

This commit is contained in:
wang-qisen 2026-08-31 20:11:14 +08:00
parent 1c9101aa72
commit b3b9dd7cdf
5 changed files with 112 additions and 11 deletions

View file

@ -140,6 +140,14 @@ class BaseAutoResourceStep(BaseStep):
return before_bytes is not None
return after_path != before_path or before_bytes != after_bytes
async def _refresh_day_index(self, day: str) -> dict:
"""Refresh and return the derived daily index for a resource-note change."""
daily_dir = self.config_value("daily_dir")
self.logger.info(f"[{self.name}] refresh index start date={day} daily_dir={daily_dir}")
index_payload = await refresh_day_index(self.file_store, day, daily_dir)
self.logger.info(f"[{self.name}] refresh index done date={day}")
return index_payload
def _find_resource_note(self, notes: list[dict], file_path: str, fallback_path: str) -> dict | None:
source = self._source_resource_link(file_path)
for note in notes:
@ -259,9 +267,7 @@ class BaseAutoResourceStep(BaseStep):
await self.file_store.delete([note_rel])
self.logger.info(f"[{self.name}] catalog delete done note={note_rel}")
self.logger.info(f"[{self.name}] refresh index start date={date_str} daily_dir={daily_dir}")
index_payload = await refresh_day_index(self.file_store, date_str, daily_dir)
self.logger.info(f"[{self.name}] refresh index done date={date_str}")
index_payload = await self._refresh_day_index(date_str)
self.context.response.success = True
self.context.response.answer = f"Deleted resource note: {note_rel}"

View file

@ -11,7 +11,7 @@ from agentscope.message import Base64Source, DataBlock, TextBlock, UserMsg
from agentscope.model import ChatModelBase
from pydantic import BaseModel, Field
from ..file_io import is_image_file, refresh_day_index
from ..file_io import is_image_file
from ..file_io._path import IMAGE_MIME_BY_EXT, IMAGE_SUFFIXES
from ._auto_resource import _SOURCE_RESOURCE_KEY, _sanitize_note_name, BaseAutoResourceStep
from ...components import R
@ -477,7 +477,7 @@ class AutoImageResourceStep(BaseAutoResourceStep):
modified = self._note_modified(before_note_path, before_note_bytes, note_path)
self.context.response.metadata.update({"path": note_path, "created": note_created, "modified": modified})
index_payload = await refresh_day_index(self.file_store, date_str, daily_dir)
index_payload = await self._refresh_day_index(date_str)
self.context.response.success = True
self.context.response.answer = f"Captioned image resource {file_path} -> {note_path}"

View file

@ -4,6 +4,7 @@ import copy
import inspect
from ...components import R
from ...enumeration import ComponentEnum
from ..base_step import BaseStep
from ._auto_resource import BaseAutoResourceStep, _results_answer
@ -35,7 +36,23 @@ class AutoResourceStep(BaseStep):
"""Resolve configured processors and allocate their per-invocation batches."""
raw_specs = self.dispatch_step_specs
if not raw_specs:
raise RuntimeError("AutoResourceStep requires resource processors in dispatch_steps")
registry = self.app_context.registry if self.app_context is not None else R
raw_specs = [
backend
for backend, step_cls in registry.get_all(ComponentEnum.STEP).items()
if isinstance(step_cls, type)
and issubclass(step_cls, BaseAutoResourceStep)
and step_cls.resource_fallback
]
if len(raw_specs) != 1:
candidates = ", ".join(sorted(raw_specs)) or "none"
raise RuntimeError(
"AutoResourceStep without dispatch_steps requires exactly one registered "
f"fallback resource processor; found: {candidates}",
)
self.logger.warning(
f"[{self.name}] dispatch_steps omitted; using registered fallback processor={raw_specs[0]}",
)
routes: list[_ProcessorRoute] = []
fallback_indexes: list[int] = []

View file

@ -5,7 +5,6 @@ import uuid
import aiofiles
from ...components import R
from ..file_io import refresh_day_index
from ._evolve import agent_reply_result_text
from ._auto_resource import BaseAutoResourceStep
@ -176,9 +175,7 @@ class AutoTextResourceStep(BaseAutoResourceStep):
return
modified = self._note_modified(before_note_path, before_note_bytes, note_path)
self.logger.info(f"[{self.name}] refresh index start date={date_str} daily_dir={daily_dir}")
index_payload = await refresh_day_index(self.file_store, date_str, daily_dir)
self.logger.info(f"[{self.name}] refresh index done date={date_str}")
index_payload = await self._refresh_day_index(date_str)
self.context.response.success = True
self.context.response.answer = agent_reply_result_text(result)

View file

@ -28,7 +28,9 @@ from PIL import Image
from reme.components import R
from reme.components.agent_wrapper import BaseAgentWrapper
from reme.components.component_registry import ComponentRegistry
from reme.components.file_store import LocalFileStore
from reme.components.job import BaseJob
from reme.components.runtime_context import RuntimeContext
from reme.enumeration import ComponentEnum
from reme.steps.evolve._auto_resource import BaseAutoResourceStep
@ -725,6 +727,85 @@ def test_auto_resource_router_requires_the_fallback_processor_to_be_last():
step._processor_routes()
def test_auto_resource_router_discovers_unique_fallback_for_legacy_config():
"""An old Step spec without dispatch_steps keeps its text-resource behavior."""
app_ctx = _make_app_context(Path.cwd())
app_ctx.registry = R.copy()
step = AutoResourceStep(app_context=app_ctx)
routes = step._processor_routes()
assert [(spec, step_cls) for spec, step_cls, _ in routes] == [
({"backend": "auto_text_resource_step"}, AutoTextResourceStep),
]
def test_auto_resource_legacy_job_config_dispatches_text_resource():
"""A real BaseJob accepts the pre-router auto_resource Step configuration."""
async def run():
with tempfile.TemporaryDirectory() as tmpdir, temp_chdir(tmpdir):
cwd = Path.cwd()
app_ctx = _make_app_context(cwd)
app_ctx.registry = R.copy()
fs = LocalFileStore(name="test_store", embedding_store="")
await fs.start()
_install_file_jobs(app_ctx, fs)
wrapper = _FakeAgentWrapper()
job = BaseJob(
name="legacy_auto_resource",
app_context=app_ctx,
steps=[
{
"backend": "auto_resource_step",
"file_store": fs,
"agent_wrapper": wrapper,
},
],
)
await job.start()
try:
source = cwd / "resource" / "2026-01-01" / "legacy.txt"
source.parent.mkdir(parents=True, exist_ok=True)
source.write_text("legacy text resource", encoding="utf-8")
response = await job(changes=[{"change": "added", "path": str(source)}])
assert response.success is True
assert response.metadata["processed"] == 1
assert response.metadata["results"][0]["success"] is True
assert "legacy text resource" in wrapper.inputs
finally:
await job.close()
await fs.close()
asyncio.run(run())
def test_auto_resource_router_rejects_ambiguous_registered_fallbacks():
"""Legacy discovery fails closed when plugins register multiple fallbacks."""
app_ctx = _make_app_context(Path.cwd())
app_ctx.registry = R.copy()
app_ctx.registry.add("second_text_fallback", AutoTextResourceStep, owner=__name__)
step = AutoResourceStep(app_context=app_ctx)
with pytest.raises(RuntimeError, match="exactly one registered fallback resource processor"):
step._processor_routes()
explicit = AutoResourceStep(app_context=app_ctx, dispatch_steps=["auto_text_resource_step"])
assert len(explicit._processor_routes()) == 1
def test_auto_resource_router_rejects_missing_registered_fallback():
"""Legacy discovery fails clearly when no fallback processor is installed."""
app_ctx = _make_app_context(Path.cwd())
app_ctx.registry = ComponentRegistry()
step = AutoResourceStep(app_context=app_ctx)
with pytest.raises(RuntimeError, match=r"fallback resource processor; found: none"):
step._processor_routes()
def test_resource_processors_have_canonical_registrations_and_isolated_prompts():
"""Each modality owns one backend and loads only its module-local prompts."""
assert R.get(ComponentEnum.STEP, "auto_resource_step") is AutoResourceStep
@ -875,7 +956,7 @@ def test_auto_image_reports_modified_when_index_refresh_fails_after_write():
async def fail_refresh(*_args, **_kwargs):
raise RuntimeError("index refresh failed")
with patch("reme.steps.evolve.auto_image_resource.refresh_day_index", new=fail_refresh):
with patch("reme.steps.evolve._auto_resource.refresh_day_index", new=fail_refresh):
resp = await _run_step(step, [{"change": "added", "path": str(source)}])
result = resp.metadata["results"][0]