import difflib import logging import re import uuid from datetime import datetime, timedelta from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.models.citation_record import CitationRecord from app.models.query import Query from app.models.query_task import QueryTask from app.services.ai_engine.platform_bridge import query_platform_raw from app.workers.citation_extractor import analyze_citations logger = logging.getLogger(__name__) class BrandMatcher: def __init__(self, target_brand: str, brand_aliases: list[str] | None = None): self.target_brand = target_brand self.brand_aliases = brand_aliases or [] def match(self, text: str) -> dict: if not text: return { "cited": False, "confidence": 0.0, "match_type": None, "position": None, "citation_text": None, } if self.target_brand in text: position, citation_text = self._extract_position_and_context(text, self.target_brand) return { "cited": True, "confidence": 1.0, "match_type": "exact", "position": position, "citation_text": citation_text, } for alias in self.brand_aliases: if alias in text: position, citation_text = self._extract_position_and_context(text, alias) return { "cited": True, "confidence": 0.9, "match_type": "alias", "position": position, "citation_text": citation_text, } best_ratio = 0.0 best_match = None for word in self._extract_candidates(text): ratio = difflib.SequenceMatcher(None, self.target_brand, word).ratio() if ratio > best_ratio: best_ratio = ratio best_match = word for alias in self.brand_aliases: for word in self._extract_candidates(text): ratio = difflib.SequenceMatcher(None, alias, word).ratio() if ratio > best_ratio: best_ratio = ratio best_match = word if best_ratio > 0.4 and best_match: position, citation_text = self._extract_position_and_context(text, best_match) return { "cited": True, "confidence": round(best_ratio, 2), "match_type": "fuzzy", "position": position, "citation_text": citation_text, } return { "cited": False, "confidence": 0.0, "match_type": None, "position": None, "citation_text": None, } def _extract_candidates(self, text: str) -> list[str]: return [w for w in re.split(r'[^\w\u4e00-\u9fff]+', text) if len(w) >= 2] def _extract_position_and_context(self, text: str, keyword: str) -> tuple[int | None, str | None]: paragraphs = [p.strip() for p in text.split('\n') if p.strip()] if not paragraphs: paragraphs = [text] for idx, paragraph in enumerate(paragraphs, start=1): if keyword in paragraph: snippet = paragraph[:200] return idx, snippet return None, None class CompetitorDetector: KNOWN_BRANDS = { "保险": ["中国平安", "中国人寿", "太平洋保险", "新华保险", "泰康保险", "中国人保", "友邦保险"], "金融": ["工商银行", "建设银行", "农业银行", "中国银行", "招商银行", "交通银行"], "科技": ["华为", "腾讯", "阿里巴巴", "百度", "字节跳动", "小米", "京东"], } def detect(self, text: str, target_brand: str) -> list[str]: if not text: return [] competitors = set() for category, brands in self.KNOWN_BRANDS.items(): for brand in brands: if brand == target_brand: continue if brand in text: competitors.add(brand) return sorted(list(competitors)) class CitationEngine: def __init__(self): self._supported_platforms = { "wenxin", "kimi", "doubao", "tongyi", "qingyan", "tiangong", "xinghuo", } self.matcher = None self.competitor_detector = CompetitorDetector() async def execute_query(self, query: Query, db: AsyncSession) -> list[CitationRecord]: self.matcher = BrandMatcher( target_brand=query.target_brand, brand_aliases=query.brand_aliases or [], ) records: list[CitationRecord] = [] platforms = query.platforms or ["wenxin", "kimi"] for platform_name in platforms: task = await self._get_or_create_task(db, query.id, platform_name) task.status = "running" task.started_at = datetime.utcnow() task.error_message = None await db.commit() try: result = await self.execute_single_platform( keyword=query.keyword, platform=platform_name, target_brand=query.target_brand, brand_aliases=query.brand_aliases or [], ) record = CitationRecord.from_citation_result( query_id=query.id, platform=platform_name, result=result, ) db.add(record) records.append(record) task.status = "success" task.completed_at = datetime.utcnow() await db.commit() except Exception as e: logger.error(f"平台 {platform_name} 查询失败: {e}") error_msg = str(e) task.status = "failed" task.error_message = error_msg task.completed_at = datetime.utcnow() record = CitationRecord.from_citation_result( query_id=query.id, platform=platform_name, result={"cited": False, "raw_response": error_msg}, ) db.add(record) records.append(record) await db.commit() query.last_queried_at = datetime.utcnow() query.next_query_at = self._calculate_next_query_at(query.frequency) await db.commit() return records async def execute_single_platform( self, keyword: str, platform: str, target_brand: str, brand_aliases: list, ) -> dict: if platform not in self._supported_platforms: raise ValueError(f"不支持的平台: {platform}") search_keyword = f"{keyword} {target_brand}" raw_response = await query_platform_raw( platform_name=platform, keyword=search_keyword, brand_name=target_brand, ) citation_analysis = analyze_citations(raw_response) matcher = BrandMatcher(target_brand=target_brand, brand_aliases=brand_aliases) match_result = matcher.match(citation_analysis.clean_response) competitor_brands = self.competitor_detector.detect( citation_analysis.clean_response, target_brand ) source_urls = [ c.source_url for c in citation_analysis.citations if c.source_url ] source_titles = [ c.source_title for c in citation_analysis.citations if c.source_title ] citation_contexts = [ c.citation_context for c in citation_analysis.citations if c.citation_context ] return { "cited": match_result["cited"], "confidence": match_result["confidence"], "match_type": match_result["match_type"], "position": match_result["position"], "citation_text": match_result["citation_text"], "competitor_brands": competitor_brands, "raw_response": raw_response, "data_source": citation_analysis.data_source, "source_urls": source_urls, "source_titles": source_titles, "citation_contexts": citation_contexts, "ai_response_text": citation_analysis.clean_response, } async def _get_or_create_task( self, db: AsyncSession, query_id: uuid.UUID, platform: str ) -> QueryTask: stmt = select(QueryTask).where( QueryTask.query_id == query_id, QueryTask.platform == platform, ) result = await db.execute(stmt) task = result.scalar_one_or_none() if not task: task = QueryTask( query_id=query_id, platform=platform, status="pending", ) db.add(task) await db.commit() await db.refresh(task) return task def _calculate_next_query_at(self, frequency: str | None) -> datetime: now = datetime.utcnow() freq_map = { "daily": timedelta(days=1), "weekly": timedelta(days=7), "monthly": timedelta(days=30), } delta = freq_map.get(frequency or "weekly", timedelta(days=7)) return now + delta async def close(self): pass