Skip to content

Commit aaaec24

Browse files
authored
Merge pull request #25 from webstackdev/feature/authority-scoring
Feature/authority scoring
2 parents 15d5cf0 + da2876a commit aaaec24

27 files changed

Lines changed: 1659 additions & 111 deletions

‎SESSION.md‎

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,5 @@
11
# Session Restore Point
22

3-
## 2026-04-29 End Of Day
4-
5-
- WP4 entity extraction is implemented end-to-end.
6-
- Backend added `EntityMention` and `EntityCandidate`, migration `core/migrations/0006_entitycandidate_entitymention.py`, new `core/entity_extraction.py`, and an `extract_entities` pipeline node between classification and relevance.
7-
- Admin now supports reviewing entity candidates and mentions; candidate accept/reject/merge actions are wired in `core/admin.py`.
8-
- API/frontend work is in place for entity mention summaries, project-scoped entity candidate review actions, and the new entity detail page at `frontend/src/app/entities/[id]/page.tsx`.
9-
- `/entities` now shows pending candidates plus recent mention summaries, and links into the entity detail page.
10-
- Focused validation that passed today:
11-
- `pytest core/tests/test_pipeline.py core/tests/test_admin.py -q`
12-
- `pytest core/tests/test_api.py -q`
13-
- `python manage.py check`
14-
- `python manage.py makemigrations --check --dry-run`
15-
- `python3 -m mypy core/pipeline.py core/entity_extraction.py core/embeddings.py core/models.py core/admin.py core/tests/test_pipeline.py core/tests/test_admin.py`
16-
- `cd frontend && npm run typecheck`
17-
- `cd frontend && npm run lint`
18-
- `cd frontend && npx vitest run src/app/entities/__tests__/page.test.tsx src/app/api/entity-candidates/[id]/__tests__/route.test.ts src/app/entities/[id]/__tests__/page.test.tsx`
19-
- Repo-wide `just lint` was rerun after fixing `frontend/src/lib/api.ts` import ordering; backend lint fully passed and direct frontend lint now passes with `FRONTEND_LINT_OK`.
20-
21-
22-
233
## Useful Commands From Today
244

255
```bash

‎core/admin.py‎

Lines changed: 156 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
BlueskyCredentials,
2626
Content,
2727
Entity,
28+
EntityAuthoritySnapshot,
2829
EntityCandidate,
2930
EntityCandidateStatus,
3031
EntityMention,
@@ -39,6 +40,30 @@
3940
from core.plugins import get_plugin_for_source_config, validate_plugin_config
4041

4142

43+
def _score_to_percent(value):
44+
"""Normalize score-like values for display as percentages."""
45+
46+
if value is None:
47+
return None
48+
numeric_value = float(value)
49+
if -1.0 <= numeric_value <= 1.0:
50+
return numeric_value * 100
51+
return numeric_value
52+
53+
54+
def _score_color(value) -> str:
55+
"""Return the admin display color for a score-like value."""
56+
57+
percent_value = _score_to_percent(value)
58+
if percent_value is None:
59+
return "inherit"
60+
if percent_value >= 75:
61+
return "green"
62+
if percent_value >= 40:
63+
return "orange"
64+
return "red"
65+
66+
4267
class BlueskyCredentialsAdminForm(forms.ModelForm):
4368
"""Admin form that accepts a plaintext Bluesky app credential input."""
4469

@@ -205,26 +230,98 @@ class ProjectConfigAdmin(admin.ModelAdmin):
205230
class EntityAdmin(admin.ModelAdmin):
206231
"""Admin configuration for tracked people, vendors, and organizations."""
207232

208-
# Replace 'authority_score' with your new method name
209-
list_display = ("name", "project", "type", "colored_score", "created_at")
233+
list_display = (
234+
"name",
235+
"project",
236+
"type",
237+
"colored_score",
238+
"latest_snapshot_summary",
239+
"created_at",
240+
)
210241
search_fields = ("name", "project__name")
211242

212243
@admin.display(description="Authority Score", ordering="authority_score")
213244
def colored_score(self, obj):
214245
"""Render the authority score with a traffic-light color cue."""
215246

216-
# Choose a color based on the value
217-
if obj.authority_score >= 80:
218-
color = "green"
219-
elif obj.authority_score >= 50:
220-
color = "orange"
221-
else:
222-
color = "red"
247+
percent_value = _score_to_percent(obj.authority_score)
248+
color = _score_color(obj.authority_score)
223249

224250
return format_html(
225251
'<span style="color: {}; font-weight: bold;">{}</span>',
226252
color,
227-
obj.authority_score,
253+
f"{percent_value:.1f}%",
254+
)
255+
256+
@admin.display(description="Latest Snapshot")
257+
def latest_snapshot_summary(self, obj):
258+
"""Show the latest authority component breakdown for an entity."""
259+
260+
latest_snapshot = obj.authority_snapshots.order_by("-computed_at").first()
261+
if latest_snapshot is None:
262+
return "-"
263+
mention_value = f"{_score_to_percent(latest_snapshot.mention_component):.1f}%"
264+
feedback_value = f"{_score_to_percent(latest_snapshot.feedback_component):.1f}%"
265+
duplicate_value = (
266+
f"{_score_to_percent(latest_snapshot.duplicate_component):.1f}%"
267+
)
268+
decayed_value = f"{_score_to_percent(latest_snapshot.decayed_prior):.1f}%"
269+
return format_html(
270+
(
271+
'<span title="Mention {}, Feedback {}, Duplicate {}, Carry {}">'
272+
"M {} | F {} | D {} | Carry {}</span>"
273+
),
274+
mention_value,
275+
feedback_value,
276+
duplicate_value,
277+
decayed_value,
278+
mention_value,
279+
feedback_value,
280+
duplicate_value,
281+
decayed_value,
282+
)
283+
284+
285+
@admin.register(EntityAuthoritySnapshot)
286+
class EntityAuthoritySnapshotAdmin(admin.ModelAdmin):
287+
"""Admin view for persisted authority-score history."""
288+
289+
list_display = (
290+
"entity",
291+
"project",
292+
"display_final_score",
293+
"display_components",
294+
"computed_at",
295+
)
296+
list_filter = (("project", admin.RelatedOnlyFieldListFilter), "computed_at")
297+
search_fields = ("entity__name", "project__name")
298+
autocomplete_fields = ("entity", "project")
299+
300+
@admin.display(description="Final Score", ordering="final_score")
301+
def display_final_score(self, obj):
302+
"""Render the recomputed final authority score as a percentage."""
303+
304+
percent_value = _score_to_percent(obj.final_score)
305+
return format_html(
306+
'<span style="color: {}; font-weight: bold;">{}</span>',
307+
_score_color(obj.final_score),
308+
f"{percent_value:.1f}%",
309+
)
310+
311+
@admin.display(description="Components")
312+
def display_components(self, obj):
313+
"""Render the stored authority components in a compact summary."""
314+
315+
mention_value = f"{_score_to_percent(obj.mention_component):.1f}%"
316+
feedback_value = f"{_score_to_percent(obj.feedback_component):.1f}%"
317+
duplicate_value = f"{_score_to_percent(obj.duplicate_component):.1f}%"
318+
decayed_value = f"{_score_to_percent(obj.decayed_prior):.1f}%"
319+
return format_html(
320+
"M {} | F {} | D {} | Carry {}",
321+
mention_value,
322+
feedback_value,
323+
duplicate_value,
324+
decayed_value,
228325
)
229326

230327

@@ -387,6 +484,7 @@ class ContentAdmin(admin.ModelAdmin):
387484

388485
list_display = (
389486
"display_relevance",
487+
"display_authority_adjusted_score",
390488
"duplicate_badge",
391489
"duplicate_parent",
392490
"is_active",
@@ -510,18 +608,31 @@ def view_trace(self, obj):
510608
link_label,
511609
)
512610

513-
@admin.display(description="Score")
611+
@admin.display(description="Base Score")
514612
def display_relevance(self, obj):
515613
"""Render the relevance score with a coarse color-coded severity band."""
516614

517615
if obj.relevance_score is None:
518616
return "-"
519-
color = (
520-
"green"
521-
if obj.relevance_score > 75
522-
else "orange" if obj.relevance_score > 40 else "red"
617+
percent_value = _score_to_percent(obj.relevance_score)
618+
return format_html(
619+
'<b style="color: {}">{}</b>',
620+
_score_color(obj.relevance_score),
621+
f"{percent_value:.1f}%",
622+
)
623+
624+
@admin.display(description="Adjusted")
625+
def display_authority_adjusted_score(self, obj):
626+
"""Render the authority-adjusted relevance score when available."""
627+
628+
if obj.authority_adjusted_score is None:
629+
return "-"
630+
percent_value = _score_to_percent(obj.authority_adjusted_score)
631+
return format_html(
632+
'<b style="color: {}">{}</b>',
633+
_score_color(obj.authority_adjusted_score),
634+
f"{percent_value:.1f}%",
523635
)
524-
return format_html('<b style="color: {};">{}%</b>', color, obj.relevance_score)
525636

526637
@admin.display(description="Duplicates", ordering="duplicate_signal_count")
527638
def duplicate_badge(self, obj):
@@ -546,15 +657,40 @@ def changelist_view(self, request, extra_context=None):
546657
"""Augment the changelist with content dashboard statistics."""
547658

548659
queryset = self.get_queryset(request)
549-
metrics = queryset.aggregate(avg_score=Avg("relevance_score"))
660+
metrics = queryset.aggregate(
661+
avg_score=Avg("relevance_score"),
662+
avg_adjusted_score=Avg("authority_adjusted_score"),
663+
)
550664

551665
extra_context = extra_context or {}
552666
extra_context["dashboard_stats"] = [
553667
{
554-
"title": "Avg Relevance",
555-
"value": f"{metrics['avg_score'] or 0:.1f}%",
668+
"title": "Avg Base Score",
669+
"value": (
670+
f"{_score_to_percent(metrics['avg_score']):.1f}%"
671+
if metrics["avg_score"] is not None
672+
else "-"
673+
),
556674
"icon": "insights",
557-
"color": "success" if (metrics["avg_score"] or 0) > 70 else "warning",
675+
"color": (
676+
"success"
677+
if _score_color(metrics["avg_score"]) == "green"
678+
else "warning"
679+
),
680+
},
681+
{
682+
"title": "Avg Adjusted Score",
683+
"value": (
684+
f"{_score_to_percent(metrics['avg_adjusted_score']):.1f}%"
685+
if metrics["avg_adjusted_score"] is not None
686+
else "-"
687+
),
688+
"icon": "auto_graph",
689+
"color": (
690+
"success"
691+
if _score_color(metrics["avg_adjusted_score"]) == "green"
692+
else "warning"
693+
),
558694
},
559695
{
560696
"title": "Total Filtered",

0 commit comments

Comments
 (0)