feat(stage7): claim clustering & contradiction candidates — semantic grouping, numeric normalization, pairwise analysis
- ClaimClusterModel: LLM-basierte semantische Gruppierung von Claims - ClaimRelationModel: SUPPORTS, CONTRADICTS, DUPLICATE, UNCERTAIN pairwise relations - ClaimNLUModel: numerische Normalisierung (%, Währungen, deutsche/englische Wörter) - stage7_normalize_numerics.py: Regex-basiert mit 200+ deutschen/englischen Zahlenwörtern - stage7_clustering.py: LLM-Clustering + pairwise claim-relation analysis - API: POST cluster-claims, GET clusters, GET claim-relations - 79 tests: numerische Normalisierung, LLM-Parsing, Clustering, Relationen, Edge-Cases - Dedup: claims mit gleichen numerischen Werten werden zusammengefasst
This commit is contained in:
@@ -273,4 +273,110 @@ class ResearchReportModel(Base):
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_research_reports_research_id", "research_id"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 7 — Claim Clustering & Contradiction Candidates
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ClaimRelationType(str, enum.Enum):
|
||||
"""Relationship types between individual claims (Stage 7)."""
|
||||
|
||||
SUPPORTS = "supports"
|
||||
CONTRADICTS = "contradicts"
|
||||
DUPLICATE = "duplicate"
|
||||
UNCERTAIN = "uncertain"
|
||||
|
||||
|
||||
class ClaimClusterModel(Base):
|
||||
"""Semantisches Cluster von Claims innerhalb eines Research-Runs."""
|
||||
|
||||
__tablename__ = "claim_clusters"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid4()))
|
||||
research_run_id = Column(String(36), nullable=False, default=lambda: str(uuid4()))
|
||||
cluster_label = Column(Text, nullable=False)
|
||||
representative_claim_id = Column(String(36), ForeignKey("claims.id"), nullable=True)
|
||||
claim_count = Column(Integer, nullable=False, default=1)
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
claims = relationship("ClaimModel", secondary="claim_cluster_mapping", back_populates="clusters")
|
||||
relations = relationship(
|
||||
"ClaimRelationModel",
|
||||
back_populates="cluster",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_claim_clusters_research_run_id", "research_run_id"),
|
||||
)
|
||||
|
||||
|
||||
# Association table: claims ↔ clusters (many-to-many)
|
||||
_claim_cluster_mapping = Base() # noqa: F811 -- dummy for type resolution
|
||||
|
||||
claim_cluster_mapping = Base()
|
||||
claim_cluster_mapping.__tablename__ = "claim_cluster_mapping"
|
||||
claim_cluster_mapping.id = Column(String(36), primary_key=True, default=lambda: str(uuid4()))
|
||||
claim_cluster_mapping.cluster_id = Column(String(36), ForeignKey("claim_clusters.id"), nullable=False)
|
||||
claim_cluster_mapping.claim_id = Column(String(36), ForeignKey("claims.id"), nullable=False)
|
||||
claim_cluster_mapping.position = Column(Integer, nullable=False, default=0)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_claim_cluster_mapping_cluster_id", "cluster_id"),
|
||||
Index("ix_claim_cluster_mapping_claim_id", "claim_id"),
|
||||
Index("uq_claim_cluster_mapping_cluster_claim", "cluster_id", "claim_id", unique=True),
|
||||
)
|
||||
|
||||
|
||||
class ClaimRelationModel(Base):
|
||||
"""Pairwise Beziehung zwischen zwei Claims innerhalb eines Clusters."""
|
||||
|
||||
__tablename__ = "claim_relations"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid4()))
|
||||
source_claim_id = Column(String(36), ForeignKey("claims.id"), nullable=False)
|
||||
target_claim_id = Column(String(36), ForeignKey("claims.id"), nullable=False)
|
||||
relation_type = Column(Enum(ClaimRelationType), nullable=False)
|
||||
confidence = Column(Float, nullable=False, default=0.5)
|
||||
reason = Column(Text, nullable=True)
|
||||
cluster_id = Column(String(36), ForeignKey("claim_clusters.id"), nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
cluster = relationship("ClaimClusterModel", back_populates="relations")
|
||||
source_claim = relationship("ClaimModel", foreign_keys=[source_claim_id])
|
||||
target_claim = relationship("ClaimModel", foreign_keys=[target_claim_id])
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_claim_relations_source_claim_id", "source_claim_id"),
|
||||
Index("ix_claim_relations_target_claim_id", "target_claim_id"),
|
||||
Index("ix_claim_relations_cluster_id", "cluster_id"),
|
||||
Index(
|
||||
"uq_claim_relations_pair",
|
||||
"source_claim_id",
|
||||
"target_claim_id",
|
||||
"relation_type",
|
||||
unique=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ClaimNLUModel(Base):
|
||||
"""Numerisch normalisierte Ausdrücke aus Claims (Stage 7 — NLU)."""
|
||||
|
||||
__tablename__ = "claim_nlu_numeric"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid4()))
|
||||
claim_id = Column(String(36), ForeignKey("claims.id"), nullable=False)
|
||||
normalized_numeric_value = Column(String(64), nullable=True)
|
||||
original_numeric_text = Column(Text, nullable=False)
|
||||
unit = Column(String(16), nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_claim_nlu_numeric_claim_id", "claim_id"),
|
||||
)
|
||||
Reference in New Issue
Block a user