hadith-api/app/models/schemas.py

396 lines
20 KiB
Python

"""
Pydantic response models for the Hadith Scholar API.
v2.0 changes:
- All fields that Neo4j/PG can return as null are now Optional with defaults.
- Added PaginationMeta / PaginatedResponse for paginated list endpoints.
- All existing model_config / json_schema_extra examples preserved.
"""
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
# ── Pagination (NEW in v2.0) ───────────────────────────────────────────────
class PaginationMeta(BaseModel):
total: int = Field(description="Total matching items")
page: int = Field(description="Current page (1-indexed)")
per_page: int = Field(description="Items per page")
pages: int = Field(description="Total pages")
model_config = {
"json_schema_extra": {
"examples": [{"total": 6986, "page": 1, "per_page": 20, "pages": 350}]
}
}
class PaginatedResponse(BaseModel):
meta: PaginationMeta
data: list
# ── Hadith ─────────────────────────────────────────────────────────────────
class HadithSummary(BaseModel):
id: str = Field(description="Unique hadith UUID")
collection: Optional[str] = Field(None, description="Collection name in English")
hadith_number: Optional[int] = Field(None, description="Hadith number within collection")
grade: Optional[str] = Field(None, description="Grading: Sahih, Hasan, Da'if, etc.")
arabic_text: Optional[str] = Field(None, description="Arabic text (truncated in lists)")
sanad_text: Optional[str] = Field(None, description="Sanad (chain) text only")
matn_text: Optional[str] = Field(None, description="Matn (body) text only")
model_config = {
"json_schema_extra": {
"examples": [{
"id": "dcf8df41-3185-4e20-a9af-db3696a48c79",
"collection": "Sahih Bukhari",
"hadith_number": 1,
"grade": "Sahih",
"arabic_text": "حَدَّثَنَا الْحُمَيْدِيُّ عَبْدُ اللَّهِ بْنُ الزُّبَيْرِ...",
"sanad_text": "حَدَّثَنَا الْحُمَيْدِيُّ...",
"matn_text": "إِنَّمَا الأَعْمَالُ بِالنِّيَّاتِ...",
}]
}
}
class TopicTag(BaseModel):
topic_arabic: str = Field("", description="Topic name in Arabic")
topic_english: str = Field("", description="Topic name in English")
category: str = Field("", description="Topic category (فقه, عقيدة, سيرة, etc.)")
class NarratorInChain(BaseModel):
order: Optional[int] = Field(None, description="Position in chain (1 = compiler-end)")
name_arabic: str = Field(description="Narrator Arabic name")
name_transliterated: str = Field("", description="Latin transliteration")
entity_type: str = Field("", description="PERSON, KUNYA, NISBA, TITLE")
transmission_verb: Optional[str] = Field(None, description="حدثنا, أخبرنا, عن, سمعت, etc.")
generation: Optional[str] = Field(None, description="صحابي, تابعي, etc.")
reliability_grade: Optional[str] = Field(None, description="ثقة, صدوق, ضعيف, etc.")
model_config = {
"json_schema_extra": {
"examples": [{
"order": 1,
"name_arabic": "الْحُمَيْدِيُّ",
"name_transliterated": "al-Humaydi",
"entity_type": "NISBA",
"transmission_verb": "حَدَّثَنَا",
"generation": "تابع التابعين",
"reliability_grade": "ثقة",
}]
}
}
class HadithDetail(BaseModel):
id: str = Field(description="Unique hadith UUID")
collection: Optional[str] = Field(None, description="Collection name")
hadith_number: Optional[int] = Field(None, description="Hadith number")
book_number: Optional[int] = Field(None, description="Book number within collection")
grade: Optional[str] = Field(None, description="Grading")
arabic_text: Optional[str] = Field(None, description="Full Arabic text")
english_text: Optional[str] = Field(None, description="English translation")
urdu_text: Optional[str] = Field(None, description="Urdu translation")
sanad_text: Optional[str] = Field(None, description="Sanad (chain) text only")
matn_text: Optional[str] = Field(None, description="Matn (body) text only")
narrator_chain: list[NarratorInChain] = Field(default_factory=list, description="Ordered narrator chain from Neo4j graph")
topics: list[TopicTag] = Field(default_factory=list, description="Topic tags for searchability")
model_config = {
"json_schema_extra": {
"examples": [{
"id": "dcf8df41-3185-4e20-a9af-db3696a48c79",
"collection": "Sahih Bukhari",
"hadith_number": 1,
"grade": "Sahih",
"arabic_text": "حَدَّثَنَا الْحُمَيْدِيُّ عَبْدُ اللَّهِ بْنُ الزُّبَيْرِ...",
"sanad_text": "حَدَّثَنَا الْحُمَيْدِيُّ...",
"matn_text": "إِنَّمَا الأَعْمَالُ بِالنِّيَّاتِ...",
"narrator_chain": [
{"order": 1, "name_arabic": "الْحُمَيْدِيُّ", "name_transliterated": "al-Humaydi", "entity_type": "NISBA", "transmission_verb": "حَدَّثَنَا"},
{"order": 2, "name_arabic": "سُفْيَانُ", "name_transliterated": "Sufyan", "entity_type": "PERSON", "transmission_verb": "حَدَّثَنَا"},
],
"topics": [
{"topic_arabic": "النية", "topic_english": "Intention", "category": "فقه"},
]
}]
}
}
# ── Narrator ───────────────────────────────────────────────────────────────
class NarratorSummary(BaseModel):
name_arabic: str = Field(description="Primary Arabic name")
name_transliterated: str = Field("", description="Latin transliteration")
entity_type: str = Field("", description="PERSON, KUNYA, NISBA, TITLE")
generation: Optional[str] = Field(None, description="طبقة: صحابي، تابعي، تابع التابعين")
reliability_grade: Optional[str] = Field(None, description="جرح وتعديل grade: ثقة، صدوق، ضعيف")
hadith_count: int = Field(0, description="Number of hadiths this narrator appears in")
model_config = {
"json_schema_extra": {
"examples": [{
"name_arabic": "أَبُو هُرَيْرَةَ",
"name_transliterated": "Abu Hurayrah",
"entity_type": "KUNYA",
"generation": "صحابي",
"reliability_grade": "ثقة",
"hadith_count": 5374
}]
}
}
class NameForm(BaseModel):
"""Alternative name forms for a narrator (kunya, nisba, laqab, etc.)."""
name: str = Field(description="Alternative name form")
type: str = Field(description="Name type: PERSON, KUNYA, NISBA, TITLE")
class FamilyInfo(BaseModel):
father: Optional[str] = None
mother: Optional[str] = None
spouse: Optional[str] = None
children: list[str] = Field(default_factory=list)
class PlaceRelation(BaseModel):
place: str = Field(description="Place name in Arabic")
relation: str = Field(description="BORN_IN, LIVED_IN, DIED_IN, or TRAVELED_TO")
model_config = {
"json_schema_extra": {
"examples": [{"place": "المدينة", "relation": "LIVED_IN"}]
}
}
class NarratorProfile(BaseModel):
"""Complete narrator profile — the mobile app profile page."""
name_arabic: str = Field(description="Primary Arabic name")
name_transliterated: str = Field("", description="Latin transliteration")
entity_type: str = Field("", description="PERSON, KUNYA, NISBA, TITLE")
full_nasab: Optional[str] = Field(None, description="Full lineage: فلان بن فلان بن فلان")
kunya: Optional[str] = Field(None, description="أبو/أم name (e.g. أبو هريرة)")
nisba: Optional[str] = Field(None, description="Attributional name (e.g. البخاري، المدني، الزهري)")
laqab: Optional[str] = Field(None, description="Title or epithet (e.g. أمير المؤمنين في الحديث)")
generation: Optional[str] = Field(None, description="طبقة: صحابي، تابعي، تابع التابعين، أتباع تابع التابعين")
reliability_grade: Optional[str] = Field(None, description="جرح وتعديل: ثقة، ثقة حافظ، صدوق، ضعيف، متروك")
reliability_detail: Optional[str] = Field(None, description="Extended grading explanation from scholars")
birth_year_hijri: Optional[int] = Field(None, description="Birth year (Hijri calendar)")
death_year_hijri: Optional[int] = Field(None, description="Death year (Hijri calendar)")
birth_year_ce: Optional[int] = Field(None, description="Birth year (CE)")
death_year_ce: Optional[int] = Field(None, description="Death year (CE)")
biography_summary_arabic: Optional[str] = Field(None, description="2-3 sentence biography in Arabic")
biography_summary_english: Optional[str] = Field(None, description="2-3 sentence biography in English")
total_hadiths_narrated_approx: Optional[int] = Field(None, description="Approximate total hadiths narrated across all collections")
hadith_count: int = Field(0, description="Hadiths in current database")
hadiths: list[HadithSummary] = Field(default_factory=list, description="Sample hadiths narrated (max 50)")
teachers: list[NarratorSummary] = Field(default_factory=list, description="Known teachers / شيوخ")
students: list[NarratorSummary] = Field(default_factory=list, description="Known students / تلاميذ")
name_forms: list[NameForm] = Field(default_factory=list, description="Alternative name forms")
family: Optional[FamilyInfo] = Field(None, description="Family info if known")
places: list[PlaceRelation] = Field(default_factory=list, description="Associated places (born, lived, died, traveled)")
tribes: list[str] = Field(default_factory=list, description="Tribal affiliations (e.g. قريش، دوس، الأنصار)")
bio_verified: bool = Field(False, description="Whether biography has been manually verified against classical sources")
model_config = {
"json_schema_extra": {
"examples": [{
"name_arabic": "أَبُو هُرَيْرَةَ",
"name_transliterated": "Abu Hurayrah",
"entity_type": "KUNYA",
"full_nasab": "عبد الرحمن بن صخر الدوسي",
"kunya": "أبو هريرة",
"nisba": "الدوسي",
"laqab": None,
"generation": "صحابي",
"reliability_grade": "ثقة",
"reliability_detail": "صحابي جليل، أكثر الصحابة رواية للحديث",
"birth_year_hijri": None,
"death_year_hijri": 57,
"birth_year_ce": None,
"death_year_ce": 676,
"biography_summary_arabic": "أبو هريرة الدوسي، صحابي جليل، أكثر الصحابة رواية للحديث النبوي. أسلم عام خيبر ولازم النبي ﷺ.",
"biography_summary_english": "Abu Hurayrah al-Dawsi, a prominent Companion and the most prolific narrator of hadith.",
"total_hadiths_narrated_approx": 5374,
"hadith_count": 142,
"teachers": [{"name_arabic": "النبي ﷺ", "name_transliterated": "Prophet Muhammad", "entity_type": "TITLE", "generation": "نبي", "reliability_grade": None, "hadith_count": 0}],
"students": [{"name_arabic": "الزهري", "name_transliterated": "al-Zuhri", "entity_type": "NISBA", "generation": "تابعي", "reliability_grade": "ثقة", "hadith_count": 0}],
"places": [{"place": "المدينة", "relation": "LIVED_IN"}],
"tribes": ["دوس"],
"bio_verified": False,
}]
}
}
# ── Isnad Chain (D3-ready) ─────────────────────────────────────────────────
class IsnadNode(BaseModel):
name_arabic: str = Field(description="Narrator Arabic name")
name_transliterated: str = Field("", description="Latin transliteration")
entity_type: str = Field("", description="PERSON, KUNYA, NISBA, TITLE")
generation: Optional[str] = Field(None, description="صحابي, تابعي, etc.")
reliability_grade: Optional[str] = Field(None, description="ثقة, صدوق, ضعيف, etc.")
class IsnadLink(BaseModel):
source: str = Field(description="name_arabic of narrator who heard")
target: str = Field(description="name_arabic of narrator who transmitted")
transmission_verb: Optional[str] = Field(None, description="حدثنا, عن, أخبرنا, etc.")
class IsnadChain(BaseModel):
hadith_id: str = Field(description="Hadith UUID")
collection: Optional[str] = Field(None, description="Collection name")
hadith_number: Optional[int] = Field(None, description="Hadith number")
nodes: list[IsnadNode] = Field(default_factory=list, description="Narrators in the chain")
links: list[IsnadLink] = Field(default_factory=list, description="Directed edges: source heard from target")
model_config = {
"json_schema_extra": {
"examples": [{
"hadith_id": "dcf8df41-3185-4e20-a9af-db3696a48c79",
"collection": "Sahih Bukhari",
"hadith_number": 1,
"nodes": [
{"name_arabic": "الْحُمَيْدِيُّ", "name_transliterated": "al-Humaydi", "entity_type": "NISBA", "generation": "تابع التابعين", "reliability_grade": "ثقة"},
{"name_arabic": "سُفْيَانُ بْنُ عُيَيْنَةَ", "name_transliterated": "Sufyan ibn Uyaynah", "entity_type": "PERSON", "generation": "تابع التابعين", "reliability_grade": "ثقة"},
{"name_arabic": "يَحْيَى بْنُ سَعِيدٍ", "name_transliterated": "Yahya ibn Sa'id al-Ansari", "entity_type": "PERSON", "generation": "تابعي", "reliability_grade": "ثقة"},
{"name_arabic": "عُمَرُ بْنُ الْخَطَّابِ", "name_transliterated": "Umar ibn al-Khattab", "entity_type": "PERSON", "generation": "صحابي", "reliability_grade": "ثقة"},
],
"links": [
{"source": "الْحُمَيْدِيُّ", "target": "سُفْيَانُ بْنُ عُيَيْنَةَ", "transmission_verb": "حَدَّثَنَا"},
{"source": "سُفْيَانُ بْنُ عُيَيْنَةَ", "target": "يَحْيَى بْنُ سَعِيدٍ", "transmission_verb": "حَدَّثَنَا"},
{"source": "يَحْيَى بْنُ سَعِيدٍ", "target": "عُمَرُ بْنُ الْخَطَّابِ", "transmission_verb": "عن"},
]
}]
}
}
# ── Relationships / Who Met Who ────────────────────────────────────────────
class NarratorInteraction(BaseModel):
narrator_a: str = Field(description="First narrator Arabic name")
narrator_a_transliterated: str = Field("", description="First narrator transliteration")
narrator_b: str = Field(description="Second narrator Arabic name")
narrator_b_transliterated: str = Field("", description="Second narrator transliteration")
relationship_type: str = Field("", description="NARRATED_FROM, TEACHER_OF, HEARD_BY, STUDENT_OF")
shared_hadith_count: int = Field(0, description="Number of hadiths connecting them")
hadith_ids: list[str] = Field(default_factory=list, description="IDs of shared hadiths (max 20)")
model_config = {
"json_schema_extra": {
"examples": [{
"narrator_a": "الزهري",
"narrator_a_transliterated": "al-Zuhri",
"narrator_b": "أنس بن مالك",
"narrator_b_transliterated": "Anas ibn Malik",
"relationship_type": "NARRATED_FROM",
"shared_hadith_count": 23,
"hadith_ids": ["abc-123", "def-456"]
}]
}
}
class NarratorConnection(BaseModel):
narrator: str = Field(description="Connected narrator Arabic name")
narrator_transliterated: str = Field("", description="Transliteration")
connection_type: str = Field(description="Relationship type")
direction: str = Field(description="'incoming' (they → this) or 'outgoing' (this → them)")
class NarratorNetwork(BaseModel):
center: NarratorSummary
connections: list[NarratorConnection] = Field(default_factory=list)
total_connections: int = 0
class PathNode(BaseModel):
name_arabic: str
name_transliterated: str = ""
generation: Optional[str] = None
class WhoMetWhoResult(BaseModel):
narrator_a: str
narrator_b: str
path: list[PathNode] = Field(default_factory=list)
path_length: Optional[int] = None
relationship_types: list[str] = Field(default_factory=list)
# ── Search ─────────────────────────────────────────────────────────────────
class SemanticSearchResult(BaseModel):
hadith: HadithSummary = Field(description="Matching hadith")
score: float = Field(description="Cosine similarity score (0-1, higher = more relevant)")
collection: str = Field("", description="Collection name")
model_config = {
"json_schema_extra": {
"examples": [{
"hadith": {
"id": "abc-123",
"collection": "Sahih Bukhari",
"hadith_number": 1,
"grade": "Sahih",
"arabic_text": "إِنَّمَا الأَعْمَالُ بِالنِّيَّاتِ..."
},
"score": 0.9234,
"collection": "Sahih Bukhari"
}]
}
}
class FullTextSearchResult(BaseModel):
hadith: HadithSummary = Field(description="Matching hadith")
score: float = Field(description="Elasticsearch relevance score")
highlights: list[str] = Field(default_factory=list, description="Text fragments with <em> highlighted matches")
model_config = {
"json_schema_extra": {
"examples": [{
"hadith": {
"id": "abc-123",
"collection": "Sahih Muslim",
"hadith_number": 1599,
"grade": "Sahih",
"arabic_text": "..."
},
"score": 12.45,
"highlights": ["...عن <em>الصلاة</em> في المسجد..."]
}]
}
}
class CombinedSearchResult(BaseModel):
hadith: HadithSummary
semantic_score: Optional[float] = None
fulltext_score: Optional[float] = None
combined_score: float = 0.0
source: str = Field(description="semantic, fulltext, or both")
# ── Stats ──────────────────────────────────────────────────────────────────
class SystemStats(BaseModel):
hadiths_pg: Optional[int] = None
narrators_neo4j: Optional[int] = None
places_neo4j: Optional[int] = None
tribes_neo4j: Optional[int] = None
relationships_neo4j: Optional[int] = None
embeddings_qdrant: Optional[int] = None
documents_es: Optional[int] = None