Livrare LOT 1 - Didi

This commit is contained in:
Dezvoltari Evotech 2026-06-25 14:13:25 -07:00
commit 5380c3fc63
990 changed files with 133308 additions and 0 deletions

View file

@ -0,0 +1,124 @@
"""Push validated ExtractedClaim objects into Atomic as Type/Claim atoms.
Each claim becomes a tiny atom with:
- source_url = `{parent_url}#claim={hash8}` so it dedups idempotently and
so `parent_url = source_url.split("#")[0]` is trivial to recover later
- tag inheritance from the parent (Country, Topic, SourceType, Credibility,
Language) plus our two new tags: Type/Claim and Stance/<X>
"""
from __future__ import annotations
from typing import Any
from shared.atomic_api import AtomicApiError, AtomicClient
from shared.logging import get_logger
from shared.taxonomy import TagResolver
from extractor.extract import ExtractedClaim
log = get_logger(__name__)
_STANCE_PATH = {
"ASSERTS": "Stance/Asserts",
"REPORTS": "Stance/Reports",
"REFUTES": "Stance/Refutes",
"QUESTIONS": "Stance/Questions",
"NEUTRAL": "Stance/Neutral",
}
def build_claim_markdown(
claim: ExtractedClaim,
*,
parent_title: str,
parent_url: str,
parent_atom_id: str,
) -> str:
"""Render a claim atom's body as Markdown.
The structure is intentionally consistent so it can be parsed back later
by Didi or by re-indexing scripts.
"""
return (
f"# Claim\n\n"
f"{claim.claim}\n\n"
f"## Quote\n"
f"> {claim.quote}\n\n"
f"## Source\n"
f"- Document: [{parent_title}]({parent_url})\n"
f"- Parent atom: `{parent_atom_id}`\n"
f"- Stance in source: {claim.stance}\n"
f"- Extraction confidence: {claim.confidence:.2f}\n"
)
def build_claim_url(parent_url: str, claim: ExtractedClaim) -> str:
"""Stable hash-based URL fragment so re-extraction dedupes naturally."""
base = parent_url.split("#", 1)[0]
return f"{base}#claim={claim.stable_hash()}"
def inherit_tag_ids(
parent_atom: dict[str, Any],
claim: ExtractedClaim,
resolver: TagResolver,
) -> list[str]:
"""Build the tag-id list for a new claim atom.
Inherits all parent tags except Type/Document, and adds Type/Claim plus
the appropriate Stance/<X>.
"""
type_doc_id = resolver.require("Type/Document")
type_claim_id = resolver.require("Type/Claim")
stance_id = resolver.require(_STANCE_PATH[claim.stance])
parent_tag_ids = [
t["id"] for t in (parent_atom.get("tags") or []) if t.get("id") != type_doc_id
]
return parent_tag_ids + [type_claim_id, stance_id]
async def push_claim(
atomic: AtomicClient,
*,
parent_atom: dict[str, Any],
claim: ExtractedClaim,
parent_title: str,
resolver: TagResolver,
) -> tuple[dict[str, Any] | None, str]:
"""Create one claim atom in Atomic. Returns (atom_dict, status).
Status is one of:
- "created": new atom was created
- "duplicate": same hash already exists, skipped
- "error": creation failed (atom_dict is None)
"""
parent_url = parent_atom.get("source_url") or ""
parent_id = parent_atom.get("id") or ""
claim_url = build_claim_url(parent_url, claim)
# Idempotency: same canonical hash → skip
existing = await atomic.get_atom_by_source_url(claim_url)
if existing:
return existing, "duplicate"
md = build_claim_markdown(
claim,
parent_title=parent_title,
parent_url=parent_url,
parent_atom_id=parent_id,
)
tag_ids = inherit_tag_ids(parent_atom, claim, resolver)
try:
atom = await atomic.create_atom(
content=md,
source_url=claim_url,
tag_ids=tag_ids,
)
return atom, "created"
except AtomicApiError as e:
log.error("push_claim_failed", url=claim_url, status=e.status, body=e.body[:200])
return None, "error"