""" 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 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": ["...عن الصلاة في المسجد..."] }] } } 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