199 lines
7.6 KiB
Python
199 lines
7.6 KiB
Python
from __future__ import annotations
|
|
|
|
from .shared import *
|
|
|
|
|
|
class SemanticFactQueryMixin:
|
|
async def upsert_fact(
|
|
self,
|
|
*,
|
|
entity_type: str,
|
|
entity_id: int,
|
|
fact_namespace: str,
|
|
fact_key: str,
|
|
fact_value_text: str,
|
|
fact_value_norm: Optional[str],
|
|
value_type: str,
|
|
confidence: float,
|
|
evidence_source_type: Optional[str],
|
|
evidence_source_id: Optional[int],
|
|
evidence_excerpt: Optional[str],
|
|
extract_version: Optional[str],
|
|
freshness_at: Optional[str],
|
|
) -> SemanticFact:
|
|
normalized_fact_value = _normalize_fact_value(fact_value_norm or fact_value_text)
|
|
row = (
|
|
await self.db.execute(
|
|
select(SemanticFact).where(
|
|
and_(
|
|
SemanticFact.tenant_id == self.tenant_id,
|
|
SemanticFact.entity_type == str(entity_type or "").strip(),
|
|
SemanticFact.entity_id == int(entity_id),
|
|
SemanticFact.fact_key == str(fact_key or "").strip(),
|
|
SemanticFact.fact_value_norm == normalized_fact_value,
|
|
)
|
|
)
|
|
)
|
|
).scalar_one_or_none()
|
|
|
|
if row is None:
|
|
row = SemanticFact(
|
|
tenant_id=self.tenant_id,
|
|
entity_type=str(entity_type or "").strip(),
|
|
entity_id=int(entity_id),
|
|
fact_namespace=str(fact_namespace or "").strip() or "general",
|
|
fact_key=str(fact_key or "").strip(),
|
|
fact_value_text=str(fact_value_text or "").strip(),
|
|
fact_value_norm=normalized_fact_value,
|
|
value_type=str(value_type or "").strip() or "keyword",
|
|
confidence=max(0.0, min(1.0, float(confidence or 0.0))),
|
|
evidence_source_type=str(evidence_source_type or "").strip() or None,
|
|
evidence_source_id=int(evidence_source_id) if evidence_source_id is not None else None,
|
|
evidence_excerpt=str(evidence_excerpt or "").strip() or None,
|
|
extract_version=str(extract_version or "").strip() or None,
|
|
freshness_at=str(freshness_at or "").strip() or None,
|
|
is_active=True,
|
|
)
|
|
self.db.add(row)
|
|
await self.db.flush()
|
|
return row
|
|
|
|
row.fact_namespace = str(fact_namespace or "").strip() or row.fact_namespace
|
|
row.fact_value_text = str(fact_value_text or "").strip() or row.fact_value_text
|
|
row.value_type = str(value_type or "").strip() or row.value_type
|
|
row.confidence = max(0.0, min(1.0, float(confidence or 0.0)))
|
|
row.evidence_source_type = str(evidence_source_type or "").strip() or row.evidence_source_type
|
|
row.evidence_source_id = int(evidence_source_id) if evidence_source_id is not None else row.evidence_source_id
|
|
row.evidence_excerpt = str(evidence_excerpt or "").strip() or row.evidence_excerpt
|
|
row.extract_version = str(extract_version or "").strip() or row.extract_version
|
|
row.freshness_at = str(freshness_at or "").strip() or row.freshness_at
|
|
row.is_active = True
|
|
await self.db.flush()
|
|
return row
|
|
|
|
async def deactivate_entity_facts(
|
|
self,
|
|
*,
|
|
entity_type: str,
|
|
entity_id: int,
|
|
) -> int:
|
|
normalized_entity_type = _normalized_entity_type(entity_type)
|
|
rows = (
|
|
await self.db.execute(
|
|
select(SemanticFact).where(
|
|
and_(
|
|
SemanticFact.tenant_id == self.tenant_id,
|
|
SemanticFact.entity_type == normalized_entity_type,
|
|
SemanticFact.entity_id == int(entity_id),
|
|
SemanticFact.is_active.is_(True),
|
|
)
|
|
)
|
|
)
|
|
).scalars().all()
|
|
for row in rows:
|
|
row.is_active = False
|
|
await self.db.flush()
|
|
return len(rows)
|
|
|
|
async def enqueue_entity_event(
|
|
self,
|
|
*,
|
|
entity_type: str,
|
|
entity_id: int,
|
|
operation: str,
|
|
trigger_type: str,
|
|
extra_payload: Optional[dict[str, Any]] = None,
|
|
) -> IngestEvent:
|
|
normalized_entity_type = _normalized_entity_type(entity_type)
|
|
event_key = f"{normalized_entity_type}:{int(entity_id)}"
|
|
payload = {
|
|
"entity_type": normalized_entity_type,
|
|
"entity_id": int(entity_id),
|
|
"operation": str(operation or "extract").strip() or "extract",
|
|
"trigger_type": str(trigger_type or "manual_retry").strip() or "manual_retry",
|
|
"tenant_id": int(self.tenant_id),
|
|
}
|
|
if isinstance(extra_payload, dict):
|
|
for key, value in extra_payload.items():
|
|
payload[str(key)] = value
|
|
|
|
row = (
|
|
await self.db.execute(
|
|
select(IngestEvent).where(
|
|
and_(
|
|
IngestEvent.tenant_id == self.tenant_id,
|
|
IngestEvent.channel == SEMANTIC_FACT_EXTRACT_CHANNEL,
|
|
IngestEvent.event_key == event_key,
|
|
)
|
|
)
|
|
)
|
|
).scalar_one_or_none()
|
|
if row is None:
|
|
row = IngestEvent(
|
|
tenant_id=self.tenant_id,
|
|
channel=SEMANTIC_FACT_EXTRACT_CHANNEL,
|
|
event_key=event_key,
|
|
status="pending",
|
|
payload=payload,
|
|
retry_count=0,
|
|
error_message=None,
|
|
processed_at=None,
|
|
)
|
|
self.db.add(row)
|
|
else:
|
|
row.status = "pending"
|
|
row.payload = payload
|
|
row.retry_count = 0
|
|
row.error_message = None
|
|
row.processed_at = None
|
|
await self.db.flush()
|
|
return row
|
|
|
|
async def query_facts(
|
|
self,
|
|
*,
|
|
entity_type: str,
|
|
query_type: str,
|
|
filters: Optional[list[dict[str, Any]]] = None,
|
|
entity_id: Optional[int] = None,
|
|
limit: int = 20,
|
|
) -> list[SemanticFact]:
|
|
_ = query_type
|
|
stmt = select(SemanticFact).where(
|
|
and_(
|
|
SemanticFact.tenant_id == self.tenant_id,
|
|
SemanticFact.entity_type == _normalized_entity_type(entity_type),
|
|
SemanticFact.is_active.is_(True),
|
|
)
|
|
)
|
|
if entity_id is not None:
|
|
stmt = stmt.where(SemanticFact.entity_id == int(entity_id))
|
|
|
|
for item in list(filters or []):
|
|
if not isinstance(item, dict):
|
|
continue
|
|
fact_key = str(item.get("fact_key") or item.get("field") or "").strip()
|
|
if fact_key:
|
|
stmt = stmt.where(SemanticFact.fact_key == fact_key)
|
|
raw_value = str(item.get("value") or item.get("normalized_value") or "").strip()
|
|
normalized_value = _normalize_fact_value(item.get("normalized_value") or raw_value)
|
|
if normalized_value:
|
|
stmt = stmt.where(
|
|
or_(
|
|
SemanticFact.fact_value_norm == normalized_value,
|
|
func.lower(SemanticFact.fact_value_text) == raw_value.lower(),
|
|
)
|
|
)
|
|
|
|
rows = (
|
|
await self.db.execute(
|
|
stmt.order_by(SemanticFact.confidence.desc(), SemanticFact.updated_at.desc(), SemanticFact.id.desc()).limit(
|
|
max(1, int(limit))
|
|
)
|
|
)
|
|
).scalars().all()
|
|
return list(rows)
|
|
|
|
|
|
__all__ = ["SemanticFactQueryMixin"]
|