File size: 3,933 Bytes
79b0bef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
"""
src/api/routes/entities.py
────────────────────────────────────────────────────────────────
Endpoints for entity frequency and co-occurrence data.

Routes
──────
  GET /entities              β€” list entities for a note
  GET /entities/top          β€” top-N most frequent entities
  GET /entities/cooccurrence β€” co-occurrence pairs for the graph
────────────────────────────────────────────────────────────────
"""

from __future__ import annotations

from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session

from src.api.schemas import EntitySummary, TopEntitiesResponse, TopEntityItem
from src.db.connection import get_db_session
from src.db.repository import EntityRepository
from src.utils.logger import get_logger

logger = get_logger(__name__)
router = APIRouter(prefix="/entities", tags=["Entities"])


@router.get(
    "",
    response_model = list[EntitySummary],
    summary        = "List entities for a stored note",
)
def list_entities_for_note(
    note_id: int     = Query(..., description="Note ID to fetch entities for"),
    db: Session      = Depends(get_db_session),
) -> list[EntitySummary]:
    """Return all entities extracted from a specific stored note.

    Entities are ordered by their position in the note text
    (start character offset ascending).

    Args:
        note_id: Primary key of the parent note.

    Returns:
        List of :class:`EntitySummary` objects.
    """
    entities = EntityRepository.list_by_note(db, note_id)
    return [EntitySummary.model_validate(e) for e in entities]


@router.get(
    "/top",
    response_model = TopEntitiesResponse,
    summary        = "Most frequently extracted entities",
)
def top_entities(
    label: str | None = Query(
        None,
        description = "Filter to this entity type (DISEASE, MEDICATION, etc.)",
    ),
    limit: int = Query(20, ge=1, le=100),
    db: Session = Depends(get_db_session),
) -> TopEntitiesResponse:
    """Return the top-N most frequently extracted entity texts.

    Used by the dashboard frequency bar chart.

    Args:
        label: Optional entity type filter.
        limit: Number of top entities to return.

    Returns:
        :class:`TopEntitiesResponse` with ranked entity texts and counts.
    """
    rows = EntityRepository.top_entities(db, label=label, limit=limit)
    return TopEntitiesResponse(
        label = label,
        limit = limit,
        items = [TopEntityItem(text=t, count=c) for t, c in rows],
    )


@router.get(
    "/cooccurrence",
    response_model = list[dict],
    summary        = "Entity co-occurrence pairs",
)
def cooccurrence_pairs(
    label:     str = Query("DISEASE", description="Entity type to analyse"),
    min_count: int = Query(5, ge=1, description="Minimum co-occurrence count"),
    limit:     int = Query(100, ge=1, le=500),
    db: Session    = Depends(get_db_session),
) -> list[dict]:
    """Return entity pairs that frequently co-occur in the same note.

    Used by the dashboard to build the co-occurrence network graph.
    Each item in the response is an edge in the graph with source,
    target, and weight (co-occurrence count).

    Args:
        label:     Entity type β€” usually DISEASE for clinical insight.
        min_count: Minimum co-occurrences for a pair to be included.
        limit:     Maximum number of pairs to return.

    Returns:
        List of dicts: ``{"source": str, "target": str, "weight": int}``.
    """
    pairs = EntityRepository.cooccurrence_pairs(
        db, label=label, min_count=min_count, limit=limit
    )
    return [
        {"source": a, "target": b, "weight": w}
        for a, b, w in pairs
    ]