Skip to content

Commit 14105d5

Browse files
committed
Address PR #1854 review: blank-span guard, prepared type, docs link/scope, comments
- Skip blank/whitespace exact_string in add_annotations_from_exact_strings (prevents wasted geocoder call and an infinite occurrence loop on find("")) - Tighten prepared tuple type to dict[str, str] | None - Fix stale docs link (walkthrough/step-9-corpus-actions.md) - Note migration system_instructions is a point-in-time snapshot - Scope end-to-end cleanup delete to the test corpus (was DB-wide OC_* wipe) - Comment the unsaved-then-save pattern, the migration identity check, and the population tie-break assumption in test_city_without_hints_resolves_to_france
1 parent 75fe3d6 commit 14105d5

6 files changed

Lines changed: 44 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4646
degraded state is visible; added tests pinning the migration→settings
4747
`system_instructions` wiring and proving geocoding is resolved once per item
4848
and reused for every occurrence (not re-resolved per match).
49+
- **Blank-span guard (#1822 review)**: `add_annotations_from_exact_strings`
50+
now skips items whose `exact_string` is blank/whitespace-only. Besides
51+
wasting a geocoder call, `doc_text.find("")` returns the search start so the
52+
occurrence loop never advanced — a blank span would have spun forever.
53+
Mirrors the blank-`raw_text` guard in `_create_geographic_annotation`.
4954

5055
- **Chunked (resumable) uploads for large files** — work around the 100 MB
5156
per-request body ceiling that upstream proxies (Cloudflare) impose on the

docs/agents/location_tagger.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ resolved coordinates and administrative codes in its `data` payload, so the
77
places show up as pins on the **Discover** and **Corpus Home** maps.
88

99
It is wired to run through the ordinary
10-
[corpus-actions](../walkthrough/key-concepts/corpus-actions.md) framework — the
10+
[corpus-actions](../walkthrough/step-9-corpus-actions.md) framework — the
1111
same execution path used by the built-in Document Assistant and Corpus
1212
Assistant — so you configure it once on a corpus and it runs automatically.
1313

@@ -42,6 +42,13 @@ them, so existing callers of the tool keep working unchanged.
4242
docker compose -f production.yml --profile migrate up migrate
4343
```
4444

45+
> **Note — the migration is a one-time snapshot.** It copies
46+
> `DEFAULT_LOCATION_TAGGER_INSTRUCTIONS` from settings into the agent's
47+
> `system_instructions` column at creation time. If you later improve the
48+
> prompt in `config/settings/base.py`, **existing databases keep the old
49+
> prompt** — update the `Location Tagger` agent record via the Django admin
50+
> (or a follow-up data migration) to pick up the revision.
51+
4552
2. **Add a corpus action.** On the corpus you want tagged, create a
4653
`CorpusAction` that points at the **Location Tagger** agent and choose a
4754
trigger:

docs/test_scripts/location_tagger_end_to_end.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,16 @@ from opencontractserver.annotations.models import Annotation
8989
from opencontractserver.constants.annotations import (
9090
OC_CITY_LABEL, OC_COUNTRY_LABEL, OC_STATE_LABEL,
9191
)
92-
Annotation.objects.filter(
93-
annotation_label__text__in=[OC_COUNTRY_LABEL, OC_STATE_LABEL, OC_CITY_LABEL]
94-
).delete()
9592
from opencontractserver.corpuses.models import CorpusAction
96-
CorpusAction.objects.filter(name='Auto location tagger').delete()
93+
# Scope the cleanup to the corpus this test targeted. A global
94+
# annotation_label__text__in delete would wipe OC_* annotations across EVERY
95+
# corpus in the database — never run that against a populated staging/prod DB.
96+
action = CorpusAction.objects.filter(name='Auto location tagger').first()
97+
if action is not None:
98+
Annotation.objects.filter(
99+
corpus=action.corpus,
100+
annotation_label__text__in=[OC_COUNTRY_LABEL, OC_STATE_LABEL, OC_CITY_LABEL],
101+
).delete()
102+
action.delete()
97103
"
98104
```

opencontractserver/agents/migrations/0015_create_location_tagger_agent.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ def create_location_tagger_agent(apps, schema_editor):
6363
instructions = getattr(
6464
settings, "DEFAULT_LOCATION_TAGGER_INSTRUCTIONS", _FALLBACK_INSTRUCTIONS
6565
)
66+
# Identity check (``is``, not ``==``) is intentional: ``getattr`` returns the
67+
# exact ``_FALLBACK_INSTRUCTIONS`` object only when the setting is missing, so
68+
# this distinguishes "setting absent" from "setting happens to equal the
69+
# fallback text".
6670
if instructions is _FALLBACK_INSTRUCTIONS:
6771
# The rich production prompt lives in settings; if it is absent (e.g. a
6872
# stripped-down CI image) we still create a working agent, but with the

opencontractserver/llms/tools/core_tools/annotations.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,18 @@ def add_annotations_from_exact_strings(
225225
# ``item`` is an AnnotationItem TypedDict (always a dict at runtime);
226226
# the inner isinstance still guards against the LLM sending a non-dict
227227
# ``hints`` value.
228+
exact_str = str(item["exact_string"])
229+
# Skip blank/whitespace-only spans. Besides wasting a geocoder call for
230+
# OC_* labels, ``doc_text.find("")`` returns the search start so the
231+
# occurrence loop below would never advance (infinite loop). Mirrors the
232+
# blank-``raw_text`` guard in ``_create_geographic_annotation``.
233+
if not exact_str.strip():
234+
continue
228235
raw_hints = item.get("hints")
229236
parsed_items.append(
230237
(
231238
str(item["label_text"]),
232-
str(item["exact_string"]),
239+
exact_str,
233240
raw_hints if isinstance(raw_hints, dict) else None,
234241
)
235242
)
@@ -349,7 +356,7 @@ def _create_annotation(pos: int, end_idx: int, label_obj):
349356
# the payload for every occurrence inside the loop. Only the OC_*
350357
# geographic labels geocode; every other label keeps ``data`` NULL
351358
# (backward compatible with existing callers such as structured extraction).
352-
prepared: list[tuple[str, str, dict | None]] = []
359+
prepared: list[tuple[str, str, dict[str, str] | None]] = []
353360
for label_text, exact_str, hints in parsed_items:
354361
geocode_label_type = LABEL_TEXT_TO_GEOCODE_LABEL_TYPE.get(label_text)
355362
annotation_data = None
@@ -379,6 +386,9 @@ def _create_annotation(pos: int, end_idx: int, label_obj):
379386

380387
end_idx = pos + len(exact_str)
381388

389+
# ``_create_annotation`` returns an *unsaved* instance; set the
390+
# pre-resolved geocode ``data`` (when present) before the single
391+
# ``save()`` so each occurrence is one DB write, not two.
382392
annot_obj = _create_annotation(pos, end_idx, label_obj)
383393
if annotation_data is not None:
384394
annot_obj.data = annotation_data

opencontractserver/tests/test_location_tagger_agent.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,11 @@ def test_city_hints_disambiguate_paris_to_texas(self):
161161
self.assertLess(ann.data["lng"], 0)
162162

163163
def test_city_without_hints_resolves_to_france(self):
164-
# Population tie-break: unhinted "Paris" is the French one.
164+
# Assumption: an unhinted "Paris" resolves to the highest-population
165+
# match (Paris, FR over Paris, TX). This ties to the geocoder's current
166+
# tie-break dataset; if that ranking changes, relax this to assert only
167+
# ``ann.data["geocoded"] is True`` and leave the country to the
168+
# geocoding service's own tests.
165169
ids = self._annotate([{"label_text": OC_CITY_LABEL, "exact_string": "Paris"}])
166170
ann = Annotation.objects.get(pk=ids[0])
167171
self.assertTrue(ann.data["geocoded"])

0 commit comments

Comments
 (0)