Files
group_fqcd_jr/app/service/relationship_service.py
T
张胜宇 e239eb778b docs: 品牌全量口径统一为「南方基金」+ 作废文档清理
1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富
   统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」;
   同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。
2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本),
   新增《文档规整方案与开发前待决事项-2026-09-17》。
3) 客服agent 四份交付文档首次纳入本分支。
2026-09-17 15:15:22 +08:00

69 lines
2.9 KiB
Python

from collections.abc import Sequence
from typing import Any, Protocol
class GraphDriver(Protocol):
async def execute_query(self, query: str, **parameters: Any) -> Sequence[Any]: ...
class GraphDegradedResult:
def __init__(self, reason: str) -> None:
self.degraded = True
self.reason = reason
class RelationshipService:
"""Controlled Neo4j boundary; callers cannot submit arbitrary labels or Cypher."""
ALLOWED_RELATIONSHIPS = frozenset({
"PREFERS", "HAS_GOAL", "INTERESTED_IN", "TRADED", "HOLDS",
"TRIGGERED_RISK", "BELONGS_TO_CATEGORY", "EXPOSED_TO_INDUSTRY",
})
def __init__(self, driver: GraphDriver) -> None:
self.driver = driver
async def neighbors(
self, customer_id: int, relationship: str, *, limit: int = 50
) -> Sequence[Any]:
if relationship not in self.ALLOWED_RELATIONSHIPS:
raise ValueError("relationship is not allowed")
safe_limit = max(1, min(limit, 100))
query = (
"MATCH (c:Customer {customer_id: $customer_id})"
f"-[r:{relationship}]->(n) RETURN n, r LIMIT $limit"
)
try:
return await self.driver.execute_query(query, customer_id=customer_id, limit=safe_limit)
except Exception as exc:
return [GraphDegradedResult(f"neo4j_unavailable:{type(exc).__name__}")]
async def paths(self, customer_id: int, *, max_hops: int = 2, limit: int = 50) -> Sequence[Any]:
hops = max(1, min(max_hops, 2))
safe_limit = max(1, min(limit, 100))
query = (
"MATCH p=(c:Customer {customer_id: $customer_id})-[*1.."
f"{hops}]->(n) RETURN p LIMIT $limit"
)
try:
return await self.driver.execute_query(query, customer_id=customer_id, limit=safe_limit)
except Exception as exc:
return [GraphDegradedResult(f"neo4j_unavailable:{type(exc).__name__}")]
async def portfolio_industry_context(self, customer_id: int) -> dict[str, object]:
"""Return only relationship enrichment; no position value comes from Neo4j."""
query = (
"MATCH (c:Customer {customer_id: $customer_id})-[:HOLDS]->"
"(h:Holding)-[:EXPOSED_TO_INDUSTRY]->(i:Industry) "
"RETURN i.industry_name AS industry_name, count(DISTINCT h) AS product_count "
"ORDER BY product_count DESC LIMIT $limit"
)
try:
rows = await self.driver.execute_query(query, customer_id=customer_id, limit=20)
except Exception as exc:
return {"degraded": True, "reason": f"neo4j_unavailable:{type(exc).__name__}"}
if any(isinstance(row, GraphDegradedResult) for row in rows):
degraded = next(row for row in rows if isinstance(row, GraphDegradedResult))
return {"degraded": True, "reason": degraded.reason}
return {"degraded": False, "overlaps": list(rows)}