256 lines
12 KiB
Python
256 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:
|
|
terms = self._extract_query_terms(question, preferred=self._extract_project_lookup_keyword(question))
|
|
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 []
|