PMFlow/backend/app/services/adaptive_rag_service/mixin_retrieval_graph.py

262 lines
12 KiB
Python

"""Adaptive RAG mixin module."""
from __future__ import annotations
from .shared import *
class AdaptiveRAGRetrievalGraphMixin:
async def _retrieve_graph_context(
self,
question: str,
*,
project_id: Optional[int],
resolved_entities: Optional[dict[str, Any]] = None,
) -> list[RetrievedItem]:
try:
resolution = resolved_entities or {}
matched_entities = (
resolution.get("matched_entities") if isinstance(resolution.get("matched_entities"), list) else []
)
anchor_ids_raw = (
resolution.get("anchor_entity_ids") if isinstance(resolution.get("anchor_entity_ids"), list) else []
)
anchor_ids: list[int] = []
for value in anchor_ids_raw:
try:
anchor_ids.append(int(value))
except Exception:
continue
seed_entities: list[KGEntity] = []
if matched_entities:
ids: list[int] = []
for row in matched_entities:
if not isinstance(row, dict):
continue
try:
ids.append(int(row.get("entity_id")))
except Exception:
continue
if ids:
seed_entities = (
await self.db.execute(
select(KGEntity).where(
and_(
KGEntity.tenant_id == self.tenant_id,
KGEntity.id.in_(ids),
)
)
)
).scalars().all()
if not seed_entities:
term_collector = getattr(self, "_collect_lookup_terms", None)
terms = term_collector(question=question, limit=6) if callable(term_collector) else []
if not terms:
normalizer = getattr(self, "_normalize_lookup_text", None)
normalized = normalizer(question) if callable(normalizer) else str(question or "").strip()
if normalized:
terms = [normalized[:32]]
if terms:
conds = []
for term in terms[:6]:
like = f"%{term}%"
conds.append(KGEntity.display_name.ilike(like))
conds.append(KGEntity.entity_key.ilike(like))
seed_entities = (
await self.db.execute(
select(KGEntity)
.where(
and_(
KGEntity.tenant_id == self.tenant_id,
or_(*conds),
)
)
.order_by(KGEntity.last_seen_at.desc(), KGEntity.id.desc())
.limit(15)
)
).scalars().all()
if not seed_entities and anchor_ids:
seed_entities = (
await self.db.execute(
select(KGEntity).where(
and_(
KGEntity.tenant_id == self.tenant_id,
KGEntity.id.in_(anchor_ids[:10]),
)
)
)
).scalars().all()
if not seed_entities:
return []
seed_ids = [int(x.id) for x in seed_entities]
relation_rows = (
await self.db.execute(
select(KGRelation)
.where(
and_(
KGRelation.tenant_id == self.tenant_id,
or_(
KGRelation.subject_entity_id.in_(seed_ids),
KGRelation.object_entity_id.in_(seed_ids),
),
)
)
.order_by(KGRelation.weight.desc(), KGRelation.id.desc())
.limit(120)
)
).scalars().all()
entity_ids: set[int] = set(seed_ids)
for rel in relation_rows:
entity_ids.add(int(rel.subject_entity_id))
entity_ids.add(int(rel.object_entity_id))
entities = (
await self.db.execute(
select(KGEntity).where(
and_(
KGEntity.tenant_id == self.tenant_id,
KGEntity.id.in_(list(entity_ids)),
)
)
)
).scalars().all()
entity_map = {int(row.id): row for row in entities}
relation_ids = [int(row.id) for row in relation_rows]
evidence_rows: list[KGEvidence] = []
if relation_ids or entity_ids:
evidence_rows = (
await self.db.execute(
select(KGEvidence)
.where(
and_(
KGEvidence.tenant_id == self.tenant_id,
or_(
KGEvidence.relation_id.in_(relation_ids or [-1]),
KGEvidence.entity_id.in_(list(entity_ids) or [-1]),
),
)
)
.order_by(KGEvidence.occurred_at.desc(), KGEvidence.id.desc())
.limit(220)
)
).scalars().all()
evidence_by_relation: dict[int, str] = {}
evidence_by_entity: dict[int, str] = {}
evidence_updated_at_by_relation: dict[int, str] = {}
evidence_updated_at_by_entity: dict[int, str] = {}
for ev in evidence_rows:
excerpt = str(ev.content_excerpt or "").strip()
occurred_at = self._isoformat_or_none(getattr(ev, "occurred_at", None))
if not excerpt:
excerpt = ""
if ev.relation_id is not None and int(ev.relation_id) not in evidence_by_relation:
evidence_by_relation[int(ev.relation_id)] = excerpt[:320]
if occurred_at and ev.relation_id is not None and int(ev.relation_id) not in evidence_updated_at_by_relation:
evidence_updated_at_by_relation[int(ev.relation_id)] = occurred_at
if ev.entity_id is not None and int(ev.entity_id) not in evidence_by_entity:
evidence_by_entity[int(ev.entity_id)] = excerpt[:320]
if occurred_at and ev.entity_id is not None and int(ev.entity_id) not in evidence_updated_at_by_entity:
evidence_updated_at_by_entity[int(ev.entity_id)] = occurred_at
out: list[RetrievedItem] = []
for rel in relation_rows:
subject = entity_map.get(int(rel.subject_entity_id))
target = entity_map.get(int(rel.object_entity_id))
if subject is None or target is None:
continue
subject_payload = self._safe_dict(subject.payload)
target_payload = self._safe_dict(target.payload)
if project_id is not None:
subject_pid = subject_payload.get("project_id")
target_pid = target_payload.get("project_id")
if subject_pid is not None and int(subject_pid) != int(project_id):
if target_pid is not None and int(target_pid) != int(project_id):
continue
evidence_text = evidence_by_relation.get(int(rel.id))
if not evidence_text:
evidence_text = evidence_by_entity.get(int(subject.id)) or evidence_by_entity.get(int(target.id)) or ""
content = (
f"图路径: {str(subject.display_name or subject.entity_key)} --[{str(rel.relation_type)}]--> "
f"{str(target.display_name or target.entity_key)}\n"
f"证据: {evidence_text or '无显式证据摘录'}"
)
weight = self._to_float(rel.weight, 1.0)
score = min(0.99, max(0.45, 0.62 + min(weight, 1.5) * 0.18))
relation_project_id = (
subject_payload.get("project_id")
or target_payload.get("project_id")
or project_id
)
metadata = self._normalize_source_registry_metadata(
source_type="graph",
metadata={
"relation_id": int(rel.id),
"relation_type": str(rel.relation_type or ""),
"subject_entity_id": int(subject.id),
"object_entity_id": int(target.id),
"subject_name": str(subject.display_name or subject.entity_key),
"object_name": str(target.display_name or target.entity_key),
"path": f"{subject.id}->{str(rel.relation_type)}->{target.id}",
"project_id": relation_project_id,
"source_ref": getattr(rel, "source_ref", None),
},
explicit_updated_at=(
evidence_updated_at_by_relation.get(int(rel.id))
or evidence_updated_at_by_entity.get(int(subject.id))
or evidence_updated_at_by_entity.get(int(target.id))
),
)
out.append(
RetrievedItem(
source="graph",
content=content[:1200],
score=score,
source_label="Knowledge Graph",
metadata=metadata,
)
)
if not out:
for seed in seed_entities[:DEFAULT_TOP_K]:
payload = self._safe_dict(seed.payload)
if project_id is not None and payload.get("project_id") is not None:
if int(payload.get("project_id")) != int(project_id):
continue
excerpt = evidence_by_entity.get(int(seed.id)) or ""
metadata = self._normalize_source_registry_metadata(
source_type="graph",
metadata={
"entity_id": int(seed.id),
"entity_type": str(seed.entity_type or ""),
"entity_key": str(seed.entity_key or ""),
"project_id": payload.get("project_id") or project_id,
"source_ref": getattr(seed, "source_ref", None),
},
explicit_updated_at=(
evidence_updated_at_by_entity.get(int(seed.id))
or getattr(seed, "last_seen_at", None)
),
)
out.append(
RetrievedItem(
source="graph",
content=f"实体: {str(seed.display_name or seed.entity_key)}\n证据: {excerpt or '无显式证据摘录'}",
score=0.58,
source_label="Knowledge Graph",
metadata=metadata,
)
)
out.sort(
key=lambda item: (
item.score,
self._source_priority_sort_value(item.metadata),
),
reverse=True,
)
return out[: max(DEFAULT_TOP_K, 6)]
except Exception:
return []