Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-rag/src/lexigram/ai/rag/citations/_models.py: 98%
84 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
1"""Citation domain models and enums."""
3from __future__ import annotations
5from dataclasses import dataclass, field
6from datetime import UTC, datetime
7from enum import StrEnum
8from typing import Any
11class CitationStyle(StrEnum):
12 """Citation formatting styles."""
14 NUMERIC = "numeric" # [1], [2], etc.
15 AUTHOR_YEAR = "author_year" # (Smith, 2023)
16 FOOTNOTE = "footnote" # Superscript numbers
17 INLINE = "inline" # Inline source references
18 APA = "apa" # APA style
19 MLA = "mla" # MLA style
20 CHICAGO = "chicago" # Chicago style
21 CUSTOM = "custom" # Custom format
24class SourceType(StrEnum):
25 """Types of sources."""
27 DOCUMENT = "document"
28 WEB_PAGE = "web_page"
29 ARTICLE = "article"
30 BOOK = "book"
31 PAPER = "paper"
32 DATABASE = "database"
33 API = "api"
34 INTERNAL = "internal"
35 UNKNOWN = "unknown"
38@dataclass
39class Source:
40 """A source of information."""
42 id: str
43 content: str
44 source_type: SourceType = SourceType.UNKNOWN
45 title: str | None = None
46 author: str | None = None
47 url: str | None = None
48 publication_date: str | None = None
49 page_number: int | None = None
50 chapter: str | None = None
51 metadata: dict[str, Any] = field(default_factory=dict)
52 timestamp: str = field(
53 default_factory=lambda: datetime.now(UTC).isoformat(),
54 )
56 def __repr__(self) -> str:
57 """Return string representation."""
58 parts = [f"id={self.id}"]
59 if self.title:
60 parts.append(f"title={self.title[:30]}")
61 if self.author:
62 parts.append(f"author={self.author}")
63 return f"Source({', '.join(parts)})"
66@dataclass
67class Citation:
68 """A citation linking text to source."""
70 source_id: str
71 text_span: str
72 start_char: int | None = None
73 end_char: int | None = None
74 confidence: float = 1.0
75 relevance_score: float = 1.0
76 citation_number: int | None = None
77 metadata: dict[str, Any] = field(default_factory=dict)
79 def __repr__(self) -> str:
80 """Return string representation."""
81 return (
82 f"Citation(source={self.source_id}, "
83 f"span='{self.text_span[:30]}...', "
84 f"conf={self.confidence:.2f})"
85 )
88@dataclass
89class CitedResponse:
90 """A response with citations."""
92 text: str
93 sources: list[Source]
94 citations: list[Citation]
95 citation_style: CitationStyle = CitationStyle.NUMERIC
96 metadata: dict[str, Any] = field(default_factory=dict)
97 timestamp: str = field(
98 default_factory=lambda: datetime.now(UTC).isoformat(),
99 )
101 @property
102 def num_sources(self) -> int:
103 """Number of unique sources."""
104 return len(self.sources)
106 @property
107 def num_citations(self) -> int:
108 """Number of citations."""
109 return len(self.citations)
111 @property
112 def avg_confidence(self) -> float:
113 """Average citation confidence."""
114 if not self.citations:
115 return 0.0
116 return sum(c.confidence for c in self.citations) / len(self.citations)
118 def get_source(self, source_id: str) -> Source | None:
119 """Get source by ID."""
120 for source in self.sources:
121 if source.id == source_id:
122 return source
123 return None
125 def get_citations_for_source(self, source_id: str) -> list[Citation]:
126 """Get all citations for a source."""
127 return list(filter(lambda c: c.source_id == source_id, self.citations))
129 def __repr__(self) -> str:
130 """Return string representation."""
131 return (
132 f"CitedResponse(sources={self.num_sources}, "
133 f"citations={self.num_citations}, "
134 f"style={self.citation_style.value})"
135 )