Delete ASP/qwen_agents/Profiler_agent/.ipynb_checkpoints
Browse files- ASP/qwen_agents/Profiler_agent/.ipynb_checkpoints/OUTPUT-checkpoint.md +0 -39
- ASP/qwen_agents/Profiler_agent/.ipynb_checkpoints/__init__-checkpoint.py +0 -0
- ASP/qwen_agents/Profiler_agent/.ipynb_checkpoints/image_viewer_mcp-checkpoint.py +0 -174
- ASP/qwen_agents/Profiler_agent/.ipynb_checkpoints/profiler_agent-checkpoint.py +0 -141
ASP/qwen_agents/Profiler_agent/.ipynb_checkpoints/OUTPUT-checkpoint.md
DELETED
|
@@ -1,39 +0,0 @@
|
|
| 1 |
-
# Output Formatting
|
| 2 |
-
|
| 3 |
-
Write your response as a Markdown report with these sections, in order:
|
| 4 |
-
|
| 5 |
-
## Crime Scene Report
|
| 6 |
-
Describe what is visible across the case images: setting, victim positioning
|
| 7 |
-
(if applicable), signs of planning vs. improvisation, anything inconsistent
|
| 8 |
-
with a normal/expected version of the scene (e.g. a living room, park, or
|
| 9 |
-
other setting), and any apparent staging, concealment, or lack thereof.
|
| 10 |
-
|
| 11 |
-
## Victimology
|
| 12 |
-
What the images/notes suggest about victim selection, means of access, and
|
| 13 |
-
relationship (if any) between offender and victim.
|
| 14 |
-
|
| 15 |
-
## Organization Level
|
| 16 |
-
Classify the scene as **Organized**, **Disorganized**, or **Mixed**, based on
|
| 17 |
-
the classic distinction:
|
| 18 |
-
- *Organized*: planning evident, victim/scene control, concealment or careful
|
| 19 |
-
disposal, deliberate cleanup — associated with higher intelligence and
|
| 20 |
-
social integration.
|
| 21 |
-
- *Disorganized*: little planning, scene left as-is, minimal concealment,
|
| 22 |
-
higher apparent anxiety/impulsivity.
|
| 23 |
-
- *Mixed*: elements of both.
|
| 24 |
-
|
| 25 |
-
## Offender Typology
|
| 26 |
-
Using the classification frameworks in `REFERENCE.md` (Palermo/Mastronardi's
|
| 27 |
-
typology and the Holmes & Holmes typology), identify which types best fit
|
| 28 |
-
the evidence; for example (not exhaustive): Visionary, Missionary (and its
|
| 29 |
-
subtypes), Hedonistic, Power/Control-oriented, or Lust-oriented, including
|
| 30 |
-
any atypical variant if applicable. You do not need to force a single type
|
| 31 |
-
if the evidence is ambiguous — say so and name the top 1-2 candidates.
|
| 32 |
-
|
| 33 |
-
## Rationale & Confidence
|
| 34 |
-
2-4 sentences tying your classification directly to specific evidence from
|
| 35 |
-
the Forensic agent's report and the case images. State your confidence
|
| 36 |
-
explicitly (Low / Medium / High) based on how much evidence is actually
|
| 37 |
-
available. This is an analytical classification for casework support, not a
|
| 38 |
-
certainty — frame it as the most likely interpretation of the evidence, not
|
| 39 |
-
a definitive claim.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ASP/qwen_agents/Profiler_agent/.ipynb_checkpoints/__init__-checkpoint.py
DELETED
|
File without changes
|
ASP/qwen_agents/Profiler_agent/.ipynb_checkpoints/image_viewer_mcp-checkpoint.py
DELETED
|
@@ -1,174 +0,0 @@
|
|
| 1 |
-
import io
|
| 2 |
-
import os
|
| 3 |
-
import re
|
| 4 |
-
import sys
|
| 5 |
-
from dataclasses import dataclass, field
|
| 6 |
-
from pathlib import Path
|
| 7 |
-
from fastmcp import FastMCP
|
| 8 |
-
from fastmcp.exceptions import ToolError
|
| 9 |
-
from fastmcp.utilities.types import Image
|
| 10 |
-
from PIL import Image as PILImage
|
| 11 |
-
|
| 12 |
-
DEFAULT_SKILL_DIR = Path(__file__).parent / ".agents" / "skills" / "criminal-behavioral-analysis"
|
| 13 |
-
IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".webp"}
|
| 14 |
-
IMAGE_MARKDOWN = re.compile(r"!\[(?P<alt>.*?)\]\((?P<path>.*?)\)")
|
| 15 |
-
ADVANCED_HEADING = "# Advanced version"
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
@dataclass
|
| 19 |
-
class Section:
|
| 20 |
-
number: int
|
| 21 |
-
label: str
|
| 22 |
-
title: str
|
| 23 |
-
images: list[Path] = field(default_factory=list)
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
class AdvancedImageLibrary:
|
| 27 |
-
def __init__(self, skill_dir: Path, max_side: int = 1600, max_per_call: int = 4):
|
| 28 |
-
self.skill_dir = skill_dir.resolve()
|
| 29 |
-
self.long_dir = self.skill_dir / "assets" / "long"
|
| 30 |
-
self.reference_file = self.skill_dir / "references" / "REFERENCE.md"
|
| 31 |
-
self.max_side = max_side
|
| 32 |
-
self.max_per_call = max_per_call
|
| 33 |
-
titles, self.descriptions = self.read_reference()
|
| 34 |
-
self.sections = self.read_sections(titles)
|
| 35 |
-
|
| 36 |
-
def read_reference(self) -> tuple[list[str], dict[Path, str]]:
|
| 37 |
-
titles: list[str] = []
|
| 38 |
-
alts: dict[Path, str] = {}
|
| 39 |
-
if not self.reference_file.exists():
|
| 40 |
-
return titles, alts
|
| 41 |
-
in_advanced = False
|
| 42 |
-
for line in self.reference_file.read_text(encoding="utf-8").splitlines():
|
| 43 |
-
if line.strip() == ADVANCED_HEADING:
|
| 44 |
-
in_advanced = True
|
| 45 |
-
elif in_advanced and line.startswith("## "):
|
| 46 |
-
titles.append(line[3:].strip())
|
| 47 |
-
elif in_advanced:
|
| 48 |
-
for match in IMAGE_MARKDOWN.finditer(line):
|
| 49 |
-
alts[(self.skill_dir / match["path"]).resolve()] = match["alt"].strip()
|
| 50 |
-
return titles, alts
|
| 51 |
-
|
| 52 |
-
def read_sections(self, titles: list[str]) -> list[Section]:
|
| 53 |
-
folders = sorted(
|
| 54 |
-
(d for d in self.long_dir.iterdir() if d.is_dir()),
|
| 55 |
-
key=lambda d: [int(t) if t.isdigit() else t for t in re.split(r"(\d+)", d.name)],
|
| 56 |
-
) if self.long_dir.is_dir() else []
|
| 57 |
-
use_titles = len(titles) == len(folders)
|
| 58 |
-
sections = []
|
| 59 |
-
for index, folder in enumerate(folders, start=1):
|
| 60 |
-
images = sorted(
|
| 61 |
-
(p for p in folder.iterdir() if p.suffix.lower() in IMAGE_SUFFIXES),
|
| 62 |
-
key=lambda p: [int(t) if t.isdigit() else t for t in re.split(r"(\d+)", p.name)],
|
| 63 |
-
)
|
| 64 |
-
sections.append(Section(
|
| 65 |
-
number=index,
|
| 66 |
-
label=folder.name,
|
| 67 |
-
title=titles[index - 1] if use_titles else folder.name,
|
| 68 |
-
images=images,
|
| 69 |
-
))
|
| 70 |
-
return sections
|
| 71 |
-
|
| 72 |
-
@property
|
| 73 |
-
def image_count(self) -> int:
|
| 74 |
-
return sum(len(s.images) for s in self.sections)
|
| 75 |
-
|
| 76 |
-
def describe(self, path: Path) -> str:
|
| 77 |
-
return self.descriptions.get(path.resolve(), "")
|
| 78 |
-
|
| 79 |
-
def relative(self, path: Path) -> str:
|
| 80 |
-
return path.resolve().relative_to(self.long_dir).as_posix()
|
| 81 |
-
|
| 82 |
-
def find_section(self, query: str) -> Section:
|
| 83 |
-
wanted = str(query).strip().lower()
|
| 84 |
-
if wanted.isdigit():
|
| 85 |
-
for section in self.sections:
|
| 86 |
-
if section.number == int(wanted):
|
| 87 |
-
return section
|
| 88 |
-
for section in self.sections:
|
| 89 |
-
if wanted and (wanted in section.title.lower() or wanted in section.label.lower()):
|
| 90 |
-
return section
|
| 91 |
-
choices = "; ".join(f"{s.number}: {s.title}" for s in self.sections)
|
| 92 |
-
raise ToolError(f"No section matches {query!r}. Available sections: {choices}")
|
| 93 |
-
|
| 94 |
-
def resolve_image(self, relative_path: str) -> Path:
|
| 95 |
-
candidate = (self.long_dir / relative_path).resolve()
|
| 96 |
-
if not candidate.is_relative_to(self.long_dir) or candidate.suffix.lower() not in IMAGE_SUFFIXES:
|
| 97 |
-
raise ToolError(f"{relative_path!r} is not an Advanced version image")
|
| 98 |
-
if not candidate.is_file():
|
| 99 |
-
raise ToolError(f"{relative_path!r} does not exist; call list_sections to see valid paths")
|
| 100 |
-
return candidate
|
| 101 |
-
|
| 102 |
-
def load(self, path: Path) -> Image:
|
| 103 |
-
with PILImage.open(path) as picture:
|
| 104 |
-
if max(picture.size) <= self.max_side:
|
| 105 |
-
return Image(path=str(path))
|
| 106 |
-
picture = picture.convert("RGB")
|
| 107 |
-
picture.thumbnail((self.max_side, self.max_side), PILImage.Resampling.LANCZOS)
|
| 108 |
-
buffer = io.BytesIO()
|
| 109 |
-
picture.save(buffer, format="JPEG", quality=88)
|
| 110 |
-
return Image(data=buffer.getvalue(), format="jpeg")
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
def build_server(library: AdvancedImageLibrary) -> FastMCP:
|
| 114 |
-
server = FastMCP("image-viewer")
|
| 115 |
-
|
| 116 |
-
@server.tool
|
| 117 |
-
def list_sections() -> list[dict]:
|
| 118 |
-
return [
|
| 119 |
-
{
|
| 120 |
-
"section": s.number,
|
| 121 |
-
"title": s.title,
|
| 122 |
-
"image_count": len(s.images),
|
| 123 |
-
"images": [
|
| 124 |
-
{"path": library.relative(p), "description": library.describe(p)}
|
| 125 |
-
for p in s.images
|
| 126 |
-
],
|
| 127 |
-
}
|
| 128 |
-
for s in library.sections
|
| 129 |
-
]
|
| 130 |
-
|
| 131 |
-
@server.tool
|
| 132 |
-
def view_section(section: str, offset: int = 0) -> list:
|
| 133 |
-
chosen = library.find_section(section)
|
| 134 |
-
start = max(int(offset), 0)
|
| 135 |
-
batch = chosen.images[start:start + library.max_per_call]
|
| 136 |
-
if not batch:
|
| 137 |
-
raise ToolError(
|
| 138 |
-
f"Section {chosen.number} has {len(chosen.images)} images; offset {start} is past the end"
|
| 139 |
-
)
|
| 140 |
-
end = start + len(batch)
|
| 141 |
-
header = f"Section {chosen.number}: {chosen.title} - images {start + 1} to {end} of {len(chosen.images)}"
|
| 142 |
-
footer = (
|
| 143 |
-
f"{len(chosen.images) - end} more image(s) in this section; call view_section again with offset={end}."
|
| 144 |
-
if end < len(chosen.images) else "End of section."
|
| 145 |
-
)
|
| 146 |
-
content: list = [header]
|
| 147 |
-
for path in batch:
|
| 148 |
-
description = library.describe(path)
|
| 149 |
-
content.append(f"{library.relative(path)}" + (f" - {description}" if description else ""))
|
| 150 |
-
content.append(library.load(path))
|
| 151 |
-
content.append(footer)
|
| 152 |
-
return content
|
| 153 |
-
|
| 154 |
-
@server.tool
|
| 155 |
-
def view_image(path: str) -> list:
|
| 156 |
-
resolved = library.resolve_image(path)
|
| 157 |
-
description = library.describe(resolved)
|
| 158 |
-
return [library.relative(resolved) + (f" - {description}" if description else ""), library.load(resolved)]
|
| 159 |
-
|
| 160 |
-
return server
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
def main() -> None:
|
| 164 |
-
skill_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_SKILL_DIR
|
| 165 |
-
library = AdvancedImageLibrary(
|
| 166 |
-
skill_dir,
|
| 167 |
-
max_side=int(os.getenv("IMAGE_VIEWER_MAX_SIDE", "1600")),
|
| 168 |
-
max_per_call=int(os.getenv("IMAGE_VIEWER_MAX_PER_CALL", "4")),
|
| 169 |
-
)
|
| 170 |
-
build_server(library).run(transport="stdio", show_banner=False)
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
if __name__ == "__main__":
|
| 174 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ASP/qwen_agents/Profiler_agent/.ipynb_checkpoints/profiler_agent-checkpoint.py
DELETED
|
@@ -1,141 +0,0 @@
|
|
| 1 |
-
import logging
|
| 2 |
-
import os
|
| 3 |
-
import sys
|
| 4 |
-
from pathlib import Path
|
| 5 |
-
from fastmcp.client import Client as FastMCPClient
|
| 6 |
-
from fastmcp.client.transports import StdioTransport
|
| 7 |
-
from pydantic_ai import Agent, RunContext
|
| 8 |
-
from pydantic_ai.mcp import MCPToolset
|
| 9 |
-
from qwen_agents.model_utils import QwenVLModel
|
| 10 |
-
from reference_version import ADVANCED, BASIC, version_from_messages
|
| 11 |
-
|
| 12 |
-
log = logging.getLogger(__name__)
|
| 13 |
-
|
| 14 |
-
PROFILER_MODEL_ID = os.getenv(
|
| 15 |
-
"PROFILER_MODEL_ID", "Kizzington/Qwen3-VL-8B-Thinking-heretic"
|
| 16 |
-
)
|
| 17 |
-
|
| 18 |
-
PROFILER_LOAD_IN_4BIT = os.getenv("PROFILER_LOAD_IN_4BIT", "false").strip().lower() not in (
|
| 19 |
-
"false", "0", "no"
|
| 20 |
-
)
|
| 21 |
-
|
| 22 |
-
SKILLS_DIR = Path(__file__).parent / ".agents" / "skills"
|
| 23 |
-
CBA_SKILL_DIR = SKILLS_DIR / "criminal-behavioral-analysis"
|
| 24 |
-
PROFILER_SKILL = CBA_SKILL_DIR / "SKILL.md"
|
| 25 |
-
|
| 26 |
-
SHORT_REFERENCE_IMAGES = sorted(
|
| 27 |
-
(CBA_SKILL_DIR / "assets" / "short").glob("*.jpg")
|
| 28 |
-
)
|
| 29 |
-
ADVANCED_IMAGE_COUNT = len(list((CBA_SKILL_DIR / "assets" / "long").rglob("*.jpg")))
|
| 30 |
-
|
| 31 |
-
IMAGE_VIEWER_SERVER = Path(__file__).parent / "image_viewer_mcp.py"
|
| 32 |
-
|
| 33 |
-
skills_client = FastMCPClient(
|
| 34 |
-
StdioTransport(
|
| 35 |
-
command=sys.executable,
|
| 36 |
-
args=[str(IMAGE_VIEWER_SERVER), str(CBA_SKILL_DIR)],
|
| 37 |
-
env={**os.environ}
|
| 38 |
-
),
|
| 39 |
-
init_timeout=60
|
| 40 |
-
)
|
| 41 |
-
ALLOWED_SKILL_TOOLS = {"list_sections", "view_section", "view_image"}
|
| 42 |
-
skills_toolset = MCPToolset(skills_client).filtered(
|
| 43 |
-
lambda ctx, tool_def: (
|
| 44 |
-
tool_def.name in ALLOWED_SKILL_TOOLS and version_from_messages(ctx.messages) != BASIC
|
| 45 |
-
)
|
| 46 |
-
)
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def load_skill_text(path: Path) -> str:
|
| 50 |
-
try:
|
| 51 |
-
return path.read_text(encoding="utf-8")
|
| 52 |
-
except FileNotFoundError as e:
|
| 53 |
-
raise RuntimeError(
|
| 54 |
-
f"Profiler agent's required skill file is missing: {path} "
|
| 55 |
-
"— this file defines the agent's core workflow and output "
|
| 56 |
-
"format; the agent cannot run correctly without it."
|
| 57 |
-
) from e
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
SKILL_MD = load_skill_text(PROFILER_SKILL)
|
| 61 |
-
OUTPUT_MD = load_skill_text(CBA_SKILL_DIR / "references" / "OUTPUT.md")
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
profiler_agent: Agent[None, str] = Agent(
|
| 65 |
-
model=QwenVLModel(
|
| 66 |
-
PROFILER_MODEL_ID,
|
| 67 |
-
processor_path="Qwen/Qwen3-VL-8B-Thinking",
|
| 68 |
-
reference_images=SHORT_REFERENCE_IMAGES,
|
| 69 |
-
temperature=0.3,
|
| 70 |
-
max_new_tokens=12288,
|
| 71 |
-
load_in_4bit=PROFILER_LOAD_IN_4BIT
|
| 72 |
-
).function_model(),
|
| 73 |
-
system_prompt=(
|
| 74 |
-
"You are a criminal behavioral profiler analyzing case images plus "
|
| 75 |
-
"a Forensic agent's report (received via A2A).\n\n"
|
| 76 |
-
"You are NOT deriving forensic conclusions yourself — use the "
|
| 77 |
-
"Forensic report's findings as evidence for your profile. Do NOT "
|
| 78 |
-
"just repeat or extend the Forensic report's own format (cause of "
|
| 79 |
-
"death, manner of death, etc.) — your job is a DIFFERENT, "
|
| 80 |
-
"behavioral-profiling report, as described in your skill file "
|
| 81 |
-
"below.\n\n"
|
| 82 |
-
"Note: some runs of this pipeline supply the Forensic report "
|
| 83 |
-
"concurrently with RAG grounding lookup rather than waiting for "
|
| 84 |
-
"it, so grounded reference material from forensic_knowledge/ may "
|
| 85 |
-
"or may not be present below — proceed with whatever you're given "
|
| 86 |
-
"and don't assume its absence means none exists.\n\n"
|
| 87 |
-
"You have already been given several reference images (the 'Basic "
|
| 88 |
-
"version' of your training material) summarizing established "
|
| 89 |
-
"criminological classification frameworks for serial offenders, "
|
| 90 |
-
"including:\n"
|
| 91 |
-
" - Palermo/Mastronardi's typology (Visionary, Missionary [with "
|
| 92 |
-
"subtypes: social protection, pseudo-religious, pseudo-political, "
|
| 93 |
-
"pseudo-cultural, racial, sexological-moralist, of the social "
|
| 94 |
-
"order], Hedonistic, Power/Control-oriented, Lust-oriented, plus "
|
| 95 |
-
"atypical variants)\n"
|
| 96 |
-
" - The Holmes & Holmes typology (male and female variants)\n"
|
| 97 |
-
" - Organized vs. Disorganized vs. Mixed crime-scene classification\n\n"
|
| 98 |
-
"--- Your skill file (criminal-behavioral-analysis/SKILL.md) ---\n"
|
| 99 |
-
"This defines your workflow. It is included here directly so you "
|
| 100 |
-
"always have it regardless of whether you call any tool — you may "
|
| 101 |
-
"still optionally use the image viewer tools to go deeper into the "
|
| 102 |
-
"Advanced version images as it describes below.\n\n"
|
| 103 |
-
f"TOOL RULES: your only tools are list_sections, view_section and view_image, "
|
| 104 |
-
f"and they only show the {ADVANCED_IMAGE_COUNT} Advanced version reference images. "
|
| 105 |
-
"list_sections lists the sections with a description of each image; view_section "
|
| 106 |
-
"returns the actual images of one section (a few at a time, use offset to page); "
|
| 107 |
-
"view_image returns one image by the path list_sections gave you. The case images "
|
| 108 |
-
"are ALREADY attached to this message: never try to open, locate or re-read them "
|
| 109 |
-
"with a tool, and never invent file paths such as /data/.... If a tool returns an "
|
| 110 |
-
"error, do NOT repeat the same call; continue with what you already have.\n\n"
|
| 111 |
-
f"{SKILL_MD}\n\n"
|
| 112 |
-
"--- Your required output format (references/OUTPUT.md), verbatim ---\n"
|
| 113 |
-
"You MUST use exactly these Markdown sections, in this order:\n\n"
|
| 114 |
-
f"{OUTPUT_MD}\n\n"
|
| 115 |
-
"---\n\n"
|
| 116 |
-
"CRITICAL — avoid speculation: base every claim on what is "
|
| 117 |
-
"actually visible in the images or stated in the Forensic report. "
|
| 118 |
-
"Never invent specific unsupported narrative details (e.g. a "
|
| 119 |
-
"specific relationship between victim and offender, a specific "
|
| 120 |
-
"motive scenario, or a named category of crime) unless the "
|
| 121 |
-
"evidence in front of you actually supports it — if the evidence "
|
| 122 |
-
"is weak or absent for a claim, say so explicitly rather than "
|
| 123 |
-
"filling the gap with an invented specific."
|
| 124 |
-
),
|
| 125 |
-
toolsets=[skills_toolset],
|
| 126 |
-
retries=4
|
| 127 |
-
)
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
@profiler_agent.output_validator
|
| 131 |
-
def warn_if_advanced_unused(ctx: RunContext[None], output: str) -> str:
|
| 132 |
-
if version_from_messages(ctx.messages) == ADVANCED:
|
| 133 |
-
viewed = any(
|
| 134 |
-
getattr(part, "tool_name", None) in ALLOWED_SKILL_TOOLS
|
| 135 |
-
for message in ctx.messages
|
| 136 |
-
for part in getattr(message, "parts", ())
|
| 137 |
-
if getattr(part, "part_kind", None) == "tool-call"
|
| 138 |
-
)
|
| 139 |
-
if not viewed:
|
| 140 |
-
log.warning("Advanced reference version was selected but the model never viewed an Advanced page")
|
| 141 |
-
return output
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|