Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "hedit"
version = "0.7.6a1"
version = "0.7.6.dev3"
description = "Multi-agent system for HED annotation generation and validation"
readme = "PKG_README.md"
requires-python = ">=3.12"
Expand Down
44 changes: 34 additions & 10 deletions src/agents/annotation_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from src.agents.state import HedAnnotationState
from src.utils import extract_text_content
from src.utils.hed_comprehensive_guide import get_comprehensive_hed_guide
from src.utils.hed_comprehensive_guide import format_semantic_hints, get_comprehensive_hed_guide
from src.utils.json_schema_loader import HedJsonSchemaLoader, load_latest_schema

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -60,21 +60,23 @@ def _build_system_prompt(
self,
vocabulary: list[str],
extendable_tags: list[str],
semantic_hints: list[dict] | None = None,
no_extend: bool = False,
) -> str:
"""Build the system prompt for the annotation agent.

The system prompt is static per schema version (vocabulary + rules)
to enable prompt caching across requests. Semantic hints are placed
in the user prompt instead.

Args:
vocabulary: List of valid short-form HED tags
extendable_tags: Tags that allow extension
semantic_hints: Optional semantic search results with relevant tags
no_extend: If True, prohibit tag extensions

Returns:
Complete system prompt with all HED rules
"""
return get_comprehensive_hed_guide(vocabulary, extendable_tags, semantic_hints, no_extend)
return get_comprehensive_hed_guide(vocabulary, extendable_tags, no_extend)

def _format_tag_suggestions(self, tag_suggestions: dict[str, list[str]]) -> str:
"""Format tag suggestions into a clear instruction block.
Expand All @@ -98,24 +100,46 @@ def _format_tag_suggestions(self, tag_suggestions: dict[str, list[str]]) -> str:
)
return "\n".join(lines)

def _format_semantic_hints(self, semantic_hints: list[dict] | None) -> str:
"""Format semantic hints for inclusion in the user prompt.

Args:
semantic_hints: List of hint dicts with tag, score, source keys

Returns:
Formatted hints section, or empty string if no hints
"""
if not semantic_hints:
return ""

logger.debug("Including %d semantic hints in user prompt", len(semantic_hints))
return "\n" + format_semantic_hints(semantic_hints)

def _build_user_prompt(
self,
description: str,
validation_errors: list[str] | None = None,
tag_suggestions: dict[str, list[str]] | None = None,
previous_annotation: str | None = None,
semantic_hints: list[dict] | None = None,
) -> str:
"""Build the user prompt for annotation.

Semantic hints are included here (not in system prompt) so the
system prompt stays static and cacheable across requests.

Args:
description: Natural language event description
validation_errors: Previous validation errors (if retrying)
tag_suggestions: LSP-suggested valid tags for invalid tags
previous_annotation: The previous annotation attempt (for targeted correction)
semantic_hints: Optional semantic search hints for relevant tags

Returns:
User prompt string
"""
hints_str = self._format_semantic_hints(semantic_hints)

if validation_errors:
errors_str = "\n".join(f"- {error}" for error in validation_errors)
suggestions_str = self._format_tag_suggestions(tag_suggestions or {})
Expand All @@ -133,12 +157,12 @@ def _build_user_prompt(

Fix these errors and generate a corrected HED annotation for:
{description}

{hints_str}
{replacement_note}CRITICAL: Output ONLY the raw HED annotation string."""

return f"""Generate a HED annotation for this event description:
{description}

{hints_str}
CRITICAL: Output ONLY the raw HED annotation string."""

async def annotate(self, state: HedAnnotationState) -> dict:
Expand Down Expand Up @@ -170,17 +194,16 @@ async def annotate(self, state: HedAnnotationState) -> dict:
# Use empty list - LLM will still generate valid annotations
extendable_tags = []

# Build prompts with complete HED rules (including semantic hints if available)
semantic_hints = state.get("semantic_hints", [])
# Build system prompt with HED rules (static per schema version for caching)
no_extend = state.get("no_extend", False)
system_prompt = self._build_system_prompt(
vocabulary,
extendable_tags,
semantic_hints if semantic_hints else None,
no_extend,
)

# Build user prompt with any feedback (use augmented errors with remediation for LLM)
# Build user prompt with feedback and semantic hints
semantic_hints = state.get("semantic_hints", [])
feedbacks = []
if state.get("validation_errors_augmented"):
feedbacks.extend(state["validation_errors_augmented"])
Expand All @@ -200,6 +223,7 @@ async def annotate(self, state: HedAnnotationState) -> dict:
feedbacks or None,
tag_suggestions or None,
previous_annotation,
semantic_hints if semantic_hints else None,
)

# Generate annotation
Expand Down
28 changes: 19 additions & 9 deletions src/utils/hed_comprehensive_guide.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"""


def _format_semantic_hints(hints: list[dict]) -> str:
"""Format semantic hints for inclusion in the guide.
def format_semantic_hints(hints: list[dict]) -> str:
"""Format semantic hints for inclusion in the user prompt.

Args:
hints: List of semantic search results, each with:
Expand All @@ -21,7 +21,7 @@ def _format_semantic_hints(hints: list[dict]) -> str:
- prefix: Optional library prefix (e.g., "sc:")

Returns:
Formatted hints section for the guide
Formatted hints section for the user prompt
"""
if not hints:
return ""
Expand All @@ -33,6 +33,8 @@ def _format_semantic_hints(hints: list[dict]) -> str:

for hint in hints:
tag = hint.get("tag", "")
if not tag:
continue
prefix = hint.get("prefix", "")
score = hint.get("score", 0)
full_tag = f"{prefix}{tag}" if prefix else tag
Expand All @@ -45,7 +47,7 @@ def _format_semantic_hints(hints: list[dict]) -> str:
low_conf.append(full_tag)

lines = [
"## POTENTIALLY RELEVANT TAGS",
"## SEMANTIC HINTS",
"",
"Based on your description, these schema tags may be relevant.",
"Note: this list may contain false positives - use your judgment.",
Expand Down Expand Up @@ -974,7 +976,6 @@ def _build_output_format_section() -> str:
def get_comprehensive_hed_guide(
vocabulary_sample: list[str],
extendable_tags: list[str],
semantic_hints: list[dict] | None = None,
no_extend: bool = False,
) -> str:
"""Generate comprehensive HED annotation guide.
Expand All @@ -983,11 +984,13 @@ def get_comprehensive_hed_guide(
annotation agent. The guide includes vocabulary constraints, semantic
rules, correction workflows, and output format instructions.

Note: Semantic hints are NOT included here to keep the system prompt
static across requests, enabling prompt caching. Hints are passed
in the user prompt instead.

Args:
vocabulary_sample: Full list of valid HED tags (complete vocabulary)
extendable_tags: Tags that allow extension
semantic_hints: Optional list of semantically relevant tags from search
Each dict has: tag, score, source, prefix (optional)
no_extend: If True, add strict instructions to prohibit tag extensions

Returns:
Expand All @@ -997,16 +1000,23 @@ def get_comprehensive_hed_guide(
extend_str = ", ".join(extendable_tags) if not no_extend else "(Extensions disabled)"

# Format optional sections
hints_section = _format_semantic_hints(semantic_hints) if semantic_hints else ""
no_extend_warning = _build_no_extend_warning() if no_extend else ""

# Assemble guide from modular sections
# Note: semantic hints are placed in the user prompt for cache efficiency
sections = [
"# HED ANNOTATION GUIDE\n",
no_extend_warning,
_build_vocabulary_check_section(),
_build_correction_workflow_section(),
hints_section,
(
"## SEMANTIC HINTS\n\n"
"The user message may include a SEMANTIC HINTS section with "
"potentially relevant tags from schema search. If present, use "
"these as guidance for tag selection, but verify each against "
"the vocabulary. If no hints section is present, proceed without them.\n\n"
"---\n\n"
),
_build_semantic_rules_section(),
_build_relation_tags_section(),
_build_event_agent_section(),
Expand Down
4 changes: 2 additions & 2 deletions src/version.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Version information for HEDit."""

__version__ = "0.7.6a1"
__version_info__ = (0, 7, 6, "alpha")
__version__ = "0.7.6.dev3"
__version_info__ = (0, 7, 6, "dev")


def get_version() -> str:
Expand Down
126 changes: 126 additions & 0 deletions tests/test_annotation_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,132 @@ def test_no_previous_annotation_when_none(self):

assert "Previous annotation:" not in result

def test_first_pass_with_semantic_hints(self):
"""Semantic hints should appear in the user prompt on first pass."""
agent = self._make_agent()
result = agent._build_user_prompt(
"A dog chasing a cat",
semantic_hints=[
{"tag": "Animal-agent", "score": 0.9, "source": "hed-lsp"},
{"tag": "Chase", "score": 0.7, "source": "hed-lsp"},
],
)

assert "SEMANTIC HINTS" in result
assert "Animal-agent" in result
assert "Chase" in result
assert "A dog chasing a cat" in result

def test_correction_pass_with_semantic_hints(self):
"""Semantic hints should also appear in correction prompts."""
agent = self._make_agent()
result = agent._build_user_prompt(
"A dog chasing a cat",
validation_errors=["[TAG_INVALID] 'Chase' is not valid"],
semantic_hints=[
{"tag": "Animal-agent", "score": 0.9, "source": "hed-lsp"},
],
)

assert "SEMANTIC HINTS" in result
assert "Animal-agent" in result
assert "TAG_INVALID" in result

def test_no_hints_no_hints_section(self):
"""No hints should not add a hints section to the user prompt."""
agent = self._make_agent()
result = agent._build_user_prompt("A red circle")

assert "SEMANTIC HINTS" not in result

def test_empty_hints_no_hints_section(self):
"""Empty hints list should not add a hints section."""
agent = self._make_agent()
result = agent._build_user_prompt("A red circle", semantic_hints=[])

assert "SEMANTIC HINTS" not in result


class TestFormatSemanticHints:
"""Tests for _format_semantic_hints method on AnnotationAgent."""

def _make_agent(self):
agent = object.__new__(AnnotationAgent)
return agent

def test_none_returns_empty(self):
"""None input should return empty string."""
agent = self._make_agent()
assert agent._format_semantic_hints(None) == ""

def test_empty_list_returns_empty(self):
"""Empty list should return empty string."""
agent = self._make_agent()
assert agent._format_semantic_hints([]) == ""

def test_valid_hints_returns_content(self):
"""Valid hints should produce formatted output."""
agent = self._make_agent()
result = agent._format_semantic_hints(
[
{"tag": "Red", "score": 0.9, "source": "hed-lsp"},
]
)

assert "SEMANTIC HINTS" in result
assert "Red" in result

def test_confidence_bucketing(self):
"""Hints should be categorized by confidence level."""
agent = self._make_agent()
result = agent._format_semantic_hints(
[
{"tag": "HighTag", "score": 0.95, "source": "hed-lsp"},
{"tag": "MedTag", "score": 0.6, "source": "hed-lsp"},
{"tag": "LowTag", "score": 0.3, "source": "hed-lsp"},
]
)

assert "High confidence" in result
assert "HighTag" in result
assert "Medium confidence" in result
assert "MedTag" in result
assert "Lower confidence" in result
assert "LowTag" in result

def test_skips_empty_tags(self):
"""Hints with empty tag should be skipped."""
agent = self._make_agent()
result = agent._format_semantic_hints(
[
{"tag": "", "score": 0.9, "source": "hed-lsp"},
{"tag": "ValidTag", "score": 0.8, "source": "hed-lsp"},
]
)

assert "ValidTag" in result


class TestSystemPromptCaching:
"""Tests that system prompt is static (no dynamic content) for caching."""

def test_system_prompt_has_hints_pointer_not_content(self):
"""System prompt should reference hints but not contain actual hint data."""
from src.utils.hed_comprehensive_guide import get_comprehensive_hed_guide

guide = get_comprehensive_hed_guide(
vocabulary_sample=["Red", "Circle"],
extendable_tags=["Animal"],
)

# Should have the pointer section
assert "## SEMANTIC HINTS" in guide
assert "may include" in guide
# Should NOT contain dynamic hint content
assert "High confidence" not in guide
assert "Medium confidence" not in guide
assert "Lower confidence" not in guide


class TestPromptSections:
"""Tests for prompt structure and sections."""
Expand Down
Loading
Loading