e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
203 lines
7.8 KiB
Python
203 lines
7.8 KiB
Python
"""Prompt hint loading and rendering helpers for tools."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections import OrderedDict
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
from deeptutor.core.tool_protocol import ToolAlias, ToolPromptHints
|
|
|
|
ToolHintEntry = tuple[str, ToolPromptHints]
|
|
|
|
_GUIDELINE_HEADER = {
|
|
"en": (
|
|
"**Autonomously decide which tool to use** based on the current sub-goal "
|
|
"and the evidence gathered so far. Consider all available options:"
|
|
),
|
|
"zh": ("**根据当前子目标和已收集的证据,自主决定使用哪个工具**。请综合考虑所有可用选项:"),
|
|
}
|
|
|
|
_PHASE_LABELS = {
|
|
"en": {
|
|
"exploration": "Phase 1: Exploration",
|
|
"expansion": "Phase 2: Expansion",
|
|
"synthesis": "Phase 3: Synthesis",
|
|
"verification": "Phase 4: Verification",
|
|
"other": "Other Tools",
|
|
},
|
|
"zh": {
|
|
"exploration": "阶段 1:基础探索",
|
|
"expansion": "阶段 2:扩展补充",
|
|
"synthesis": "阶段 3:综合推理",
|
|
"verification": "阶段 4:验证核查",
|
|
"other": "其他工具",
|
|
},
|
|
}
|
|
|
|
_PHASE_ORDER = ["exploration", "expansion", "synthesis", "verification", "other"]
|
|
|
|
|
|
def _normalize_language(language: str) -> str:
|
|
normalized = language.lower()
|
|
if normalized.startswith("zh"):
|
|
return "zh"
|
|
if normalized.startswith("en"):
|
|
return "en"
|
|
return normalized
|
|
|
|
|
|
def load_prompt_hints(tool_name: str, language: str = "en") -> ToolPromptHints:
|
|
"""Load per-tool prompt hints from YAML with zh/en fallback."""
|
|
normalized_language = _normalize_language(language)
|
|
base_dir = Path(__file__).parent / "hints"
|
|
candidates = [base_dir / normalized_language / f"{tool_name}.yaml"]
|
|
if normalized_language != "en":
|
|
candidates.append(base_dir / "en" / f"{tool_name}.yaml")
|
|
|
|
for path in candidates:
|
|
if not path.is_file():
|
|
continue
|
|
with open(path, encoding="utf-8") as file:
|
|
data = yaml.safe_load(file) or {}
|
|
aliases = [
|
|
ToolAlias(
|
|
name=str(item.get("name", "")).strip(),
|
|
description=str(item.get("description", "")).strip(),
|
|
input_format=str(item.get("input_format", "")).strip(),
|
|
when_to_use=str(item.get("when_to_use", "")).strip(),
|
|
phase=str(item.get("phase", "")).strip(),
|
|
)
|
|
for item in data.get("aliases", [])
|
|
if str(item.get("name", "")).strip()
|
|
]
|
|
return ToolPromptHints(
|
|
short_description=str(data.get("short_description", "")).strip(),
|
|
when_to_use=str(data.get("when_to_use", "")).strip(),
|
|
input_format=str(data.get("input_format", "")).strip(),
|
|
guideline=str(data.get("guideline", "")).strip(),
|
|
note=str(data.get("note", "")).strip(),
|
|
phase=str(data.get("phase", "")).strip(),
|
|
aliases=aliases,
|
|
)
|
|
|
|
return ToolPromptHints()
|
|
|
|
|
|
class ToolPromptComposer:
|
|
"""Render prompt metadata into reusable prompt fragments."""
|
|
|
|
def __init__(self, language: str = "en") -> None:
|
|
self.language = _normalize_language(language)
|
|
|
|
def format_list(self, hints: list[ToolHintEntry]) -> str:
|
|
lines: list[str] = []
|
|
for name, hint in hints:
|
|
if hint.short_description:
|
|
lines.append(f"- {name}: {hint.short_description}")
|
|
return "\n".join(lines)
|
|
|
|
def format_list_with_usage(self, hints: list[ToolHintEntry]) -> str:
|
|
"""Per-tool bullet that includes ``when_to_use`` and ``input_format``.
|
|
|
|
Used by the chat persona prompt so the LLM has enough per-tool
|
|
guidance to decide *whether* to call it, not just *what it is*.
|
|
Tools without a ``short_description`` are skipped entirely so the
|
|
block never carries empty bullets.
|
|
"""
|
|
when_label = "When to use" if self.language != "zh" else "适用场景"
|
|
input_label = "Input" if self.language != "zh" else "参数格式"
|
|
blocks: list[str] = []
|
|
for name, hint in hints:
|
|
if not hint.short_description:
|
|
continue
|
|
entry: list[str] = [f"- `{name}` — {hint.short_description}"]
|
|
if hint.when_to_use:
|
|
entry.append(f" {when_label}: {hint.when_to_use}")
|
|
if hint.input_format:
|
|
entry.append(f" {input_label}: {hint.input_format}")
|
|
blocks.append("\n".join(entry))
|
|
return "\n".join(blocks)
|
|
|
|
def format_table(
|
|
self,
|
|
hints: list[ToolHintEntry],
|
|
control_actions: list[dict[str, str]] | None = None,
|
|
) -> str:
|
|
parts: list[str] = []
|
|
table_lines = [
|
|
"| action | When to use | action_input |",
|
|
"|--------|------------|--------------|",
|
|
]
|
|
for name, hint in hints:
|
|
if hint.when_to_use or hint.input_format:
|
|
table_lines.append(f"| `{name}` | {hint.when_to_use} | {hint.input_format} |")
|
|
for control in control_actions or []:
|
|
table_lines.append(
|
|
f"| `{control['name']}` | {control['when_to_use']} | {control['input_format']} |"
|
|
)
|
|
parts.append("\n".join(table_lines))
|
|
|
|
guidelines = [f" - `{name}` {hint.guideline}" for name, hint in hints if hint.guideline]
|
|
if guidelines:
|
|
header = _GUIDELINE_HEADER.get(self.language, _GUIDELINE_HEADER["en"])
|
|
parts.append(f"{header}\n" + "\n".join(guidelines))
|
|
|
|
notes = [f"- {hint.note}" for _, hint in hints if hint.note]
|
|
if notes:
|
|
parts.append("\n".join(notes))
|
|
|
|
return "\n\n".join(parts)
|
|
|
|
def format_aliases(self, hints: list[ToolHintEntry]) -> str:
|
|
lines: list[str] = []
|
|
for name, hint in hints:
|
|
if hint.aliases:
|
|
for alias in hint.aliases:
|
|
description = alias.description or hint.short_description
|
|
input_format = alias.input_format or hint.input_format or "Natural language"
|
|
lines.append(f"- {alias.name}: {description} | Query format: {input_format}")
|
|
continue
|
|
if hint.short_description:
|
|
lines.append(
|
|
f"- {name}: {hint.short_description} | Query format: {hint.input_format or 'Natural language'}"
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
def format_phased(self, hints: list[ToolHintEntry]) -> str:
|
|
grouped: OrderedDict[str, list[str]] = OrderedDict((phase, []) for phase in _PHASE_ORDER)
|
|
for name, hint in hints:
|
|
phase = hint.phase or "other"
|
|
grouped.setdefault(phase, [])
|
|
if hint.aliases:
|
|
for alias in hint.aliases:
|
|
alias_phase = alias.phase or phase
|
|
grouped.setdefault(alias_phase, [])
|
|
alias_text = (
|
|
alias.when_to_use
|
|
or alias.description
|
|
or hint.guideline
|
|
or hint.short_description
|
|
)
|
|
if alias_text:
|
|
grouped[alias_phase].append(f"- `{alias.name}`: {alias_text}")
|
|
continue
|
|
if hint.guideline:
|
|
grouped[phase].append(f"- `{name}`: {hint.guideline}")
|
|
elif hint.short_description:
|
|
grouped[phase].append(f"- `{name}`: {hint.short_description}")
|
|
|
|
labels = _PHASE_LABELS.get(self.language, _PHASE_LABELS["en"])
|
|
sections: list[str] = []
|
|
for phase in _PHASE_ORDER:
|
|
items = grouped.get(phase) or []
|
|
if not items:
|
|
continue
|
|
label = labels.get(phase, labels["other"])
|
|
sections.append(f"**{label}**\n" + "\n".join(items))
|
|
return "\n\n".join(sections)
|
|
|
|
|
|
__all__ = ["ToolPromptComposer", "load_prompt_hints"]
|