| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451 |
- from __future__ import annotations
- import hashlib
- import json
- import tempfile
- import unittest
- from pathlib import Path
- from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
- from langchain_core.tools import tool
- from production_build_agents.capabilities import CAPABILITIES
- from production_build_agents.preprocess.brief_access import (
- build_allowed_source_paths,
- load_production_brief,
- resolve_direct_json_path,
- )
- from production_build_agents.contracts.models import (
- CandidateArtifact,
- ExecutorCandidate,
- ArtifactBindingClaim,
- )
- from production_build_agents.agents.executor.agent import (
- ExecutorOutputError,
- run_executor_agent,
- )
- from production_build_agents.agents.executor.context import (
- build_executor_system_prompt,
- build_executor_user_payload,
- create_executor_run_id,
- load_task_contract,
- )
- from production_build_agents.agents.executor.provenance import (
- ExecutorProvenanceError,
- build_tool_call_records,
- validate_candidate_provenance,
- )
- from production_build_agents.agents.executor.skills.registry import (
- ExecutorSkillError,
- load_executor_skill,
- )
- from production_build_agents.run.records import save_executor_delivery
- from production_build_agents.run.langgraph_checkpointer import (
- checkpoint_messages,
- create_sqlite_checkpointer,
- )
- from production_build_agents.tools.registry import ToolRegistry, create_default_tool_registry
- from tests.support.executor_fixtures import (
- build_executor_candidate,
- build_executor_model,
- )
- from tests.support.fake_models import ToolAwareFakeChatModel
- from tests.support.planner_fixtures import (
- build_planned_task,
- build_task_package,
- )
- BRIEF_PATH = (
- Path(__file__).parents[1] / "fixtures" / "minimal_brief.json"
- )
- IMAGE_URL = "https://example.test/generated-reference.png"
- SOURCE_URL = "https://example.test/research-source"
- VIDEO_URL = "https://example.test/generated-video.mp4"
- FRAME_URL = "https://example.test/generated-video-frame.jpg"
- _FAKE_MEDIA_TEMP = tempfile.TemporaryDirectory()
- _FAKE_MEDIA_DIR = Path(_FAKE_MEDIA_TEMP.name)
- def _fake_media_path(source: str, suffix: str) -> Path:
- path = _FAKE_MEDIA_DIR / (
- hashlib.sha256(source.encode("utf-8")).hexdigest() + suffix
- )
- path.write_bytes(source.encode("utf-8"))
- return path
- def _fake_tool_registry() -> ToolRegistry:
- @tool("search_tool")
- def search_tool(query: str, limit: int = 5) -> dict:
- """发现测试工具。"""
- return {
- "success": True,
- "results": [{"tool_id": "fake-tool", "summary": query}],
- "_duration_ms": 3,
- }
- @tool("inspect_tool")
- def inspect_tool(tool_ids: list[str]) -> dict:
- """返回测试工具参数。"""
- return {
- "success": True,
- "tools": {tool_id: {"input_schema": {}} for tool_id in tool_ids},
- "_duration_ms": 4,
- }
- @tool("run_tool")
- def run_tool(tool_id: str, params: dict) -> dict:
- """执行测试工具。"""
- url = params.get("result_url") or {
- "image": IMAGE_URL,
- "video": VIDEO_URL,
- }.get(params.get("kind"), SOURCE_URL)
- return {
- "success": True,
- "tool_id": tool_id,
- "url": url,
- "_duration_ms": 5,
- }
- @tool("view_images")
- def view_images(image_sources: list[str]) -> dict:
- """查看测试图片。"""
- return {
- "success": True,
- "images": [
- {
- "source": source,
- "local_path": str(
- _fake_media_path(source, ".png")
- ),
- "data_url": (
- "data:image/png;base64,"
- "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
- ),
- }
- for source in image_sources
- ],
- "_duration_ms": 6,
- }
- @tool("crop_image")
- def crop_image(image: str, box: list[float]) -> dict:
- """裁剪测试图片。"""
- return {"success": True, "image_urls": [image]}
- @tool("grid_collage")
- def grid_collage(images: list[str]) -> dict:
- """拼接测试图片。"""
- return {"success": True, "image_urls": images[:1]}
- @tool("overlay_text")
- def overlay_text(image: str, texts: list[dict]) -> dict:
- """叠加测试文字。"""
- return {"success": True, "image_urls": [image]}
- @tool("extract_frames")
- def extract_frames(video: str, num_frames: int = 5) -> dict:
- """抽取测试视频帧。"""
- return {
- "success": True,
- "source": video,
- "local_path": str(_fake_media_path(video, ".mp4")),
- "frames": [
- {
- "source": FRAME_URL,
- "timestamp_sec": 0,
- }
- ],
- "_duration_ms": 7,
- }
- @tool("video_trim")
- def video_trim(video: str, start_sec: float = 0) -> dict:
- """裁剪测试视频。"""
- return {"success": True, "video_urls": [video]}
- @tool("video_concat")
- def video_concat(videos: list[str]) -> dict:
- """拼接测试视频。"""
- return {"success": True, "video_urls": videos[:1]}
- @tool("video_mux_audio")
- def video_mux_audio(video: str, audio: str) -> dict:
- """合入测试音频。"""
- return {"success": True, "video_urls": [video]}
- tools = [
- search_tool,
- inspect_tool,
- run_tool,
- view_images,
- crop_image,
- grid_collage,
- overlay_text,
- extract_frames,
- video_trim,
- video_concat,
- video_mux_audio,
- ]
- return ToolRegistry({item.name: item for item in tools})
- def _tool_call(name: str, args: dict, call_id: str) -> AIMessage:
- return AIMessage(
- content="",
- tool_calls=[
- {
- "name": name,
- "args": args,
- "id": call_id,
- "type": "tool_call",
- }
- ],
- )
- class ExecutorSkillTest(unittest.TestCase):
- def test_generic_skills_have_distinct_tool_boundaries(self) -> None:
- analysis = load_executor_skill("structured-analysis")
- research = load_executor_skill("external-research")
- image = load_executor_skill("image-production")
- video = load_executor_skill("video-production")
- reference = load_executor_skill("reference-inspection")
- self.assertEqual(analysis.allowed_tools, ())
- self.assertEqual(
- research.allowed_tools,
- ("search_tool", "inspect_tool", "run_tool"),
- )
- self.assertIn("view_images", image.allowed_tools)
- self.assertIn("crop_image", image.allowed_tools)
- self.assertEqual(
- image.allowed_remote_tool_ids,
- (
- "seedream_generate_image",
- "gpt-image",
- "nano_banana_image",
- ),
- )
- self.assertIn("extract_frames", video.allowed_tools)
- self.assertEqual(
- video.allowed_remote_tool_ids,
- ("seedance_generate_video",),
- )
- self.assertTrue(
- research.verification_policy.require_inspect_before_run
- )
- self.assertEqual(
- image.verification_policy.final_artifact_check_tool,
- "view_images",
- )
- self.assertEqual(
- video.verification_policy.final_artifact_check_tool,
- "extract_frames",
- )
- self.assertEqual(
- reference.allowed_tools,
- ("probe_media", "view_images", "extract_frames"),
- )
- self.assertEqual(
- set(CAPABILITIES[reference.skill_id].artifact_types),
- {"image", "video", "audio"},
- )
- def test_unknown_skill_is_rejected(self) -> None:
- with self.assertRaisesRegex(ExecutorSkillError, "未注册"):
- load_executor_skill("unknown")
- class ExecutorContextTest(unittest.TestCase):
- def test_model_receives_only_requested_brief_excerpt(self) -> None:
- with tempfile.TemporaryDirectory() as temp_dir:
- task = build_task_package(
- BRIEF_PATH,
- planned_task=build_planned_task(
- source_paths=["$.核心制作点[0]"]
- ),
- plan_path=Path(temp_dir) / "plan.json",
- )
- brief, plan, planned_task = load_task_contract(task)
- expectation = plan.stage_requirements[
- 0
- ].artifact_expectations[0]
- sibling = expectation.model_copy(
- update={
- "expectation_id": "Requirement1-Expectation2",
- "usage_scope": "不属于当前 Task 的兄弟期待",
- }
- )
- requirement = plan.stage_requirements[0].model_copy(
- update={
- "artifact_expectations": [expectation, sibling]
- }
- )
- plan = plan.model_copy(
- update={"stage_requirements": [requirement]}
- )
- Path(task.plan_uri).write_text(
- plan.model_dump_json(indent=2),
- encoding="utf-8",
- )
- brief, plan, planned_task = load_task_contract(task)
- run_id = create_executor_run_id(task)
- payload = build_executor_user_payload(
- executor_run_id=run_id,
- task=task,
- brief=brief,
- plan=plan,
- planned_task=planned_task,
- )
- self.assertEqual(
- set(payload["production_brief_excerpt"]),
- {"$.核心制作点[0]"},
- )
- self.assertNotIn("production_brief", payload)
- self.assertEqual(
- payload["allowed_source_paths"],
- planned_task.source_paths,
- )
- self.assertEqual(
- payload["resolved_task"],
- planned_task.model_dump(mode="json"),
- )
- self.assertNotIn("plan", payload)
- self.assertNotIn("stage_requirements", payload)
- self.assertNotIn("requirements", payload)
- self.assertEqual(
- [
- item["expectation"]["expectation_id"]
- for item in payload["selected_expectations"]
- ],
- ["Requirement1-Expectation1"],
- )
- self.assertNotIn(
- "Requirement1-Expectation2",
- json.dumps(payload, ensure_ascii=False),
- )
- self.assertEqual(
- set(payload["task_package"]),
- {
- "schema_version",
- "run_id",
- "plan_id",
- "plan_version",
- "task_id",
- "production_brief_uri",
- "plan_uri",
- "dependency_deliveries",
- },
- )
- prompt = build_executor_system_prompt(
- load_executor_skill(planned_task.skill_id)
- )
- self.assertIn("structured-analysis", prompt)
- self.assertNotIn("{{SKILL_ID}}", prompt)
- def test_shared_json_path_reader_keeps_stable_boundaries(self) -> None:
- _, brief = load_production_brief(BRIEF_PATH)
- payload = brief.model_dump(mode="json", by_alias=True)
- self.assertIn("$.制作表.形式结果[0]", build_allowed_source_paths(brief))
- self.assertEqual(
- resolve_direct_json_path(payload, "$.帖子类型"),
- payload["帖子类型"],
- )
- def test_dependency_manifest_loads_only_summary_and_artifact_refs(self) -> None:
- first_task = build_task_package(BRIEF_PATH)
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- delivery = run_executor_agent(
- first_task,
- run_dir=root,
- model=build_executor_model(build_executor_candidate()),
- tool_registry=_fake_tool_registry(),
- )
- manifest = save_executor_delivery(root, delivery)
- second_task = build_task_package(
- BRIEF_PATH,
- planned_task=build_planned_task(
- task_id="Task2",
- depends_on=["Task1"],
- ),
- dependency_artifacts={"Task1": str(manifest)},
- )
- brief, plan, planned_task = load_task_contract(second_task)
- payload = build_executor_user_payload(
- executor_run_id=create_executor_run_id(second_task),
- task=second_task,
- brief=brief,
- plan=plan,
- planned_task=planned_task,
- )
- dependency = payload["dependency_artifacts"][0]
- self.assertEqual(dependency["task_id"], "Task1")
- self.assertIsInstance(dependency["summary"], str)
- self.assertEqual(dependency["summary"], delivery.summary)
- self.assertEqual(
- dependency["artifacts"][0]["uri"],
- delivery.artifacts[0].uri,
- )
- self.assertNotIn("tool_calls", dependency)
- class GeneralExecutorTest(unittest.TestCase):
- def test_reference_source_must_match_probe_local_mapping(self) -> None:
- source_a = "https://example.test/a.png"
- source_b = "https://example.test/b.png"
- local_a = str(BRIEF_PATH.resolve())
- planned = build_planned_task(
- skill_id="reference-inspection",
- deliverable_type="reference_collection",
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- _, _, resolved_task = load_task_contract(task)
- candidate = ExecutorCandidate(
- run_id=task.run_id,
- plan_id=task.plan_id,
- task_id=task.task_id,
- plan_version=task.plan_version,
- deliverable_type="reference_collection",
- artifacts=[
- CandidateArtifact(
- artifact_type="image",
- uri=local_a,
- source_uri=source_b,
- description="错误绑定的参考图",
- )
- ],
- artifact_binding_claims=[
- ArtifactBindingClaim(
- expectation_id="Requirement1-Expectation1",
- artifact_uri=local_a,
- )
- ],
- summary="错误地把 A 的缓存声明为 B",
- )
- messages = [
- AIMessage(
- content="",
- tool_calls=[
- {
- "name": "probe_media",
- "args": {"source": source_a},
- "id": "probe-a",
- "type": "tool_call",
- },
- {
- "name": "view_images",
- "args": {"image_sources": [local_a]},
- "id": "view-a",
- "type": "tool_call",
- },
- ],
- ),
- ToolMessage(
- content=json.dumps(
- {
- "success": True,
- "source": source_a,
- "local_path": local_a,
- "media_type": "image",
- }
- ),
- tool_call_id="probe-a",
- name="probe_media",
- ),
- ToolMessage(
- content=json.dumps(
- {
- "success": True,
- "images": [
- {
- "source": local_a,
- "local_path": local_a,
- }
- ],
- }
- ),
- tool_call_id="view-a",
- name="view_images",
- ),
- ]
- with self.assertRaisesRegex(
- ExecutorProvenanceError,
- "probe_media",
- ):
- validate_candidate_provenance(
- candidate,
- task,
- resolved_task,
- build_tool_call_records(messages),
- skill=load_executor_skill("reference-inspection"),
- input_refs={source_b},
- )
- def test_reference_inspection_adopts_real_viewed_image(self) -> None:
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- image_path = root / "existing-reference.png"
- from PIL import Image
- Image.new("RGB", (24, 24), "green").save(image_path)
- brief_payload = json.loads(BRIEF_PATH.read_text(encoding="utf-8"))
- brief_payload["核心制作点"][0]["测试已有媒体"] = str(image_path)
- brief_path = root / "production_brief.json"
- brief_path.write_text(
- json.dumps(brief_payload, ensure_ascii=False),
- encoding="utf-8",
- )
- planned = build_planned_task(
- skill_id="reference-inspection",
- deliverable_type="reference_collection",
- max_attempts=2,
- )
- task = build_task_package(
- brief_path,
- planned_task=planned,
- )
- candidate = ExecutorCandidate(
- run_id=task.run_id,
- plan_id=task.plan_id,
- task_id=task.task_id,
- plan_version=task.plan_version,
- deliverable_type="reference_collection",
- artifacts=[
- CandidateArtifact(
- artifact_type="image",
- uri=str(image_path.resolve()),
- source_uri=str(image_path.resolve()),
- description="已检查并缓存的人物参考图",
- )
- ],
- artifact_binding_claims=[
- ArtifactBindingClaim(
- expectation_id="Requirement1-Expectation1",
- artifact_uri=str(image_path.resolve()),
- evidence_tool_call_ids=["probe-reference"],
- )
- ],
- summary="已有参考图可用于后续生产",
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "probe_media",
- {"source": str(image_path.resolve())},
- "probe-reference",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [str(image_path.resolve())]},
- "view-reference",
- ),
- AIMessage(content=candidate.model_dump_json()),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- delivery = run_executor_agent(
- task,
- run_dir=root / "run",
- model=model,
- tool_registry=create_default_tool_registry(
- output_dir=root / "tool-output",
- ),
- )
- self.assertEqual(delivery.skill_id, "reference-inspection")
- self.assertEqual(delivery.artifacts[0].artifact_type, "image")
- self.assertEqual(
- delivery.artifacts[0].source_uri,
- str(image_path.resolve()),
- )
- def test_reference_review_accepts_separate_probe_and_view_caches(
- self,
- ) -> None:
- source_url = "https://example.test/existing-reference.png"
- # 使用一个真实存在、且与 source_url 不同的路径模拟 probe 缓存;
- # 文件内容不由该测试读取。
- probe_path = str(BRIEF_PATH.resolve())
- @tool("probe_media")
- def probe_media(source: str) -> dict:
- """探测测试媒体。"""
- return {
- "success": True,
- "source": source,
- "local_path": probe_path,
- "media_type": "image",
- "mime_type": "image/png",
- "width": 1080,
- "height": 1920,
- }
- @tool("view_images")
- def view_images(image_sources: list[str]) -> dict:
- """查看测试图片,并模拟另一套真实缓存路径。"""
- return {
- "success": True,
- "images": [
- {
- "source": source,
- "local_path": (
- "/cache/view/existing-reference.png"
- ),
- "data_url": (
- "data:image/png;base64,"
- "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
- ),
- }
- for source in image_sources
- ],
- }
- @tool("extract_frames")
- def extract_frames(video: str) -> dict:
- """占位视频抽帧工具。"""
- return {"success": True, "video": video, "frames": []}
- planned = build_planned_task(
- skill_id="reference-inspection",
- deliverable_type="reference_collection",
- max_attempts=1,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = ExecutorCandidate(
- run_id=task.run_id,
- plan_id=task.plan_id,
- task_id=task.task_id,
- plan_version=task.plan_version,
- deliverable_type="reference_collection",
- artifacts=[
- CandidateArtifact(
- artifact_type="image",
- uri=probe_path,
- source_uri=source_url,
- description="已检查的人物参考图",
- )
- ],
- artifact_binding_claims=[
- ArtifactBindingClaim(
- expectation_id="Requirement1-Expectation1",
- artifact_uri=probe_path,
- evidence_tool_call_ids=["probe-reference"],
- )
- ],
- summary="参考图已经完成探测和查看",
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "probe_media",
- {"source": source_url},
- "probe-reference",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [source_url]},
- "view-reference",
- ),
- AIMessage(content=candidate.model_dump_json()),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- delivery = run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=ToolRegistry(
- {
- item.name: item
- for item in (
- probe_media,
- view_images,
- extract_frames,
- )
- }
- ),
- )
- self.assertEqual(delivery.artifacts[0].uri, probe_path)
- self.assertEqual(delivery.artifacts[0].source_uri, source_url)
- def test_reference_video_review_links_source_to_probe_cache(
- self,
- ) -> None:
- source_url = "https://example.test/existing-reference.mp4"
- probe_path = str(BRIEF_PATH.resolve())
- frame_path = str(BRIEF_PATH.resolve())
- @tool("probe_media")
- def probe_media(source: str) -> dict:
- """探测测试视频。"""
- return {
- "success": True,
- "source": source,
- "local_path": probe_path,
- "media_type": "video",
- "mime_type": "video/mp4",
- "duration_sec": 5,
- "width": 1080,
- "height": 1920,
- }
- @tool("extract_frames")
- def extract_frames(video: str) -> dict:
- """从原始来源抽取测试视频帧。"""
- return {
- "success": True,
- "video": video,
- "frames": [{"local_path": frame_path}],
- }
- @tool("view_images")
- def view_images(image_sources: list[str]) -> dict:
- """查看测试视频帧。"""
- return {
- "success": True,
- "images": [
- {
- "source": source,
- "local_path": source,
- "data_url": (
- "data:image/png;base64,"
- "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
- ),
- }
- for source in image_sources
- ],
- }
- planned = build_planned_task(
- skill_id="reference-inspection",
- deliverable_type="reference_collection",
- max_attempts=1,
- )
- task = build_task_package(
- BRIEF_PATH,
- planned_task=planned,
- expectation_artifact_type="video",
- )
- candidate = ExecutorCandidate(
- run_id=task.run_id,
- plan_id=task.plan_id,
- task_id=task.task_id,
- plan_version=task.plan_version,
- deliverable_type="reference_collection",
- artifacts=[
- CandidateArtifact(
- artifact_type="video",
- uri=probe_path,
- source_uri=source_url,
- description="已检查的动作参考视频",
- )
- ],
- artifact_binding_claims=[
- ArtifactBindingClaim(
- expectation_id="Requirement1-Expectation1",
- artifact_uri=probe_path,
- evidence_tool_call_ids=["probe-reference"],
- )
- ],
- summary="参考视频已经完成探测、抽帧和查看",
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "probe_media",
- {"source": source_url},
- "probe-reference",
- ),
- _tool_call(
- "extract_frames",
- {"video": source_url},
- "extract-reference",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [frame_path]},
- "view-reference",
- ),
- AIMessage(content=candidate.model_dump_json()),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- delivery = run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=ToolRegistry(
- {
- item.name: item
- for item in (
- probe_media,
- view_images,
- extract_frames,
- )
- }
- ),
- )
- self.assertEqual(delivery.artifacts[0].uri, probe_path)
- self.assertEqual(delivery.artifacts[0].source_uri, source_url)
- def test_analysis_task_writes_local_structured_artifact(self) -> None:
- task = build_task_package(BRIEF_PATH)
- candidate = build_executor_candidate()
- self.assertEqual(candidate.schema_version, "0.3")
- with tempfile.TemporaryDirectory() as temp_dir:
- delivery = run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=build_executor_model(candidate),
- tool_registry=_fake_tool_registry(),
- )
- self.assertEqual(delivery.skill_id, "structured-analysis")
- self.assertEqual(delivery.attempt_count, 1)
- self.assertEqual(
- delivery.artifacts[0].artifact_id,
- "Task1-v1-artifact-1",
- )
- self.assertIsInstance(delivery.summary, str)
- self.assertTrue(Path(delivery.manifest_uri).is_file())
- artifact_path = Path(delivery.artifacts[0].uri)
- self.assertTrue(artifact_path.is_file())
- self.assertNotEqual(
- artifact_path.resolve(),
- Path(delivery.manifest_uri).resolve(),
- )
- self.assertEqual(delivery.tool_calls, [])
- saved = ExecutorCandidate.model_validate_json(
- Path(delivery.manifest_uri).read_text(encoding="utf-8")
- )
- self.assertEqual(saved.payload["global_constraints"], ["统一主视觉"])
- materialized = json.loads(artifact_path.read_text(encoding="utf-8"))
- self.assertEqual(
- materialized["payload"]["global_constraints"],
- ["统一主视觉"],
- )
- def test_same_task_execution_reuses_stable_agent_run(self) -> None:
- task = build_task_package(BRIEF_PATH)
- candidate = build_executor_candidate()
- model = build_executor_model(candidate, candidate)
- with tempfile.TemporaryDirectory() as temp_dir:
- first = run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- second = run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- self.assertEqual(first.executor_run_id, second.executor_run_id)
- def test_invalid_output_retries_inside_same_task(self) -> None:
- task = build_task_package(BRIEF_PATH)
- valid = build_executor_candidate()
- invalid = valid.model_dump(mode="json")
- invalid["payload"] = {}
- model = ToolAwareFakeChatModel(
- responses=[
- AIMessage(content=json.dumps(invalid, ensure_ascii=False)),
- AIMessage(content=valid.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- delivery = run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- self.assertEqual(delivery.attempt_count, 2)
- self.assertIn(task.task_id, delivery.executor_run_id)
- def test_correction_exposes_exact_evidence_call_ids(self) -> None:
- planned = build_planned_task(
- skill_id="external-research",
- deliverable_type="research_result",
- max_attempts=2,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- valid = build_executor_candidate(
- deliverable_type="research_result",
- artifact_uri=SOURCE_URL,
- evidence_tool_call_ids=["call-run"],
- )
- invalid = valid.model_copy(deep=True)
- invalid.artifact_binding_claims[0].evidence_tool_call_ids = []
- final_message = (
- '字段结构:{"items":{"type":"string"},"type":"array"}\n'
- + valid.model_dump_json()
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call("search_tool", {"query": "资料"}, "call-search"),
- _tool_call(
- "inspect_tool",
- {"tool_ids": ["fake-tool"]},
- "call-inspect",
- ),
- _tool_call(
- "run_tool",
- {"tool_id": "fake-tool", "params": {"kind": "research"}},
- "call-run",
- ),
- AIMessage(content=invalid.model_dump_json()),
- AIMessage(content=final_message),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- run_dir = Path(temp_dir)
- delivery = run_executor_agent(
- task,
- run_dir=run_dir,
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- with create_sqlite_checkpointer(
- run_dir,
- filename="agent_checkpoints.sqlite",
- ) as checkpointer:
- messages = checkpoint_messages(
- checkpointer,
- {
- "configurable": {
- "thread_id": create_executor_run_id(task)
- }
- },
- )
- corrections = [
- message.content
- for message in messages
- if isinstance(message, HumanMessage)
- and isinstance(message.content, str)
- and message.content.startswith("上一版未通过")
- ]
- self.assertEqual(delivery.attempt_count, 2)
- self.assertIn('"tool_call_id": "call-run"', corrections[-1])
- def test_research_task_must_use_real_tool_output(self) -> None:
- planned = build_planned_task(
- skill_id="external-research",
- deliverable_type="research_result",
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="research_result",
- artifact_uri=SOURCE_URL,
- evidence_tool_call_ids=["call-run"],
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call("search_tool", {"query": "资料"}, "call-search"),
- _tool_call(
- "inspect_tool",
- {"tool_ids": ["fake-tool"]},
- "call-inspect",
- ),
- _tool_call(
- "run_tool",
- {"tool_id": "fake-tool", "params": {"kind": "research"}},
- "call-run",
- ),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- delivery = run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- self.assertEqual(
- [item.tool_name for item in delivery.tool_calls],
- ["search_tool", "inspect_tool", "run_tool"],
- )
- self.assertTrue(
- [item.duration_ms for item in delivery.tool_calls] == [3, 4, 5]
- )
- self.assertEqual(
- delivery.findings[0].source_urls,
- [SOURCE_URL],
- )
- self.assertIn(
- SOURCE_URL,
- delivery.tool_calls[-1].output_excerpt or "",
- )
- def test_image_task_calls_generation_and_views_result(self) -> None:
- planned = build_planned_task(
- skill_id="image-production",
- deliverable_type="image",
- max_attempts=1,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="image",
- artifact_uri=IMAGE_URL,
- evidence_tool_call_ids=["run"],
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call("search_tool", {"query": "图片生成"}, "search"),
- _tool_call(
- "inspect_tool",
- {"tool_ids": ["seedream_generate_image"]},
- "inspect",
- ),
- _tool_call(
- "run_tool",
- {
- "tool_id": "seedream_generate_image",
- "params": {"kind": "image"},
- },
- "run",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [IMAGE_URL]},
- "view",
- ),
- AIMessage(content=candidate.model_dump_json()),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- delivery = run_executor_agent(
- task,
- run_dir=root,
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- config = {
- "configurable": {
- "thread_id": create_executor_run_id(task)
- }
- }
- with create_sqlite_checkpointer(
- root,
- filename="agent_checkpoints.sqlite",
- ) as checkpointer:
- messages = checkpoint_messages(checkpointer, config)
- self.assertEqual(delivery.artifacts[0].uri, IMAGE_URL)
- self.assertEqual(delivery.tool_calls[-1].tool_name, "view_images")
- self.assertTrue(
- any(
- isinstance(message.content, list)
- and any(
- isinstance(block, dict)
- and block.get("type") == "image_url"
- for block in message.content
- )
- for message in messages
- )
- )
- def test_research_run_requires_prior_matching_inspect(self) -> None:
- planned = build_planned_task(
- skill_id="external-research",
- deliverable_type="research_result",
- max_attempts=1,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="research_result",
- artifact_uri=SOURCE_URL,
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "run_tool",
- {"tool_id": "fake-tool", "params": {"kind": "research"}},
- "run",
- ),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- with self.assertRaisesRegex(
- ExecutorOutputError,
- "run_tool 前必须成功 inspect",
- ):
- run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- def test_image_final_artifact_must_be_viewed(self) -> None:
- planned = build_planned_task(
- skill_id="image-production",
- deliverable_type="image",
- max_attempts=1,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="image",
- artifact_uri=IMAGE_URL,
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "inspect_tool",
- {"tool_ids": ["seedream_generate_image"]},
- "inspect",
- ),
- _tool_call(
- "run_tool",
- {
- "tool_id": "seedream_generate_image",
- "params": {"kind": "image"},
- },
- "run",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [SOURCE_URL]},
- "view",
- ),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- with self.assertRaisesRegex(
- ExecutorOutputError,
- "最终 Artifact 必须经过 view_images",
- ):
- run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- def test_view_images_does_not_prove_image_origin(self) -> None:
- planned = build_planned_task(
- skill_id="image-production",
- deliverable_type="image",
- max_attempts=1,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="image",
- artifact_uri=IMAGE_URL,
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "view_images",
- {"image_sources": [IMAGE_URL]},
- "view",
- ),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- with self.assertRaisesRegex(
- ExecutorOutputError,
- "未出现在工具或依赖产物",
- ):
- run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- def test_video_task_calls_generation_and_extracts_final_frames(self) -> None:
- planned = build_planned_task(
- skill_id="video-production",
- deliverable_type="video",
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="video",
- artifact_uri=VIDEO_URL,
- evidence_tool_call_ids=["run"],
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "inspect_tool",
- {"tool_ids": ["seedance_generate_video"]},
- "inspect",
- ),
- _tool_call(
- "run_tool",
- {
- "tool_id": "seedance_generate_video",
- "params": {"kind": "video"},
- },
- "run",
- ),
- _tool_call(
- "extract_frames",
- {"video": VIDEO_URL, "num_frames": 3},
- "extract",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [FRAME_URL]},
- "view-frames",
- ),
- AIMessage(content=candidate.model_dump_json()),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- delivery = run_executor_agent(
- task,
- run_dir=root,
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- config = {
- "configurable": {
- "thread_id": create_executor_run_id(task)
- }
- }
- with create_sqlite_checkpointer(
- root,
- filename="agent_checkpoints.sqlite",
- ) as checkpointer:
- messages = checkpoint_messages(checkpointer, config)
- self.assertEqual(delivery.artifacts[0].uri, VIDEO_URL)
- self.assertEqual(delivery.tool_calls[-1].tool_name, "view_images")
- self.assertTrue(
- any(
- isinstance(message.content, list)
- and any(
- isinstance(block, dict)
- and block.get("type") == "image_url"
- for block in message.content
- )
- for message in messages
- )
- )
- def test_media_review_and_format_repairs_share_attempt_budget(self) -> None:
- planned = build_planned_task(
- skill_id="image-production",
- deliverable_type="image",
- max_attempts=2,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="image",
- artifact_uri=IMAGE_URL,
- evidence_tool_call_ids=["run"],
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "inspect_tool",
- {"tool_ids": ["seedream_generate_image"]},
- "inspect",
- ),
- _tool_call(
- "run_tool",
- {
- "tool_id": "seedream_generate_image",
- "params": {"kind": "image"},
- },
- "run",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [IMAGE_URL]},
- "view",
- ),
- AIMessage(content="{}"),
- AIMessage(content=candidate.model_dump_json()),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- delivery = run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- self.assertEqual(delivery.attempt_count, 2)
- def test_media_artifact_changes_cannot_bypass_attempt_budget(self) -> None:
- image_b = "https://example.test/generated-reference-b.png"
- image_c = "https://example.test/generated-reference-c.png"
- planned = build_planned_task(
- skill_id="image-production",
- deliverable_type="image",
- max_attempts=2,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate_a = build_executor_candidate(
- deliverable_type="image",
- artifact_uri=IMAGE_URL,
- evidence_tool_call_ids=["run-a"],
- )
- candidate_b = build_executor_candidate(
- deliverable_type="image",
- artifact_uri=image_b,
- evidence_tool_call_ids=["run-b"],
- )
- candidate_c = build_executor_candidate(
- deliverable_type="image",
- artifact_uri=image_c,
- evidence_tool_call_ids=["run-c"],
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "inspect_tool",
- {"tool_ids": ["seedream_generate_image"]},
- "inspect",
- ),
- _tool_call(
- "run_tool",
- {
- "tool_id": "seedream_generate_image",
- "params": {
- "kind": "image",
- "result_url": IMAGE_URL,
- },
- },
- "run-a",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [IMAGE_URL]},
- "view-a",
- ),
- AIMessage(content=candidate_a.model_dump_json()),
- _tool_call(
- "run_tool",
- {
- "tool_id": "seedream_generate_image",
- "params": {
- "kind": "image",
- "result_url": image_b,
- },
- },
- "run-b",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [image_b]},
- "view-b",
- ),
- AIMessage(content=candidate_b.model_dump_json()),
- _tool_call(
- "run_tool",
- {
- "tool_id": "seedream_generate_image",
- "params": {
- "kind": "image",
- "result_url": image_c,
- },
- },
- "run-c",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [image_c]},
- "view-c",
- ),
- AIMessage(content=candidate_c.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- with self.assertRaisesRegex(
- ExecutorOutputError,
- "耗尽修正次数",
- ):
- run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- def test_fabricated_remote_uri_is_rejected(self) -> None:
- planned = build_planned_task(
- skill_id="external-research",
- deliverable_type="research_result",
- max_attempts=1,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="research_result",
- artifact_uri="https://fabricated.test/source",
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "run_tool",
- {"tool_id": "fake-tool", "params": {"kind": "research"}},
- "run",
- ),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- with self.assertRaisesRegex(
- ExecutorOutputError,
- "未出现在工具",
- ):
- run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- def test_analysis_payload_cannot_hide_a_fabricated_url(self) -> None:
- planned = build_planned_task(max_attempts=1)
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate()
- candidate.payload["fake_reference"] = "https://fabricated.test/image.png"
- with tempfile.TemporaryDirectory() as temp_dir:
- with self.assertRaisesRegex(
- ExecutorOutputError,
- "未出现在工具",
- ):
- run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=build_executor_model(candidate),
- tool_registry=_fake_tool_registry(),
- )
- if __name__ == "__main__":
- unittest.main()
|