PMFlow/backend/app/api/signals_api/ingest_fetch.py

298 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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 ...core.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.signal.signal_capture_service.impact_analyzer import analyze_impact
from ...services.signal.signal_capture_service.external_fetcher import fetch_hn_stories
from ...services.integrations.integration_core.lark_service import LarkService
from ...services.llm_service import llm_service
from ...services.integrations.integration_core.plane_service 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",
}