298 lines
11 KiB
Python
298 lines
11 KiB
Python
"""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",
|
||
}
|