Spaces:
Paused
Paused
Download backend/api/analyze_semantic.py from dqy08/InfoLens1: direct link, hf CLI and curl.
- Browser
- Download file 13.8 kB
-
https://huggingface.co/spaces/dqy08/InfoLens1/resolve/main/backend/api/analyze_semantic.py
- Command line
-
hf download hf://spaces/dqy08/InfoLens1/backend/api/analyze_semantic.py
-
curl -L -o analyze_semantic.py https://huggingface.co/spaces/dqy08/InfoLens1/resolve/main/backend/api/analyze_semantic.py
13.8 kB
| """Semantic analysis API | |
| 原生接口: | |
| - analyze_semantic_relevance:全文相关度(full_match_degree) | |
| - analyze_semantic_keywords:关键词归因(token_attention) | |
| 兼容:analyze_semantic(submode=count|fill_blank) | |
| """ | |
| import gc | |
| import json | |
| import queue | |
| import threading | |
| import time | |
| from typing import Literal, Optional | |
| from backend.models.model_manager import inference_lock | |
| from backend.platform.oom import exit_if_oom | |
| from backend.core.semantic_analyzer import ( | |
| analyze_relevance, | |
| analyze_relevance_batch, | |
| analyze_keywords, | |
| ) | |
| from backend.api.sse_utils import ( | |
| SSEProgressReporter, | |
| consume_progress_queue, | |
| send_result_event, | |
| send_error_event, | |
| ) | |
| from backend.platform.access_log import get_client_ip | |
| from backend.api.analyze import QueueTimeoutError, ANALYSIS_TIMEOUT, LOCK_WAIT_TIMEOUT | |
| ResponseShape = Literal["legacy", "relevance", "keywords"] | |
| SemanticKind = Literal["relevance", "keywords"] | |
| _LEGACY_SUBMODE = { | |
| "count": "relevance", | |
| "fill_blank": "keywords", | |
| } | |
| def _log_request(query, text, client_ip=None, privacy_mode=False, kind: Optional[SemanticKind] = None): | |
| from backend.platform.access_log import log_analyze_semantic_request | |
| return log_analyze_semantic_request(query, text, client_ip, privacy_mode, kind=kind) | |
| def _build_success_response( | |
| result, | |
| debug_info: bool = False, | |
| shape: ResponseShape = "legacy", | |
| ): | |
| """ | |
| shape: | |
| - legacy:两字段都带(relevance 补 attention=[];keywords 补 degree=1) | |
| - relevance:只带 full_match_degree | |
| - keywords:只带 token_attention | |
| """ | |
| if isinstance(result, list): | |
| return { | |
| "success": True, | |
| "results": [ | |
| { | |
| "model": r["model"], | |
| "full_match_degree": r["full_match_degree"], | |
| "input_token_count": r["input_token_count"], | |
| } | |
| for r in result | |
| ], | |
| } | |
| resp = { | |
| "success": True, | |
| "model": result["model"], | |
| "input_token_count": result["input_token_count"], | |
| } | |
| if shape == "relevance": | |
| resp["full_match_degree"] = result["full_match_degree"] | |
| elif shape == "keywords": | |
| resp["token_attention"] = result["token_attention"] | |
| else: | |
| # legacy:原生结果缺哪边补哪边 | |
| resp["full_match_degree"] = result["full_match_degree"] if "full_match_degree" in result else 1.0 | |
| resp["token_attention"] = result["token_attention"] if "token_attention" in result else [] | |
| if debug_info and "debug_info" in result: | |
| resp["debug_info"] = result["debug_info"] | |
| return resp | |
| def _run_core(kind: SemanticKind, query: str, text, progress_callback, debug_info: bool): | |
| if kind == "relevance": | |
| if isinstance(text, list): | |
| return analyze_relevance_batch(query, text, progress_callback=progress_callback) | |
| return analyze_relevance( | |
| query, text, progress_callback=progress_callback, debug_info=debug_info, | |
| ) | |
| if isinstance(text, list): | |
| raise ValueError("keywords does not support batch texts") | |
| return analyze_keywords( | |
| query, text, progress_callback=progress_callback, debug_info=debug_info, | |
| ) | |
| def _generate_semantic_events( | |
| query: str, | |
| text, | |
| kind: SemanticKind, | |
| debug_info: bool = False, | |
| client_ip: Optional[str] = None, | |
| request_id: Optional[int] = None, | |
| shape: ResponseShape = "legacy", | |
| ): | |
| """流式语义分析核心:生成 SSE 事件流(progress + result/error)。""" | |
| if client_ip is None: | |
| client_ip = get_client_ip() | |
| start_time = time.perf_counter() | |
| log_text = text if isinstance(text, str) else f"[批量 x{len(text)}] " + text[0] | |
| if request_id is None: | |
| request_id = _log_request(query, log_text, client_ip, kind=kind) | |
| progress_queue = queue.Queue() | |
| analysis_done = threading.Event() | |
| analysis_result = None | |
| analysis_error = None | |
| lock_wait_time = None | |
| def progress_callback(step: int, total_steps: int, stage: str, percentage: Optional[int]): | |
| progress_queue.put(("progress", step, total_steps, stage, percentage)) | |
| def run_analysis(): | |
| nonlocal analysis_result, analysis_error, lock_wait_time | |
| try: | |
| lock_wait_start = time.perf_counter() | |
| lock_acquired = inference_lock.acquire(timeout=LOCK_WAIT_TIMEOUT) | |
| if not lock_acquired: | |
| analysis_error = QueueTimeoutError( | |
| f"Queue wait exceeded {LOCK_WAIT_TIMEOUT} seconds; server busy, try again later" | |
| ) | |
| return | |
| lock_wait_time = time.perf_counter() - lock_wait_start | |
| try: | |
| from backend.platform.access_log import log_analyze_semantic_start | |
| log_analyze_semantic_start(request_id, lock_wait_time, stream_mode=True, kind=kind) | |
| analysis_result = _run_core(kind, query, text, progress_callback, debug_info) | |
| finally: | |
| inference_lock.release() | |
| except Exception as e: | |
| analysis_error = e | |
| finally: | |
| analysis_done.set() | |
| progress_queue.put(("done", None, None)) | |
| try: | |
| analysis_thread = threading.Thread(target=run_analysis, daemon=True) | |
| analysis_thread.start() | |
| timeout_reached = False | |
| for kind_ev, event_str in consume_progress_queue( | |
| progress_queue, analysis_done, start_time, ANALYSIS_TIMEOUT, "语义分析" | |
| ): | |
| if kind_ev == 'timeout': | |
| timeout_reached = True | |
| yield event_str | |
| break | |
| if kind_ev == 'progress': | |
| yield event_str | |
| elif kind_ev == 'done': | |
| break | |
| if timeout_reached: | |
| gc.collect() | |
| return | |
| if analysis_error: | |
| if isinstance(analysis_error, QueueTimeoutError): | |
| print(f"⏱️ 排队超时: {analysis_error}") | |
| yield send_error_event(str(analysis_error), 503) | |
| gc.collect() | |
| return | |
| raise analysis_error | |
| if analysis_result is None: | |
| print("⚠️ 语义分析结果为空,但没有错误信息") | |
| yield send_error_event("Analysis failed: no result", 500) | |
| gc.collect() | |
| return | |
| elapsed = time.perf_counter() - start_time | |
| from backend.platform.inference_response_log import log_analyze_semantic_response | |
| from backend.platform.model_routing import is_worker | |
| if isinstance(analysis_result, dict): | |
| log_result = analysis_result | |
| else: | |
| total_tokens = sum(r.get("input_token_count", 0) for r in analysis_result) | |
| log_result = {"input_token_count": total_tokens} | |
| log_analyze_semantic_response( | |
| request_id=request_id, | |
| result=log_result, | |
| elapsed=elapsed, | |
| wait_time=lock_wait_time, | |
| for_worker=is_worker(), | |
| kind=kind, | |
| ) | |
| yield send_result_event(_build_success_response(analysis_result, debug_info, shape=shape)) | |
| except Exception as e: | |
| import traceback | |
| traceback.print_exc() | |
| exit_if_oom(e, defer_seconds=1) | |
| yield send_error_event(str(e), 500) | |
| finally: | |
| gc.collect() | |
| def _analyze_semantic_with_stream( | |
| query: str, text, kind: SemanticKind, debug_info: bool = False, | |
| client_ip: Optional[str] = None, request_id: Optional[int] = None, | |
| shape: ResponseShape = "legacy", | |
| ): | |
| return SSEProgressReporter( | |
| lambda: _generate_semantic_events( | |
| query, text, kind, debug_info, client_ip, request_id, shape=shape, | |
| ) | |
| ).create_response() | |
| def _analyze_semantic_plain( | |
| query: str, text, kind: SemanticKind, debug_info: bool = False, | |
| client_ip: Optional[str] = None, request_id: Optional[int] = None, | |
| shape: ResponseShape = "legacy", | |
| ): | |
| result = None | |
| error_msg = None | |
| status_code = 500 | |
| try: | |
| for event_str in _generate_semantic_events( | |
| query, text, kind, debug_info, client_ip, request_id, shape=shape, | |
| ): | |
| if not event_str.startswith('data: '): | |
| continue | |
| data = json.loads(event_str[6:].strip()) | |
| t = data.get('type') | |
| if t == 'result': | |
| result = data.get('data') | |
| elif t == 'error': | |
| error_msg = data.get('message', 'Analysis failed') | |
| status_code = data.get('status_code', 500) | |
| break | |
| except Exception as e: | |
| import traceback | |
| traceback.print_exc() | |
| exit_if_oom(e, defer_seconds=1) | |
| error_msg = str(e) | |
| finally: | |
| gc.collect() | |
| if error_msg: | |
| return {"success": False, "message": error_msg}, status_code | |
| if result is None: | |
| return {"success": False, "message": "Analysis failed: no result"}, 500 | |
| return result, 200 | |
| def _ingress_semantic( | |
| semantic_request: dict, | |
| *, | |
| api_path: str, | |
| kind: SemanticKind, | |
| content, | |
| log_text: str, | |
| shape: ResponseShape, | |
| ): | |
| query = (semantic_request.get("query") or "") | |
| stream = semantic_request.get("stream", False) | |
| debug_info = bool(semantic_request.get("debug_info", False)) | |
| privacy_mode = bool(semantic_request.get("privacy_mode", False)) | |
| client_ip = get_client_ip() | |
| logged: dict = {"request_id": None} | |
| def log_fn(): | |
| logged["request_id"] = _log_request(query, log_text, client_ip, privacy_mode, kind=kind) | |
| def local_fn(): | |
| rid = logged["request_id"] | |
| if stream: | |
| return _analyze_semantic_with_stream( | |
| query, content, kind, debug_info, client_ip, rid, shape=shape, | |
| ) | |
| return _analyze_semantic_plain( | |
| query, content, kind, debug_info, client_ip, rid, shape=shape, | |
| ) | |
| from backend.models.model_manager import ModelSlot | |
| from backend.platform.inference_ingress import ingress_inference | |
| from backend.platform.inference_response_log import make_analyze_semantic_response_logger | |
| from backend.platform.model_routing import is_local | |
| return ingress_inference( | |
| slot=ModelSlot.INSTRUCT, | |
| api_path=api_path, | |
| json_body=semantic_request, | |
| stream=bool(stream), | |
| timeout=60.0, | |
| log_fn=log_fn, | |
| local_fn=local_fn, | |
| response_log_fn=( | |
| None | |
| if is_local(ModelSlot.INSTRUCT) | |
| else make_analyze_semantic_response_logger(logged, kind=kind) | |
| ), | |
| ) | |
| def _parse_text_or_texts(semantic_request, *, allow_texts: bool): | |
| """返回 (content, log_text) 或 (None, error_response)。""" | |
| query = (semantic_request.get("query") or "") | |
| if not query: | |
| return None, ({"success": False, "message": "Missing query"}, 400) | |
| texts = semantic_request.get("texts") | |
| text = semantic_request.get("text") or "" | |
| if texts is not None: | |
| if not allow_texts: | |
| return None, ({"success": False, "message": "texts is not supported on this endpoint"}, 400) | |
| if not isinstance(texts, list) or not texts or not all(isinstance(t, str) and t for t in texts): | |
| return None, ({"success": False, "message": "texts must be a non-empty string array"}, 400) | |
| return texts, f"[批量 x{len(texts)}] " + texts[0] | |
| if not text: | |
| return None, ({"success": False, "message": "Missing text"}, 400) | |
| return text, text | |
| def analyze_semantic_relevance(semantic_request): | |
| """全文相关度:只返回 full_match_degree(可批量 texts)。""" | |
| content, log_or_err = _parse_text_or_texts(semantic_request, allow_texts=True) | |
| if content is None: | |
| return log_or_err | |
| return _ingress_semantic( | |
| semantic_request, | |
| api_path="/api/analyze-semantic-relevance", | |
| kind="relevance", | |
| content=content, | |
| log_text=log_or_err, | |
| shape="relevance", | |
| ) | |
| def analyze_semantic_keywords(semantic_request): | |
| """关键词归因:只返回 token_attention(单条 text)。""" | |
| content, log_or_err = _parse_text_or_texts(semantic_request, allow_texts=False) | |
| if content is None: | |
| return log_or_err | |
| return _ingress_semantic( | |
| semantic_request, | |
| api_path="/api/analyze-semantic-keywords", | |
| kind="keywords", | |
| content=content, | |
| log_text=log_or_err, | |
| shape="keywords", | |
| ) | |
| def analyze_semantic(semantic_request): | |
| """ | |
| 兼容旧接口:submode=count|fill_blank(映射到 relevance|keywords)。 | |
| texts 批量仅支持 count(未传 submode 时按 count)。 | |
| """ | |
| texts = semantic_request.get("texts") | |
| submode = (semantic_request.get("submode") or "").strip() or None | |
| if submode == "match_score": | |
| return {"success": False, "message": "submode match_score has been removed"}, 400 | |
| if texts is not None: | |
| if submode and submode != "count": | |
| return {"success": False, "message": "texts batch mode only supports submode=count"}, 400 | |
| kind: SemanticKind = "relevance" | |
| else: | |
| mapped = _LEGACY_SUBMODE.get(submode or "fill_blank") | |
| if mapped is None: | |
| return {"success": False, "message": f"Unknown submode: {submode}"}, 400 | |
| kind = mapped # type: ignore[assignment] | |
| content, log_or_err = _parse_text_or_texts(semantic_request, allow_texts=True) | |
| if content is None: | |
| return log_or_err | |
| return _ingress_semantic( | |
| semantic_request, | |
| api_path="/api/analyze-semantic", | |
| kind=kind, | |
| content=content, | |
| log_text=log_or_err, | |
| shape="legacy", | |
| ) | |