hedit 0.7.5.dev2__tar.gz → 0.7.6a2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hedit-0.7.5.dev2/hedit.egg-info → hedit-0.7.6a2}/PKG-INFO +1 -1
- {hedit-0.7.5.dev2 → hedit-0.7.6a2/hedit.egg-info}/PKG-INFO +1 -1
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/pyproject.toml +1 -1
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/agents/annotation_agent.py +34 -10
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/agents/workflow.py +31 -28
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/api/main.py +27 -17
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/utils/hed_comprehensive_guide.py +19 -9
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/version.py +2 -2
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_annotation_agent.py +126 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_comprehensive_guide.py +13 -27
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/LICENSE +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/PKG_README.md +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/README.md +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/hedit.egg-info/SOURCES.txt +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/hedit.egg-info/dependency_links.txt +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/hedit.egg-info/entry_points.txt +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/hedit.egg-info/requires.txt +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/hedit.egg-info/top_level.txt +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/setup.cfg +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/__init__.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/agents/__init__.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/agents/assessment_agent.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/agents/evaluation_agent.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/agents/feedback_summarizer.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/agents/feedback_triage_agent.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/agents/state.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/agents/validation_agent.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/agents/vision_agent.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/api/__init__.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/api/models.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/api/security.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/cli/__init__.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/cli/api_executor.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/cli/client.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/cli/commands/__init__.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/cli/config.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/cli/executor.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/cli/local_executor.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/cli/main.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/cli/output.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/scripts/__init__.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/scripts/process_feedback.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/telemetry/__init__.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/telemetry/collector.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/telemetry/schema.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/telemetry/storage.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/utils/__init__.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/utils/error_remediation.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/utils/github_client.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/utils/hed_rules.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/utils/image_processing.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/utils/json_schema_loader.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/utils/litellm_llm.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/utils/openrouter_llm.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/utils/schema_loader.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/validation/__init__.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/validation/hed_lsp.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/src/validation/hed_validator.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_api_endpoints.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_cli_client.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_cli_config.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_cli_integration.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_cli_main.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_error_remediation.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_feedback_integration.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_feedback_triage.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_github_client.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_hed_lsp.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_integration_openrouter.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_json_schema_loader.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_keyword_extraction.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_litellm_llm.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_no_extend_propagation.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_openrouter_llm.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_schema_loader.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_security.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_state.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_telemetry.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_validation.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_validation_agent.py +0 -0
- {hedit-0.7.5.dev2 → hedit-0.7.6a2}/tests/test_version.py +0 -0
|
@@ -12,7 +12,7 @@ from langchain_core.messages import HumanMessage, SystemMessage
|
|
|
12
12
|
|
|
13
13
|
from src.agents.state import HedAnnotationState
|
|
14
14
|
from src.utils import extract_text_content
|
|
15
|
-
from src.utils.hed_comprehensive_guide import get_comprehensive_hed_guide
|
|
15
|
+
from src.utils.hed_comprehensive_guide import format_semantic_hints, get_comprehensive_hed_guide
|
|
16
16
|
from src.utils.json_schema_loader import HedJsonSchemaLoader, load_latest_schema
|
|
17
17
|
|
|
18
18
|
logger = logging.getLogger(__name__)
|
|
@@ -60,21 +60,23 @@ class AnnotationAgent:
|
|
|
60
60
|
self,
|
|
61
61
|
vocabulary: list[str],
|
|
62
62
|
extendable_tags: list[str],
|
|
63
|
-
semantic_hints: list[dict] | None = None,
|
|
64
63
|
no_extend: bool = False,
|
|
65
64
|
) -> str:
|
|
66
65
|
"""Build the system prompt for the annotation agent.
|
|
67
66
|
|
|
67
|
+
The system prompt is static per schema version (vocabulary + rules)
|
|
68
|
+
to enable prompt caching across requests. Semantic hints are placed
|
|
69
|
+
in the user prompt instead.
|
|
70
|
+
|
|
68
71
|
Args:
|
|
69
72
|
vocabulary: List of valid short-form HED tags
|
|
70
73
|
extendable_tags: Tags that allow extension
|
|
71
|
-
semantic_hints: Optional semantic search results with relevant tags
|
|
72
74
|
no_extend: If True, prohibit tag extensions
|
|
73
75
|
|
|
74
76
|
Returns:
|
|
75
77
|
Complete system prompt with all HED rules
|
|
76
78
|
"""
|
|
77
|
-
return get_comprehensive_hed_guide(vocabulary, extendable_tags,
|
|
79
|
+
return get_comprehensive_hed_guide(vocabulary, extendable_tags, no_extend)
|
|
78
80
|
|
|
79
81
|
def _format_tag_suggestions(self, tag_suggestions: dict[str, list[str]]) -> str:
|
|
80
82
|
"""Format tag suggestions into a clear instruction block.
|
|
@@ -98,24 +100,46 @@ class AnnotationAgent:
|
|
|
98
100
|
)
|
|
99
101
|
return "\n".join(lines)
|
|
100
102
|
|
|
103
|
+
def _format_semantic_hints(self, semantic_hints: list[dict] | None) -> str:
|
|
104
|
+
"""Format semantic hints for inclusion in the user prompt.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
semantic_hints: List of hint dicts with tag, score, source keys
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Formatted hints section, or empty string if no hints
|
|
111
|
+
"""
|
|
112
|
+
if not semantic_hints:
|
|
113
|
+
return ""
|
|
114
|
+
|
|
115
|
+
logger.debug("Including %d semantic hints in user prompt", len(semantic_hints))
|
|
116
|
+
return "\n" + format_semantic_hints(semantic_hints)
|
|
117
|
+
|
|
101
118
|
def _build_user_prompt(
|
|
102
119
|
self,
|
|
103
120
|
description: str,
|
|
104
121
|
validation_errors: list[str] | None = None,
|
|
105
122
|
tag_suggestions: dict[str, list[str]] | None = None,
|
|
106
123
|
previous_annotation: str | None = None,
|
|
124
|
+
semantic_hints: list[dict] | None = None,
|
|
107
125
|
) -> str:
|
|
108
126
|
"""Build the user prompt for annotation.
|
|
109
127
|
|
|
128
|
+
Semantic hints are included here (not in system prompt) so the
|
|
129
|
+
system prompt stays static and cacheable across requests.
|
|
130
|
+
|
|
110
131
|
Args:
|
|
111
132
|
description: Natural language event description
|
|
112
133
|
validation_errors: Previous validation errors (if retrying)
|
|
113
134
|
tag_suggestions: LSP-suggested valid tags for invalid tags
|
|
114
135
|
previous_annotation: The previous annotation attempt (for targeted correction)
|
|
136
|
+
semantic_hints: Optional semantic search hints for relevant tags
|
|
115
137
|
|
|
116
138
|
Returns:
|
|
117
139
|
User prompt string
|
|
118
140
|
"""
|
|
141
|
+
hints_str = self._format_semantic_hints(semantic_hints)
|
|
142
|
+
|
|
119
143
|
if validation_errors:
|
|
120
144
|
errors_str = "\n".join(f"- {error}" for error in validation_errors)
|
|
121
145
|
suggestions_str = self._format_tag_suggestions(tag_suggestions or {})
|
|
@@ -133,12 +157,12 @@ class AnnotationAgent:
|
|
|
133
157
|
|
|
134
158
|
Fix these errors and generate a corrected HED annotation for:
|
|
135
159
|
{description}
|
|
136
|
-
|
|
160
|
+
{hints_str}
|
|
137
161
|
{replacement_note}CRITICAL: Output ONLY the raw HED annotation string."""
|
|
138
162
|
|
|
139
163
|
return f"""Generate a HED annotation for this event description:
|
|
140
164
|
{description}
|
|
141
|
-
|
|
165
|
+
{hints_str}
|
|
142
166
|
CRITICAL: Output ONLY the raw HED annotation string."""
|
|
143
167
|
|
|
144
168
|
async def annotate(self, state: HedAnnotationState) -> dict:
|
|
@@ -170,17 +194,16 @@ CRITICAL: Output ONLY the raw HED annotation string."""
|
|
|
170
194
|
# Use empty list - LLM will still generate valid annotations
|
|
171
195
|
extendable_tags = []
|
|
172
196
|
|
|
173
|
-
# Build
|
|
174
|
-
semantic_hints = state.get("semantic_hints", [])
|
|
197
|
+
# Build system prompt with HED rules (static per schema version for caching)
|
|
175
198
|
no_extend = state.get("no_extend", False)
|
|
176
199
|
system_prompt = self._build_system_prompt(
|
|
177
200
|
vocabulary,
|
|
178
201
|
extendable_tags,
|
|
179
|
-
semantic_hints if semantic_hints else None,
|
|
180
202
|
no_extend,
|
|
181
203
|
)
|
|
182
204
|
|
|
183
|
-
# Build user prompt with
|
|
205
|
+
# Build user prompt with feedback and semantic hints
|
|
206
|
+
semantic_hints = state.get("semantic_hints", [])
|
|
184
207
|
feedbacks = []
|
|
185
208
|
if state.get("validation_errors_augmented"):
|
|
186
209
|
feedbacks.extend(state["validation_errors_augmented"])
|
|
@@ -200,6 +223,7 @@ CRITICAL: Output ONLY the raw HED annotation string."""
|
|
|
200
223
|
feedbacks or None,
|
|
201
224
|
tag_suggestions or None,
|
|
202
225
|
previous_annotation,
|
|
226
|
+
semantic_hints if semantic_hints else None,
|
|
203
227
|
)
|
|
204
228
|
|
|
205
229
|
# Generate annotation
|
|
@@ -4,6 +4,7 @@ This module defines the multi-agent workflow that orchestrates
|
|
|
4
4
|
annotation, validation, evaluation, and assessment.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import asyncio
|
|
7
8
|
import logging
|
|
8
9
|
import time
|
|
9
10
|
from pathlib import Path
|
|
@@ -239,28 +240,32 @@ class HedAnnotationWorkflow:
|
|
|
239
240
|
semantic_hints: list[dict] = []
|
|
240
241
|
|
|
241
242
|
if keywords and self.hed_lsp_client:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
result = self.hed_lsp_client.suggest
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
"[WORKFLOW] hed-lsp suggestion failed for '%s': %s",
|
|
259
|
-
keyword,
|
|
260
|
-
result.error,
|
|
243
|
+
# Query hed-lsp for each keyword individually for better results
|
|
244
|
+
for keyword in keywords:
|
|
245
|
+
try:
|
|
246
|
+
result = await asyncio.to_thread(self.hed_lsp_client.suggest, keyword)
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.warning("[WORKFLOW] hed-lsp error for '%s': %s", keyword, e)
|
|
249
|
+
continue
|
|
250
|
+
if result.success:
|
|
251
|
+
for s in result.suggestions:
|
|
252
|
+
semantic_hints.append(
|
|
253
|
+
{
|
|
254
|
+
"tag": s.tag,
|
|
255
|
+
"keyword": keyword,
|
|
256
|
+
"score": s.score or 0.0,
|
|
257
|
+
"source": "hed-lsp",
|
|
258
|
+
}
|
|
261
259
|
)
|
|
262
|
-
|
|
263
|
-
|
|
260
|
+
else:
|
|
261
|
+
logger.debug(
|
|
262
|
+
"[WORKFLOW] hed-lsp suggestion failed for '%s': %s",
|
|
263
|
+
keyword,
|
|
264
|
+
result.error,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Deduplicate by tag, keeping highest score
|
|
268
|
+
if semantic_hints:
|
|
264
269
|
seen_tags: dict[str, dict] = {}
|
|
265
270
|
for hint in semantic_hints:
|
|
266
271
|
tag = hint["tag"]
|
|
@@ -268,13 +273,11 @@ class HedAnnotationWorkflow:
|
|
|
268
273
|
seen_tags[tag] = hint
|
|
269
274
|
semantic_hints = sorted(seen_tags.values(), key=lambda h: h["score"], reverse=True)
|
|
270
275
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
except Exception as e:
|
|
277
|
-
logger.warning("[WORKFLOW] hed-lsp error: %s", e, exc_info=True)
|
|
276
|
+
logger.info(
|
|
277
|
+
"[WORKFLOW] hed-lsp suggested %d unique tags from %d keywords",
|
|
278
|
+
len(semantic_hints),
|
|
279
|
+
len(keywords),
|
|
280
|
+
)
|
|
278
281
|
elif keywords:
|
|
279
282
|
# LSP not available; still store keywords for the annotation agent
|
|
280
283
|
logger.info(
|
|
@@ -668,11 +668,16 @@ async def annotate(
|
|
|
668
668
|
or req.headers.get("x-openrouter-model")
|
|
669
669
|
or os.getenv("ANNOTATION_MODEL", "openai/gpt-oss-120b")
|
|
670
670
|
)
|
|
671
|
-
temperature =
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
671
|
+
temperature = request.temperature
|
|
672
|
+
if temperature is None:
|
|
673
|
+
temp_header = req.headers.get("x-openrouter-temperature")
|
|
674
|
+
if temp_header is not None:
|
|
675
|
+
try:
|
|
676
|
+
temperature = float(temp_header)
|
|
677
|
+
except ValueError:
|
|
678
|
+
temperature = None
|
|
679
|
+
if temperature is None:
|
|
680
|
+
temperature = _byok_config.get("temperature", 0.1)
|
|
676
681
|
|
|
677
682
|
event = TelemetryEvent.create(
|
|
678
683
|
description=request.description,
|
|
@@ -874,11 +879,16 @@ async def annotate_from_image(
|
|
|
874
879
|
or req.headers.get("x-openrouter-model")
|
|
875
880
|
or os.getenv("ANNOTATION_MODEL", "openai/gpt-oss-120b")
|
|
876
881
|
)
|
|
877
|
-
temperature =
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
+
temperature = request.temperature
|
|
883
|
+
if temperature is None:
|
|
884
|
+
temp_header = req.headers.get("x-openrouter-temperature")
|
|
885
|
+
if temp_header is not None:
|
|
886
|
+
try:
|
|
887
|
+
temperature = float(temp_header)
|
|
888
|
+
except ValueError:
|
|
889
|
+
temperature = None
|
|
890
|
+
if temperature is None:
|
|
891
|
+
temperature = _byok_config.get("temperature", 0.1)
|
|
882
892
|
|
|
883
893
|
event = TelemetryEvent.create(
|
|
884
894
|
description=image_description, # Use generated image description
|
|
@@ -1204,7 +1214,7 @@ async def annotate_stream(
|
|
|
1204
1214
|
description=request.description,
|
|
1205
1215
|
)
|
|
1206
1216
|
except Exception:
|
|
1207
|
-
logging.
|
|
1217
|
+
logging.warning("Telemetry collection failed for streaming request", exc_info=True)
|
|
1208
1218
|
|
|
1209
1219
|
yield send_event("done", {"message": "Workflow completed"})
|
|
1210
1220
|
|
|
@@ -1230,7 +1240,7 @@ async def annotate_stream(
|
|
|
1230
1240
|
description=request.description,
|
|
1231
1241
|
)
|
|
1232
1242
|
except Exception:
|
|
1233
|
-
logging.
|
|
1243
|
+
logging.warning("Telemetry collection failed on timeout", exc_info=True)
|
|
1234
1244
|
yield send_event("done", {"message": "Workflow ended with error"})
|
|
1235
1245
|
except RateLimitError:
|
|
1236
1246
|
logging.exception("Streaming workflow rate limit")
|
|
@@ -1252,7 +1262,7 @@ async def annotate_stream(
|
|
|
1252
1262
|
description=request.description,
|
|
1253
1263
|
)
|
|
1254
1264
|
except Exception:
|
|
1255
|
-
logging.
|
|
1265
|
+
logging.warning("Telemetry collection failed on rate limit", exc_info=True)
|
|
1256
1266
|
yield send_event("done", {"message": "Workflow ended with error"})
|
|
1257
1267
|
except Exception:
|
|
1258
1268
|
logging.exception("Streaming workflow error")
|
|
@@ -1274,7 +1284,7 @@ async def annotate_stream(
|
|
|
1274
1284
|
description=request.description,
|
|
1275
1285
|
)
|
|
1276
1286
|
except Exception:
|
|
1277
|
-
logging.
|
|
1287
|
+
logging.warning("Telemetry collection failed on error", exc_info=True)
|
|
1278
1288
|
yield send_event("done", {"message": "Workflow ended with error"})
|
|
1279
1289
|
|
|
1280
1290
|
return StreamingResponse(
|
|
@@ -1583,7 +1593,7 @@ async def annotate_from_image_stream(
|
|
|
1583
1593
|
description=image_description or "image-annotation-failed",
|
|
1584
1594
|
)
|
|
1585
1595
|
except Exception:
|
|
1586
|
-
logging.
|
|
1596
|
+
logging.warning("Telemetry collection failed on image timeout", exc_info=True)
|
|
1587
1597
|
yield send_event("done", {"message": "Workflow ended with error"})
|
|
1588
1598
|
except RateLimitError:
|
|
1589
1599
|
logging.exception("Streaming image workflow rate limit")
|
|
@@ -1605,7 +1615,7 @@ async def annotate_from_image_stream(
|
|
|
1605
1615
|
description=image_description or "image-annotation-failed",
|
|
1606
1616
|
)
|
|
1607
1617
|
except Exception:
|
|
1608
|
-
logging.
|
|
1618
|
+
logging.warning("Telemetry collection failed on image rate limit", exc_info=True)
|
|
1609
1619
|
yield send_event("done", {"message": "Workflow ended with error"})
|
|
1610
1620
|
except Exception:
|
|
1611
1621
|
logging.exception("Streaming image annotation workflow error")
|
|
@@ -1627,7 +1637,7 @@ async def annotate_from_image_stream(
|
|
|
1627
1637
|
description=image_description or "image-annotation-failed",
|
|
1628
1638
|
)
|
|
1629
1639
|
except Exception:
|
|
1630
|
-
logging.
|
|
1640
|
+
logging.warning("Telemetry collection failed on image error", exc_info=True)
|
|
1631
1641
|
yield send_event("done", {"message": "Workflow ended with error"})
|
|
1632
1642
|
|
|
1633
1643
|
return StreamingResponse(
|
|
@@ -10,8 +10,8 @@ assembles them into the full system prompt.
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def
|
|
14
|
-
"""Format semantic hints for inclusion in the
|
|
13
|
+
def format_semantic_hints(hints: list[dict]) -> str:
|
|
14
|
+
"""Format semantic hints for inclusion in the user prompt.
|
|
15
15
|
|
|
16
16
|
Args:
|
|
17
17
|
hints: List of semantic search results, each with:
|
|
@@ -21,7 +21,7 @@ def _format_semantic_hints(hints: list[dict]) -> str:
|
|
|
21
21
|
- prefix: Optional library prefix (e.g., "sc:")
|
|
22
22
|
|
|
23
23
|
Returns:
|
|
24
|
-
Formatted hints section for the
|
|
24
|
+
Formatted hints section for the user prompt
|
|
25
25
|
"""
|
|
26
26
|
if not hints:
|
|
27
27
|
return ""
|
|
@@ -33,6 +33,8 @@ def _format_semantic_hints(hints: list[dict]) -> str:
|
|
|
33
33
|
|
|
34
34
|
for hint in hints:
|
|
35
35
|
tag = hint.get("tag", "")
|
|
36
|
+
if not tag:
|
|
37
|
+
continue
|
|
36
38
|
prefix = hint.get("prefix", "")
|
|
37
39
|
score = hint.get("score", 0)
|
|
38
40
|
full_tag = f"{prefix}{tag}" if prefix else tag
|
|
@@ -45,7 +47,7 @@ def _format_semantic_hints(hints: list[dict]) -> str:
|
|
|
45
47
|
low_conf.append(full_tag)
|
|
46
48
|
|
|
47
49
|
lines = [
|
|
48
|
-
"##
|
|
50
|
+
"## SEMANTIC HINTS",
|
|
49
51
|
"",
|
|
50
52
|
"Based on your description, these schema tags may be relevant.",
|
|
51
53
|
"Note: this list may contain false positives - use your judgment.",
|
|
@@ -974,7 +976,6 @@ Just output the raw HED annotation string directly.
|
|
|
974
976
|
def get_comprehensive_hed_guide(
|
|
975
977
|
vocabulary_sample: list[str],
|
|
976
978
|
extendable_tags: list[str],
|
|
977
|
-
semantic_hints: list[dict] | None = None,
|
|
978
979
|
no_extend: bool = False,
|
|
979
980
|
) -> str:
|
|
980
981
|
"""Generate comprehensive HED annotation guide.
|
|
@@ -983,11 +984,13 @@ def get_comprehensive_hed_guide(
|
|
|
983
984
|
annotation agent. The guide includes vocabulary constraints, semantic
|
|
984
985
|
rules, correction workflows, and output format instructions.
|
|
985
986
|
|
|
987
|
+
Note: Semantic hints are NOT included here to keep the system prompt
|
|
988
|
+
static across requests, enabling prompt caching. Hints are passed
|
|
989
|
+
in the user prompt instead.
|
|
990
|
+
|
|
986
991
|
Args:
|
|
987
992
|
vocabulary_sample: Full list of valid HED tags (complete vocabulary)
|
|
988
993
|
extendable_tags: Tags that allow extension
|
|
989
|
-
semantic_hints: Optional list of semantically relevant tags from search
|
|
990
|
-
Each dict has: tag, score, source, prefix (optional)
|
|
991
994
|
no_extend: If True, add strict instructions to prohibit tag extensions
|
|
992
995
|
|
|
993
996
|
Returns:
|
|
@@ -997,16 +1000,23 @@ def get_comprehensive_hed_guide(
|
|
|
997
1000
|
extend_str = ", ".join(extendable_tags) if not no_extend else "(Extensions disabled)"
|
|
998
1001
|
|
|
999
1002
|
# Format optional sections
|
|
1000
|
-
hints_section = _format_semantic_hints(semantic_hints) if semantic_hints else ""
|
|
1001
1003
|
no_extend_warning = _build_no_extend_warning() if no_extend else ""
|
|
1002
1004
|
|
|
1003
1005
|
# Assemble guide from modular sections
|
|
1006
|
+
# Note: semantic hints are placed in the user prompt for cache efficiency
|
|
1004
1007
|
sections = [
|
|
1005
1008
|
"# HED ANNOTATION GUIDE\n",
|
|
1006
1009
|
no_extend_warning,
|
|
1007
1010
|
_build_vocabulary_check_section(),
|
|
1008
1011
|
_build_correction_workflow_section(),
|
|
1009
|
-
|
|
1012
|
+
(
|
|
1013
|
+
"## SEMANTIC HINTS\n\n"
|
|
1014
|
+
"The user message may include a SEMANTIC HINTS section with "
|
|
1015
|
+
"potentially relevant tags from schema search. If present, use "
|
|
1016
|
+
"these as guidance for tag selection, but verify each against "
|
|
1017
|
+
"the vocabulary. If no hints section is present, proceed without them.\n\n"
|
|
1018
|
+
"---\n\n"
|
|
1019
|
+
),
|
|
1010
1020
|
_build_semantic_rules_section(),
|
|
1011
1021
|
_build_relation_tags_section(),
|
|
1012
1022
|
_build_event_agent_section(),
|
|
@@ -193,6 +193,132 @@ class TestBuildUserPrompt:
|
|
|
193
193
|
|
|
194
194
|
assert "Previous annotation:" not in result
|
|
195
195
|
|
|
196
|
+
def test_first_pass_with_semantic_hints(self):
|
|
197
|
+
"""Semantic hints should appear in the user prompt on first pass."""
|
|
198
|
+
agent = self._make_agent()
|
|
199
|
+
result = agent._build_user_prompt(
|
|
200
|
+
"A dog chasing a cat",
|
|
201
|
+
semantic_hints=[
|
|
202
|
+
{"tag": "Animal-agent", "score": 0.9, "source": "hed-lsp"},
|
|
203
|
+
{"tag": "Chase", "score": 0.7, "source": "hed-lsp"},
|
|
204
|
+
],
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
assert "SEMANTIC HINTS" in result
|
|
208
|
+
assert "Animal-agent" in result
|
|
209
|
+
assert "Chase" in result
|
|
210
|
+
assert "A dog chasing a cat" in result
|
|
211
|
+
|
|
212
|
+
def test_correction_pass_with_semantic_hints(self):
|
|
213
|
+
"""Semantic hints should also appear in correction prompts."""
|
|
214
|
+
agent = self._make_agent()
|
|
215
|
+
result = agent._build_user_prompt(
|
|
216
|
+
"A dog chasing a cat",
|
|
217
|
+
validation_errors=["[TAG_INVALID] 'Chase' is not valid"],
|
|
218
|
+
semantic_hints=[
|
|
219
|
+
{"tag": "Animal-agent", "score": 0.9, "source": "hed-lsp"},
|
|
220
|
+
],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
assert "SEMANTIC HINTS" in result
|
|
224
|
+
assert "Animal-agent" in result
|
|
225
|
+
assert "TAG_INVALID" in result
|
|
226
|
+
|
|
227
|
+
def test_no_hints_no_hints_section(self):
|
|
228
|
+
"""No hints should not add a hints section to the user prompt."""
|
|
229
|
+
agent = self._make_agent()
|
|
230
|
+
result = agent._build_user_prompt("A red circle")
|
|
231
|
+
|
|
232
|
+
assert "SEMANTIC HINTS" not in result
|
|
233
|
+
|
|
234
|
+
def test_empty_hints_no_hints_section(self):
|
|
235
|
+
"""Empty hints list should not add a hints section."""
|
|
236
|
+
agent = self._make_agent()
|
|
237
|
+
result = agent._build_user_prompt("A red circle", semantic_hints=[])
|
|
238
|
+
|
|
239
|
+
assert "SEMANTIC HINTS" not in result
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class TestFormatSemanticHints:
|
|
243
|
+
"""Tests for _format_semantic_hints method on AnnotationAgent."""
|
|
244
|
+
|
|
245
|
+
def _make_agent(self):
|
|
246
|
+
agent = object.__new__(AnnotationAgent)
|
|
247
|
+
return agent
|
|
248
|
+
|
|
249
|
+
def test_none_returns_empty(self):
|
|
250
|
+
"""None input should return empty string."""
|
|
251
|
+
agent = self._make_agent()
|
|
252
|
+
assert agent._format_semantic_hints(None) == ""
|
|
253
|
+
|
|
254
|
+
def test_empty_list_returns_empty(self):
|
|
255
|
+
"""Empty list should return empty string."""
|
|
256
|
+
agent = self._make_agent()
|
|
257
|
+
assert agent._format_semantic_hints([]) == ""
|
|
258
|
+
|
|
259
|
+
def test_valid_hints_returns_content(self):
|
|
260
|
+
"""Valid hints should produce formatted output."""
|
|
261
|
+
agent = self._make_agent()
|
|
262
|
+
result = agent._format_semantic_hints(
|
|
263
|
+
[
|
|
264
|
+
{"tag": "Red", "score": 0.9, "source": "hed-lsp"},
|
|
265
|
+
]
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
assert "SEMANTIC HINTS" in result
|
|
269
|
+
assert "Red" in result
|
|
270
|
+
|
|
271
|
+
def test_confidence_bucketing(self):
|
|
272
|
+
"""Hints should be categorized by confidence level."""
|
|
273
|
+
agent = self._make_agent()
|
|
274
|
+
result = agent._format_semantic_hints(
|
|
275
|
+
[
|
|
276
|
+
{"tag": "HighTag", "score": 0.95, "source": "hed-lsp"},
|
|
277
|
+
{"tag": "MedTag", "score": 0.6, "source": "hed-lsp"},
|
|
278
|
+
{"tag": "LowTag", "score": 0.3, "source": "hed-lsp"},
|
|
279
|
+
]
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
assert "High confidence" in result
|
|
283
|
+
assert "HighTag" in result
|
|
284
|
+
assert "Medium confidence" in result
|
|
285
|
+
assert "MedTag" in result
|
|
286
|
+
assert "Lower confidence" in result
|
|
287
|
+
assert "LowTag" in result
|
|
288
|
+
|
|
289
|
+
def test_skips_empty_tags(self):
|
|
290
|
+
"""Hints with empty tag should be skipped."""
|
|
291
|
+
agent = self._make_agent()
|
|
292
|
+
result = agent._format_semantic_hints(
|
|
293
|
+
[
|
|
294
|
+
{"tag": "", "score": 0.9, "source": "hed-lsp"},
|
|
295
|
+
{"tag": "ValidTag", "score": 0.8, "source": "hed-lsp"},
|
|
296
|
+
]
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
assert "ValidTag" in result
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class TestSystemPromptCaching:
|
|
303
|
+
"""Tests that system prompt is static (no dynamic content) for caching."""
|
|
304
|
+
|
|
305
|
+
def test_system_prompt_has_hints_pointer_not_content(self):
|
|
306
|
+
"""System prompt should reference hints but not contain actual hint data."""
|
|
307
|
+
from src.utils.hed_comprehensive_guide import get_comprehensive_hed_guide
|
|
308
|
+
|
|
309
|
+
guide = get_comprehensive_hed_guide(
|
|
310
|
+
vocabulary_sample=["Red", "Circle"],
|
|
311
|
+
extendable_tags=["Animal"],
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Should have the pointer section
|
|
315
|
+
assert "## SEMANTIC HINTS" in guide
|
|
316
|
+
assert "may include" in guide
|
|
317
|
+
# Should NOT contain dynamic hint content
|
|
318
|
+
assert "High confidence" not in guide
|
|
319
|
+
assert "Medium confidence" not in guide
|
|
320
|
+
assert "Lower confidence" not in guide
|
|
321
|
+
|
|
196
322
|
|
|
197
323
|
class TestPromptSections:
|
|
198
324
|
"""Tests for prompt structure and sections."""
|
|
@@ -45,38 +45,24 @@ class TestComprehensiveGuide:
|
|
|
45
45
|
# Should show extensions as disabled
|
|
46
46
|
assert "(Extensions disabled)" in guide
|
|
47
47
|
|
|
48
|
-
def
|
|
49
|
-
"""Test guide
|
|
48
|
+
def test_guide_has_semantic_hints_pointer(self):
|
|
49
|
+
"""Test guide includes a pointer to check user message for semantic hints."""
|
|
50
50
|
vocabulary = ["Event", "Reward", "Animal-agent"]
|
|
51
51
|
extendable_tags = ["Label"]
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
assert "POTENTIALLY RELEVANT TAGS" in guide
|
|
62
|
-
assert "Reward" in guide
|
|
63
|
-
assert "Animal-agent" in guide
|
|
64
|
-
# Check confidence indicators
|
|
65
|
-
assert "high" in guide.lower() or "0.95" in guide
|
|
66
|
-
|
|
67
|
-
def test_guide_with_semantic_hints_and_no_extend(self):
|
|
68
|
-
"""Test guide with both semantic hints and no_extend."""
|
|
52
|
+
|
|
53
|
+
guide = get_comprehensive_hed_guide(vocabulary, extendable_tags)
|
|
54
|
+
|
|
55
|
+
# System prompt should point to user message for hints (not contain them)
|
|
56
|
+
assert "SEMANTIC HINTS" in guide
|
|
57
|
+
assert "user message" in guide.lower()
|
|
58
|
+
|
|
59
|
+
def test_guide_no_extend_with_hints_pointer(self):
|
|
60
|
+
"""Test guide with no_extend has both hints pointer and extension warning."""
|
|
69
61
|
vocabulary = ["Event", "Visual-presentation"]
|
|
70
62
|
extendable_tags = ["Label"]
|
|
71
|
-
semantic_hints = [
|
|
72
|
-
{"tag": "Visual-presentation", "prefix": "", "score": 0.9, "source": "keyword"},
|
|
73
|
-
]
|
|
74
63
|
|
|
75
|
-
guide = get_comprehensive_hed_guide(
|
|
76
|
-
vocabulary, extendable_tags, semantic_hints=semantic_hints, no_extend=True
|
|
77
|
-
)
|
|
64
|
+
guide = get_comprehensive_hed_guide(vocabulary, extendable_tags, no_extend=True)
|
|
78
65
|
|
|
79
|
-
|
|
80
|
-
assert "POTENTIALLY RELEVANT TAGS" in guide
|
|
66
|
+
assert "SEMANTIC HINTS" in guide
|
|
81
67
|
assert "EXTENSIONS STRICTLY PROHIBITED" in guide
|
|
82
68
|
assert "(Extensions disabled)" in guide
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|