"""Signal ingest/fetch endpoints.""" from __future__ import annotations import json from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from ...database import get_db from ...dependencies.tenant import get_tenant_id from ...models import Signal from ...schemas import SignalCreate, SignalPriority, classify_signal_priority from ...services.analyzer import analyze_impact from ...services.fetcher import fetch_hn_stories from ...services.lark import LarkService from ...services.llm import llm_service from ...services.plane import plane_service router = APIRouter() lark_service = LarkService() @router.post("/ingest", response_model=dict) async def ingest_signal( signal: SignalCreate, background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db), tenant_id: int = Depends(get_tenant_id), ): """接入外部信号,按优先级处理""" initial_priority = signal.priority or classify_signal_priority(signal.impact_score) new_signal = Signal( source=signal.source, content=signal.content, impact_score=signal.impact_score, priority=initial_priority.value, ) db.add(new_signal) await db.commit() await db.refresh(new_signal) if initial_priority == SignalPriority.LOW: return { "id": str(new_signal.id), "priority": initial_priority.value, "status": "recorded", "message": "Low priority signal recorded without processing", } async def process_signal_internal(): try: relevant_context = await llm_service.find_relevant_context( signal_content=signal.content, db_session=db, top_k=3, min_similarity=0.3, tenant_id=tenant_id, ) strategy_result = await llm_service.evaluate_with_voting_async( signal_content=signal.content, relevant_context=relevant_context, impact_score=signal.impact_score, db=db, tenant_id=tenant_id, ) except RuntimeError: if initial_priority == SignalPriority.CRITICAL: raise HTTPException(status_code=403, detail="预算超限,AI 调用被阻止(hard limit)") return { "id": str(new_signal.id), "priority": initial_priority.value, "status": "blocked", "processing_mode": "background", "message": "预算超限,AI 调用被阻止(hard limit)", } is_strategic = strategy_result.get("is_strategic", False) decision_proposal = strategy_result.get("decision_proposal", {}) tasks = strategy_result.get("tasks", []) confidence = strategy_result.get("confidence", 0.0) final_priority = classify_signal_priority(signal.impact_score, confidence) new_signal.priority = final_priority.value sync_result = None notification_sent = False if final_priority in [SignalPriority.CRITICAL, SignalPriority.HIGH]: if is_strategic and tasks: sync_result = await plane_service.sync_tasks( tasks, is_urgent=(final_priority == SignalPriority.CRITICAL), ) default_chat_id = "oc_1234567890" await lark_service.send_strategic_alert( default_chat_id, signal.content, decision_proposal, tasks, ) notification_sent = True new_signal.impact_score = 1.0 analysis_data = { "decision_proposal": decision_proposal, "competitiveness_radar": strategy_result.get("competitiveness_radar"), "detected_tech_stacks": strategy_result.get("detected_tech_stacks", []), "related_knowledge_nodes": strategy_result.get("related_knowledge_nodes", []), "rag_context_used": len(relevant_context) if relevant_context else 0, "rag_context_sources": [ctx["id"] for ctx in (relevant_context or [])], } if "_voting" in strategy_result: voting_info = strategy_result["_voting"] analysis_data["voting"] = { "enabled": voting_info.get("enabled"), "models_used": voting_info.get("models_used"), "strategic_votes": voting_info.get("strategic_votes"), "total_votes": voting_info.get("total_votes"), "unanimous": voting_info.get("unanimous"), } new_signal.strategic_analysis = json.dumps(analysis_data) new_signal.tasks_generated = json.dumps(tasks) new_signal.processed = True await db.commit() await db.refresh(new_signal) return { "id": str(new_signal.id), "priority": final_priority.value, "is_strategic": is_strategic, "confidence": confidence, "decision_proposal": decision_proposal, "rag_context_used": len(relevant_context) if relevant_context else 0, "voting": strategy_result.get("_voting"), "tasks_generated": len(tasks), "sync_result": sync_result, "notification_sent": notification_sent, } if initial_priority == SignalPriority.CRITICAL: result = await process_signal_internal() result["processing_mode"] = "synchronous" return result background_tasks.add_task(process_signal_internal) return { "id": str(new_signal.id), "priority": initial_priority.value, "status": "queued", "processing_mode": "background", "message": f"{initial_priority.value.upper()} priority signal queued for processing", } @router.post("/fetch", response_model=dict) async def trigger_signal_fetch( background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db), tenant_id: int = Depends(get_tenant_id), ): """手动触发抓取并分析信号""" def _stop_ollama(): import os import signal import subprocess killed = False try: result = subprocess.run( ["pgrep", "-f", "ollama"], capture_output=True, text=True, ) if result.returncode == 0: for pid in result.stdout.strip().split("\n"): if pid: try: os.kill(int(pid), signal.SIGTERM) killed = True except ProcessLookupError: pass except Exception as exc: print(f"[AI] pgrep failed: {exc}") if not killed: try: result = subprocess.run( ["pkill", "-9", "-f", "ollama"], capture_output=True, timeout=5, ) killed = result.returncode == 0 except Exception as exc: print(f"[AI] pkill failed: {exc}") if killed: print("[AI] Ollama stopped - analysis complete") raw_stories = await fetch_hn_stories(limit=30) new_signals_count = 0 processed_count = 0 for story in raw_stories: result = await db.execute(select(Signal).where(Signal.content == story["content"])) if result.scalars().first(): continue impact = analyze_impact(story["content"]) if impact > 0.4: initial_priority = classify_signal_priority(impact) new_signal = Signal( source=story["source"], content=story["content"], impact_score=impact, priority=initial_priority.value, ) db.add(new_signal) await db.commit() await db.refresh(new_signal) new_signals_count += 1 if initial_priority == SignalPriority.LOW: continue async def process_fetched(): nonlocal processed_count try: relevant_context = await llm_service.find_relevant_context( signal_content=story["content"], db_session=db, top_k=3, min_similarity=0.3, tenant_id=tenant_id, ) strategy_result = await llm_service.evaluate_with_voting_async( signal_content=story["content"], relevant_context=relevant_context, impact_score=impact, db=db, tenant_id=tenant_id, ) except RuntimeError: return is_strategic = strategy_result.get("is_strategic", False) decision_proposal = strategy_result.get("decision_proposal", {}) confidence = strategy_result.get("confidence", 0.0) final_priority = classify_signal_priority(impact, confidence) new_signal.priority = final_priority.value analysis_data = { "decision_proposal": decision_proposal, "competitiveness_radar": strategy_result.get("competitiveness_radar"), "detected_tech_stacks": strategy_result.get("detected_tech_stacks", []), "related_knowledge_nodes": strategy_result.get("related_knowledge_nodes", []), "rag_context_used": len(relevant_context) if relevant_context else 0, "rag_context_sources": [ctx["id"] for ctx in (relevant_context or [])], "confidence": confidence, "is_strategic": is_strategic, } if "_voting" in strategy_result: vi = strategy_result["_voting"] analysis_data["voting"] = { "enabled": vi.get("enabled"), "models_used": vi.get("models_used"), "strategic_votes": vi.get("strategic_votes"), "total_votes": vi.get("total_votes"), "unanimous": vi.get("unanimous"), } new_signal.strategic_analysis = json.dumps(analysis_data) new_signal.tasks_generated = json.dumps(strategy_result.get("tasks", [])) new_signal.processed = True await db.commit() processed_count += 1 if initial_priority in [SignalPriority.MEDIUM, SignalPriority.HIGH]: background_tasks.add_task(process_fetched) else: await process_fetched() _stop_ollama() return { "status": "success", "new_signals_count": new_signals_count, "processed_count": processed_count, "message": f"Fetched {new_signals_count} signals, {processed_count} analyzed", }