feat: wire image resources into the resource watch loop

This commit is contained in:
wang-qisen 2026-08-27 17:38:21 +08:00
parent 1c4e13270b
commit 3ddd06c085
5 changed files with 90 additions and 5 deletions

View file

@ -49,8 +49,21 @@ workspace/
meeting-notes.csv
```
The current Beta version is best suited to text-based resources such as `md`, `txt`, `json`, `jsonl`, `csv`, `yaml`, and
`html`.
Text resources such as `md`, `txt`, `json`, `jsonl`, `csv`, `yaml`, and `html` are the primary fit. Image resources
(`png`, `jpg`, `jpeg`, `webp`, `gif`, `bmp`, `tiff`, `heic`) produce caption cards as described in
[Image Resources](#image-resources).
## Image Resources
Image files are interpreted the same way: a vision model writes a caption card that links back to the original image.
The card body starts with an `![[resource/...]]` embed link and the frontmatter carries `kind: image` and `media_type`,
so text search reaches image content through the caption.
The vision model is the `vision` instance of `as_llm` when configured, and otherwise falls back to the `default`
instance — a multimodal default model needs no extra configuration. Images larger than the request budget or in
provider-unfriendly formats are downscaled or re-encoded in memory for the request only; the original file under
`resource/` is never modified. When an image changes, its card is rewritten in place; when the image is deleted, the
card is removed with it.
## Resource Cards

View file

@ -46,7 +46,13 @@ workspace/
meeting-notes.csv
```
当前 Beta 版本更适合处理文本类资源,例如 `md``txt``json``jsonl``csv``yaml``html`
当前 Beta 版本以文本类资源为主,例如 `md``txt``json``jsonl``csv``yaml``html`;图像资源(`png``jpg``jpeg``webp``gif``bmp``tiff``heic`)会生成 caption 卡片,见下文[图像资源](#图像资源)一节。
## 图像资源
图像文件的解读方式相同:视觉模型写入一张 caption 卡片并链接原图。卡片正文以 `![[resource/...]]` 嵌入链接开头frontmatter 携带 `kind: image``media_type`,文本检索因此可以通过 caption 命中图像内容。
视觉模型优先使用配置中的 `as_llm` `vision` 实例,未配置时回退到 `default` 实例——默认模型具备视觉能力时无需额外配置。超过请求预算或格式不被模型接受的图像,仅在请求前于内存中降采样或转码;`resource/` 下的原图文件不会被修改。图像变更时卡片原地重写;图像删除时卡片随之删除。
## 资源卡片

View file

@ -30,6 +30,8 @@ dependencies = [
"mistletoe>=1.5.1",
"numpy>=2.2.6",
"openai>=2.26.0",
"pillow>=10.0.0",
"pillow-heif>=0.13.0",
"psutil>=5.9",
"pydantic>=2.12.5",
"python-frontmatter>=1.1.0",

View file

@ -20,7 +20,7 @@ jobs:
resource_watch_loop:
backend: background
watch_dirs: [resource_dir]
watch_suffixes: [md, txt, json, jsonl, csv, yaml, html]
watch_suffixes: [md, txt, json, jsonl, csv, yaml, html, png, jpg, jpeg, webp, gif, bmp, tiff, heic]
steps:
- backend: init_changes_step
monitor_type: file_catalog
@ -29,11 +29,13 @@ jobs:
- backend: update_catalog_step
file_catalog: resource
- backend: auto_resource_step
- backend: auto_image_step
- backend: watch_changes_step
dispatch_steps:
- backend: update_catalog_step
file_catalog: resource
- backend: auto_resource_step
- backend: auto_image_step
digest_watch_loop:
backend: background
@ -186,6 +188,30 @@ jobs:
steps:
- backend: auto_resource_step
auto_image:
backend: base
description: "Auto-image: interpret image resource files into daily caption notes"
parameters:
type: object
properties:
changes:
type: array
description: "image resource change batch, each item has path/file_path and change"
items:
type: object
properties:
path:
type: string
file_path:
type: string
change:
type: string
description: "added/modified/deleted"
required:
- changes
steps:
- backend: auto_image_step
proactive:
backend: base
description: "Proactive: read daily/<date>/interests.yaml and expose the latest user-interest topics."
@ -755,6 +781,17 @@ components:
max_tokens: 65536
thinking_enable: false
# Optional dedicated vision model for image resources (auto_image_step).
# Falls back to the "default" instance above when absent; uncomment to
# decouple the vision model from the main LLM.
# vision:
# backend: ${VLM_BACKEND:-openai}
# model: ${VLM_MODEL_NAME:-}
# stream: false
# credential:
# api_key: ${VLM_API_KEY:-}
# base_url: ${VLM_BASE_URL:-}
agent_wrapper:
default:
backend: agentscope

View file

@ -11,7 +11,7 @@ import frontmatter
from watchfiles import Change
from ..base_step import BaseStep
from ..file_io import refresh_day_index, validate_filename_component
from ..file_io import is_image_file, refresh_day_index, validate_filename_component
from ...components import R
from ._evolve import agent_reply_result_text, now
@ -473,6 +473,10 @@ class AutoResourceStep(BaseStep):
)
self.logger.info(f"[{self.name}] done {note_path} modified={modified}")
def _skip_image_change(self, file_path: str) -> bool:
"""Return True for image changes, which auto_image_step interprets."""
return is_image_file(file_path)
async def _handle_change(self, file_path: str, raw_change) -> dict:
assert self.context is not None
# Handlers write item-scoped fields into the shared response. Start each
@ -509,6 +513,29 @@ class AutoResourceStep(BaseStep):
note_stem = _compute_note_stem(filename)
self.logger.info(f"[{self.name}] {change.name} file_path={file_path} note_stem={note_stem}")
if self._skip_image_change(file_path):
# Image resources are binary; the text interpretation below would
# read them as mojibake. They are handled by auto_image_step.
answer = f"Skipped image resource file: {file_path}"
self.context.response.success = True
self.context.response.answer = answer
self.context.response.metadata.update(
{
"path": file_path,
"action": "skipped",
"reason": "image_file",
"modified": False,
},
)
self.logger.info(f"[{self.name}] skip change file_path={file_path} reason=image_file")
return {
"success": True,
"path": file_path,
"change": change.name,
"answer": answer,
"metadata": dict(self.context.response.metadata),
}
if change == Change.deleted:
await self._handle_delete(file_path, date_str, note_stem)
else: