Add Cloud Tour Libo knowledge graph platform
This commit is contained in:
560
app/api/plaza.py
560
app/api/plaza.py
@@ -9,17 +9,123 @@ import time
|
||||
from typing import Any
|
||||
|
||||
import h3
|
||||
from falkordb import FalkorDB
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from app.auth import CurrentUser
|
||||
from app.config import settings
|
||||
from app.db import get_agent_settings, get_plaza_overview, get_plaza_alerts, get_conn
|
||||
from app.graph_qa_engine import answer_graph_question
|
||||
from app.llm_client import LlmClient
|
||||
from app.project_context import ProjectContext, get_project_context
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
SPATIAL_GRAPH_NAME = "guiyang_spatial_v1"
|
||||
LIBO_BUS_DATASET = "libo_bus_xlsx_v1"
|
||||
|
||||
FOOD_ENRICHMENT_KEYS = (
|
||||
"food_fusion_status",
|
||||
"food_match_confidence",
|
||||
"food_match_rule",
|
||||
"food_match_score",
|
||||
"food_name_similarity",
|
||||
"food_address_similarity",
|
||||
"food_coordinate_distance_m",
|
||||
"dianping_shop_id",
|
||||
"dianping_name",
|
||||
"dianping_address",
|
||||
"dianping_url",
|
||||
"dianping_rating",
|
||||
"dianping_review_count",
|
||||
"dianping_avg_price",
|
||||
"dianping_area",
|
||||
"dianping_category",
|
||||
"dianping_score_details",
|
||||
"dianping_ranking",
|
||||
"dianping_business_status",
|
||||
"dianping_business_hours",
|
||||
"dianping_tags",
|
||||
"dianping_transportation",
|
||||
"dianping_shop_image",
|
||||
"dianping_review_tags",
|
||||
"recommended_dish_count",
|
||||
"recommended_dish_names",
|
||||
"group_buy_count",
|
||||
"group_buy_min_price",
|
||||
"group_buy_max_price",
|
||||
"group_buy_titles",
|
||||
"group_buy_items_json",
|
||||
"review_sample_count",
|
||||
"review_sample_avg_stars",
|
||||
"review_sample_summary",
|
||||
"food_review_items_json",
|
||||
"food_review_tags_json",
|
||||
"menu_image_count",
|
||||
"candidate_dianping_name",
|
||||
"food_candidate_name_similarity",
|
||||
"food_candidate_address_similarity",
|
||||
"enrichment_updated_at",
|
||||
)
|
||||
|
||||
HOTEL_ENRICHMENT_KEYS = (
|
||||
"hotel_fusion_status",
|
||||
"hotel_match_confidence",
|
||||
"hotel_match_rule",
|
||||
"hotel_name_similarity",
|
||||
"hotel_address_similarity",
|
||||
"hotel_phone_exact_match",
|
||||
"ctrip_hotel_id",
|
||||
"ctrip_name_cn",
|
||||
"ctrip_name_en",
|
||||
"ctrip_address",
|
||||
"ctrip_phone",
|
||||
"ctrip_star_level",
|
||||
"ctrip_diamond_level",
|
||||
"ctrip_opened_year",
|
||||
"ctrip_room_count",
|
||||
"ctrip_hotel_type",
|
||||
"ctrip_ranking",
|
||||
"ctrip_rating",
|
||||
"ctrip_rating_description",
|
||||
"ctrip_review_count",
|
||||
"ctrip_transportation",
|
||||
"ctrip_tags",
|
||||
"ctrip_popular_facilities",
|
||||
"ctrip_score_details",
|
||||
"ctrip_cleanliness_score",
|
||||
"ctrip_facilities_score",
|
||||
"ctrip_environment_score",
|
||||
"ctrip_service_score",
|
||||
"ctrip_description",
|
||||
"ctrip_url",
|
||||
"room_type_count",
|
||||
"room_type_names",
|
||||
"room_min_price",
|
||||
"room_max_price",
|
||||
"offer_count",
|
||||
"offer_min_price",
|
||||
"offer_max_price",
|
||||
"room_items_json",
|
||||
"facility_service_count",
|
||||
"facility_summary",
|
||||
"facility_category_items_json",
|
||||
"policy_count",
|
||||
"policy_summary",
|
||||
"guest_review_sample_count",
|
||||
"guest_review_sample_avg_score",
|
||||
"latest_review_at",
|
||||
"review_travel_types",
|
||||
"review_items_json",
|
||||
"hotel_image_samples",
|
||||
"nearby_place_count",
|
||||
"nearby_place_summary",
|
||||
"nearby_category_items_json",
|
||||
"candidate_ctrip_name",
|
||||
"hotel_candidate_name_similarity",
|
||||
"hotel_candidate_address_similarity",
|
||||
"enrichment_updated_at",
|
||||
)
|
||||
|
||||
CATEGORY_ALIASES: dict[str, list[str]] = {
|
||||
"美食": ["美食", "吃", "餐厅", "饭店", "火锅", "小吃", "烧烤", "咖啡", "奶茶", "酸汤鱼"],
|
||||
@@ -313,13 +419,463 @@ async def amap_config(_user: CurrentUser = None):
|
||||
}
|
||||
|
||||
|
||||
def _is_enterprise_travel_graph(graph_name: str) -> bool:
|
||||
lower = graph_name.lower()
|
||||
return "baixinghui" in lower or ("travel" in lower and graph_name != SPATIAL_GRAPH_NAME)
|
||||
|
||||
|
||||
def _resolve_spatial_graph_name(graph_name: str) -> str:
|
||||
"""Map the city project graph name to the materialized spatial POI graph."""
|
||||
lower = graph_name.lower()
|
||||
if graph_name == SPATIAL_GRAPH_NAME or lower.endswith("_spatial_v1"):
|
||||
return graph_name
|
||||
if graph_name == "guiyang_new2":
|
||||
return SPATIAL_GRAPH_NAME
|
||||
return graph_name
|
||||
|
||||
|
||||
def _iso_datetime(value: Any) -> str:
|
||||
return value.isoformat() if value else ""
|
||||
|
||||
|
||||
def _build_libo_bus_route_payload(
|
||||
rows: list[list[Any]],
|
||||
graph_name: str,
|
||||
) -> dict[str, Any]:
|
||||
routes: dict[str, dict[str, Any]] = {}
|
||||
stop_ids: set[str] = set()
|
||||
for row in rows:
|
||||
(
|
||||
route_id,
|
||||
line_name,
|
||||
direction,
|
||||
start_stop,
|
||||
end_stop,
|
||||
stop_count,
|
||||
fare_yuan,
|
||||
first_bus,
|
||||
last_bus,
|
||||
distance_km,
|
||||
path_source,
|
||||
stop_id,
|
||||
stop_name,
|
||||
lng,
|
||||
lat,
|
||||
sequence,
|
||||
) = row
|
||||
route_key = str(route_id or "")
|
||||
if not route_key:
|
||||
continue
|
||||
route = routes.setdefault(
|
||||
route_key,
|
||||
{
|
||||
"route_id": route_key,
|
||||
"line_name": str(line_name or ""),
|
||||
"direction": str(direction or ""),
|
||||
"start_stop": str(start_stop or ""),
|
||||
"end_stop": str(end_stop or ""),
|
||||
"stop_count": int(stop_count or 0),
|
||||
"fare_yuan": float(fare_yuan or 0),
|
||||
"first_bus": str(first_bus or ""),
|
||||
"last_bus": str(last_bus or ""),
|
||||
"distance_km": float(distance_km or 0),
|
||||
"path_source": str(path_source or "station_sequence"),
|
||||
"stops": [],
|
||||
},
|
||||
)
|
||||
if stop_id and lng is not None and lat is not None:
|
||||
stop_key = str(stop_id)
|
||||
stop_ids.add(stop_key)
|
||||
route["stops"].append(
|
||||
{
|
||||
"stop_id": stop_key,
|
||||
"name": str(stop_name or "未命名公交站"),
|
||||
"lng": float(lng),
|
||||
"lat": float(lat),
|
||||
"sequence": int(sequence or 0),
|
||||
}
|
||||
)
|
||||
|
||||
items = sorted(
|
||||
routes.values(),
|
||||
key=lambda route: (
|
||||
route["line_name"],
|
||||
route["route_id"],
|
||||
),
|
||||
)
|
||||
return {
|
||||
"graph_name": graph_name,
|
||||
"dataset": LIBO_BUS_DATASET,
|
||||
"line_count": len({route["line_name"] for route in items}),
|
||||
"direction_count": len(items),
|
||||
"stop_count": len(stop_ids),
|
||||
"route_stop_count": sum(len(route["stops"]) for route in items),
|
||||
"items": items,
|
||||
}
|
||||
|
||||
|
||||
def _read_libo_bus_routes(graph_name: str) -> dict[str, Any]:
|
||||
graph = FalkorDB(
|
||||
host=settings.falkordb_host,
|
||||
port=settings.falkordb_port,
|
||||
).select_graph(graph_name)
|
||||
rows = graph.query(
|
||||
"MATCH (b:BusLine) WHERE b.dataset=$dataset "
|
||||
"OPTIONAL MATCH (b)-[r:STOPS_AT]->(s:Place) "
|
||||
"RETURN b.route_id,b.line_name,b.direction,b.start_stop,b.end_stop,"
|
||||
"b.stop_count,b.fare_yuan,b.first_bus,b.last_bus,b.distance_km,"
|
||||
"b.path_source,s.element_id,s.name,s.lng,s.lat,r.sequence "
|
||||
"ORDER BY b.line_name,b.route_id,r.sequence",
|
||||
{"dataset": LIBO_BUS_DATASET},
|
||||
).result_set
|
||||
return _build_libo_bus_route_payload(rows, graph_name)
|
||||
|
||||
|
||||
_INTERNAL_ENRICHMENT_KEYS = {
|
||||
"food_fusion_status",
|
||||
"food_match_confidence",
|
||||
"food_match_rule",
|
||||
"food_match_score",
|
||||
"food_name_similarity",
|
||||
"food_address_similarity",
|
||||
"food_coordinate_distance_m",
|
||||
"candidate_dianping_name",
|
||||
"food_candidate_name_similarity",
|
||||
"food_candidate_address_similarity",
|
||||
"hotel_fusion_status",
|
||||
"hotel_match_confidence",
|
||||
"hotel_match_rule",
|
||||
"hotel_name_similarity",
|
||||
"hotel_address_similarity",
|
||||
"hotel_phone_exact_match",
|
||||
"candidate_ctrip_name",
|
||||
"hotel_candidate_name_similarity",
|
||||
"hotel_candidate_address_similarity",
|
||||
"ctrip_description",
|
||||
}
|
||||
|
||||
_ZERO_MEANS_MISSING_ENRICHMENT_KEYS = {
|
||||
"dianping_rating",
|
||||
"dianping_review_count",
|
||||
"dianping_avg_price",
|
||||
"group_buy_count",
|
||||
"review_sample_count",
|
||||
"review_sample_avg_stars",
|
||||
"ctrip_rating",
|
||||
"ctrip_review_count",
|
||||
"ctrip_room_count",
|
||||
"room_min_price",
|
||||
"room_max_price",
|
||||
"offer_count",
|
||||
"offer_min_price",
|
||||
"offer_max_price",
|
||||
"guest_review_sample_count",
|
||||
"guest_review_sample_avg_score",
|
||||
}
|
||||
|
||||
|
||||
def _clean_enrichment_display_value(key: str, value: Any) -> Any:
|
||||
"""Convert scraped platform values into stable, user-facing display values."""
|
||||
if key in _INTERNAL_ENRICHMENT_KEYS:
|
||||
return None
|
||||
if key in _ZERO_MEANS_MISSING_ENRICHMENT_KEYS:
|
||||
try:
|
||||
if float(value) == 0:
|
||||
return None
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if key == "ctrip_diamond_level":
|
||||
text = str(value or "").strip()
|
||||
if re.fullmatch(r"\d+(?:\.\d+)?钻", text):
|
||||
return text
|
||||
matched = re.fullmatch(
|
||||
r"(\d+(?:\.\d+)?)\s+out\s+of\s+5\s+(?:rating|diamonds?)",
|
||||
text,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
return f"{matched.group(1)}钻" if matched else None
|
||||
return value
|
||||
|
||||
|
||||
def _graph_poi_enrichment(
|
||||
graph_name: str,
|
||||
element_id: str,
|
||||
) -> tuple[list[str], dict[str, Any], dict[str, Any]]:
|
||||
graph = FalkorDB(
|
||||
host=settings.falkordb_host,
|
||||
port=settings.falkordb_port,
|
||||
).select_graph(graph_name)
|
||||
result = graph.query(
|
||||
"MATCH (n {element_id:$element_id}) "
|
||||
"RETURN labels(n), properties(n) LIMIT 1",
|
||||
{"element_id": element_id},
|
||||
).result_set
|
||||
if not result:
|
||||
return [], {}, {}
|
||||
|
||||
labels = [str(label) for label in (result[0][0] or [])]
|
||||
properties = result[0][1] if isinstance(result[0][1], dict) else {}
|
||||
|
||||
def pick(keys: tuple[str, ...]) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {}
|
||||
for key in keys:
|
||||
if key not in properties:
|
||||
continue
|
||||
value = _clean_enrichment_display_value(key, properties[key])
|
||||
if value in (None, "", "[]", "{}"):
|
||||
continue
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
return labels, pick(FOOD_ENRICHMENT_KEYS), pick(HOTEL_ENRICHMENT_KEYS)
|
||||
|
||||
|
||||
def _graph_poi_categories(graph_name: str) -> dict[str, list[str]]:
|
||||
graph = FalkorDB(
|
||||
host=settings.falkordb_host,
|
||||
port=settings.falkordb_port,
|
||||
).select_graph(graph_name)
|
||||
rows = graph.query(
|
||||
"MATCH (n) "
|
||||
"WHERE n.element_id IS NOT NULL "
|
||||
"AND any(label IN labels(n) WHERE label IN "
|
||||
"['FoodPlace','Hotel','ScenicSpot','TransitFacility']) "
|
||||
"RETURN n.element_id, labels(n)"
|
||||
).result_set
|
||||
label_names = {
|
||||
"FoodPlace": "美食",
|
||||
"Hotel": "酒店",
|
||||
"ScenicSpot": "景点",
|
||||
"TransitFacility": "交通设施",
|
||||
}
|
||||
result: dict[str, list[str]] = {}
|
||||
for element_id, labels in rows:
|
||||
categories = [
|
||||
label_names[label]
|
||||
for label in (labels or [])
|
||||
if label in label_names
|
||||
]
|
||||
if element_id and categories:
|
||||
result[str(element_id)] = categories
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/plaza/map-pois")
|
||||
async def map_pois(
|
||||
context: ProjectContext = Depends(get_project_context),
|
||||
_user: CurrentUser = None,
|
||||
):
|
||||
"""Return lightweight, project-scoped POI points for the knowledge map."""
|
||||
graph_name = _resolve_spatial_graph_name(context.graph_name)
|
||||
s = settings.db_schema
|
||||
async with get_conn() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute(
|
||||
f"""SELECT element_id, gaode_poi_id, name, type_label, place_type,
|
||||
lng, lat, address, district, city
|
||||
FROM {s}.amap_spatial_pois
|
||||
WHERE graph_name=%s
|
||||
ORDER BY type_label, name
|
||||
LIMIT 15000""",
|
||||
(graph_name,),
|
||||
)
|
||||
rows = await cur.fetchall()
|
||||
graph_categories = await asyncio.to_thread(
|
||||
_graph_poi_categories,
|
||||
graph_name,
|
||||
)
|
||||
|
||||
items = [
|
||||
{
|
||||
"id": row["element_id"],
|
||||
"gaode_poi_id": row["gaode_poi_id"],
|
||||
"name": row["name"] or "未命名POI",
|
||||
"category": row["type_label"] or "其他地点",
|
||||
"categories": graph_categories.get(
|
||||
str(row["element_id"]),
|
||||
[row["type_label"] or "其他地点"],
|
||||
),
|
||||
"place_type": row["place_type"] or "poi",
|
||||
"lng": float(row["lng"]),
|
||||
"lat": float(row["lat"]),
|
||||
"address": row["address"] or "",
|
||||
"district": row["district"] or "",
|
||||
"city": row["city"] or "",
|
||||
}
|
||||
for row in rows
|
||||
if row.get("lng") is not None and row.get("lat") is not None
|
||||
]
|
||||
category_counts: dict[str, int] = {}
|
||||
for item in items:
|
||||
for category in item["categories"]:
|
||||
category_counts[category] = category_counts.get(category, 0) + 1
|
||||
return {
|
||||
"graph_name": graph_name,
|
||||
"total": len(items),
|
||||
"categories": [
|
||||
{"category": category, "count": count}
|
||||
for category, count in sorted(
|
||||
category_counts.items(),
|
||||
key=lambda pair: (-pair[1], pair[0]),
|
||||
)
|
||||
],
|
||||
"items": items,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/plaza/bus-routes")
|
||||
async def bus_routes(
|
||||
context: ProjectContext = Depends(get_project_context),
|
||||
_user: CurrentUser = None,
|
||||
):
|
||||
"""Return the active project's graph-backed bus routes and ordered stops."""
|
||||
graph_name = _resolve_spatial_graph_name(context.graph_name)
|
||||
return await asyncio.to_thread(_read_libo_bus_routes, graph_name)
|
||||
|
||||
|
||||
@router.get("/plaza/map-pois/{place_id}")
|
||||
async def map_poi_detail(
|
||||
place_id: str,
|
||||
context: ProjectContext = Depends(get_project_context),
|
||||
_user: CurrentUser = None,
|
||||
):
|
||||
"""Return useful detail fields for one POI in the active project."""
|
||||
graph_name = _resolve_spatial_graph_name(context.graph_name)
|
||||
s = settings.db_schema
|
||||
async with get_conn() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute(
|
||||
f"""SELECT element_id, gaode_poi_id, name, type_label, place_type,
|
||||
amap_type, typecode, lng, lat, province, city, district,
|
||||
adcode, business_area, address, tel, open_time, rating,
|
||||
cost, level, tags, photo_urls, source, source_cell_id,
|
||||
source_resolution, source_scope_adcode,
|
||||
raw_jsonb,
|
||||
first_fetched_at, last_fetched_at
|
||||
FROM {s}.amap_spatial_pois
|
||||
WHERE graph_name=%s
|
||||
AND (element_id=%s OR gaode_poi_id=%s)
|
||||
LIMIT 1""",
|
||||
(graph_name, place_id, place_id.removeprefix("amap:")),
|
||||
)
|
||||
row = await cur.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="当前项目中未找到该POI")
|
||||
|
||||
raw = row.get("raw_jsonb") or {}
|
||||
if not isinstance(raw, dict):
|
||||
raw = {}
|
||||
graph_labels, food_enrichment, hotel_enrichment = await asyncio.to_thread(
|
||||
_graph_poi_enrichment,
|
||||
graph_name,
|
||||
row["element_id"],
|
||||
)
|
||||
external_photos = _photo_urls(food_enrichment.get("dianping_shop_image"))
|
||||
external_photos.extend(_photo_urls(hotel_enrichment.get("hotel_image_samples")))
|
||||
photo_urls = list(dict.fromkeys([
|
||||
*_photo_urls(row["photo_urls"]),
|
||||
*external_photos,
|
||||
]))
|
||||
|
||||
return {
|
||||
"graph_name": graph_name,
|
||||
"id": row["element_id"],
|
||||
"gaode_poi_id": row["gaode_poi_id"],
|
||||
"name": row["name"] or "未命名POI",
|
||||
"category": row["type_label"] or "其他地点",
|
||||
"place_type": row["place_type"] or "poi",
|
||||
"business_subcategory": raw.get("business_subcategory") or "",
|
||||
"scenic_type": raw.get("scenic_type") or "",
|
||||
"scenic_level": raw.get("scenic_level") or "",
|
||||
"parent_scenic": raw.get("parent_scenic") or "",
|
||||
"scenic_grade": raw.get("scenic_grade") or "",
|
||||
"visitor_value": raw.get("visitor_value") or "",
|
||||
"audit_result": raw.get("audit_result") or "",
|
||||
"audit_confidence": raw.get("audit_confidence") or "",
|
||||
"audit_basis": raw.get("audit_basis") or "",
|
||||
"audit_date": raw.get("audit_date") or "",
|
||||
"amap_type": row["amap_type"] or "",
|
||||
"typecode": row["typecode"] or "",
|
||||
"scan_hit_count": raw.get("scan_hit_count") or 0,
|
||||
"matched_scan_types": raw.get("matched_scan_types") or [],
|
||||
"lng": float(row["lng"]),
|
||||
"lat": float(row["lat"]),
|
||||
"province": row["province"] or "",
|
||||
"city": row["city"] or "",
|
||||
"district": row["district"] or "",
|
||||
"adcode": row["adcode"] or "",
|
||||
"business_area": row["business_area"] or "",
|
||||
"address": row["address"] or "",
|
||||
"tel": row["tel"] or "",
|
||||
"open_time": row["open_time"] or "",
|
||||
"rating": row["rating"] or "",
|
||||
"cost": row["cost"] or "",
|
||||
"level": row["level"] or "",
|
||||
"tags": row["tags"] or "",
|
||||
"photo_urls": photo_urls,
|
||||
"source": row["source"] or "",
|
||||
"source_cell_id": row["source_cell_id"] or "",
|
||||
"source_resolution": row["source_resolution"],
|
||||
"source_scope_adcode": row["source_scope_adcode"] or "",
|
||||
"first_fetched_at": _iso_datetime(row["first_fetched_at"]),
|
||||
"last_fetched_at": _iso_datetime(row["last_fetched_at"]),
|
||||
"graph_labels": graph_labels,
|
||||
"food_enrichment": food_enrichment,
|
||||
"hotel_enrichment": hotel_enrichment,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/plaza/user-query")
|
||||
async def user_query(body: dict, _user: CurrentUser = None):
|
||||
async def user_query(
|
||||
body: dict,
|
||||
context: ProjectContext = Depends(get_project_context),
|
||||
_user: CurrentUser = None,
|
||||
):
|
||||
"""User-facing KG nearby query: NL intent -> H3 recall -> distance/rank -> LLM answer."""
|
||||
started_at = time.perf_counter()
|
||||
question = str(body.get("question") or "").strip()
|
||||
if not question:
|
||||
raise HTTPException(400, "question required")
|
||||
graph_name = str(body.get("graph_name") or context.graph_name or SPATIAL_GRAPH_NAME)
|
||||
if _is_enterprise_travel_graph(graph_name):
|
||||
try:
|
||||
graph_response = await answer_graph_question(
|
||||
question,
|
||||
graph_name,
|
||||
customer_context=body.get("customer_context") if isinstance(body.get("customer_context"), dict) else {},
|
||||
limit=int(body.get("limit") or 80),
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise HTTPException(status_code=502, detail=f"LLM 图查询问答失败: {str(exc)[:220]}") from exc
|
||||
trace = graph_response.get("trace") or {}
|
||||
trace.update({
|
||||
"latency_ms": max(1, round((time.perf_counter() - started_at) * 1000)),
|
||||
"rule_query_used": False,
|
||||
"plaza_user_mode": "enterprise_graph_qa",
|
||||
})
|
||||
return {
|
||||
"mode": "travel_customer_service",
|
||||
"question": question,
|
||||
"graph_name": graph_name,
|
||||
"user_location": {
|
||||
"lng": float(body.get("lng", 106.7135) or 106.7135),
|
||||
"lat": float(body.get("lat", 26.5744) or 26.5744),
|
||||
},
|
||||
"intent": {
|
||||
"radius_m": int(body.get("radius_m") or 1000),
|
||||
"category": "百姓惠客服",
|
||||
"keywords": [],
|
||||
"user_need": question,
|
||||
},
|
||||
"answer": graph_response.get("copy_text") or graph_response.get("answer") or "",
|
||||
"results": [],
|
||||
"plans": graph_response.get("plans") or [],
|
||||
"evidence": graph_response.get("evidence") or [],
|
||||
"follow_up_questions": graph_response.get("follow_up_questions") or [],
|
||||
"risk_notes": graph_response.get("risk_notes") or [],
|
||||
"trace": trace,
|
||||
}
|
||||
graph_name = _resolve_spatial_graph_name(graph_name)
|
||||
try:
|
||||
lng = float(body.get("lng", 106.7135))
|
||||
lat = float(body.get("lat", 26.5744))
|
||||
@@ -327,8 +883,6 @@ async def user_query(body: dict, _user: CurrentUser = None):
|
||||
raise HTTPException(400, "lng/lat required") from exc
|
||||
radius_input = body.get("radius_m")
|
||||
radius_m = int(radius_input) if radius_input not in (None, "") else None
|
||||
graph_name = str(body.get("graph_name") or SPATIAL_GRAPH_NAME)
|
||||
|
||||
use_llm = body.get("use_llm") is True
|
||||
fallback = _rule_intent(question, radius_m)
|
||||
if use_llm:
|
||||
|
||||
Reference in New Issue
Block a user