diff --git a/clickhouse-1c/ai/company_intelligence_api.py b/clickhouse-1c/ai/company_intelligence_api.py
index f6b1369..ff30e65 100644
--- a/clickhouse-1c/ai/company_intelligence_api.py
+++ b/clickhouse-1c/ai/company_intelligence_api.py
@@ -61,6 +61,14 @@ def manager_brief_state_dir() -> Path:
return root / "state" / "manager-brief"
+def weekly_digest_state_dir() -> Path:
+ root = Path(os.getenv("AW_1C_ROOT", "/opt/activitywatch/clickhouse-1c"))
+ configured = os.getenv("AW_1C_WEEKLY_DIGEST_STATE_DIR")
+ if configured:
+ return Path(configured)
+ return root / "state" / "weekly-digest"
+
+
def load_latest_manager_brief() -> dict[str, Any]:
latest_path = manager_brief_state_dir() / "latest.json"
if not latest_path.exists():
@@ -68,6 +76,13 @@ def load_latest_manager_brief() -> dict[str, Any]:
return json.loads(latest_path.read_text(encoding="utf-8"))
+def load_latest_weekly_digest() -> dict[str, Any]:
+ latest_path = weekly_digest_state_dir() / "latest.json"
+ if not latest_path.exists():
+ raise HTTPException(status_code=404, detail="weekly digest not generated yet")
+ return json.loads(latest_path.read_text(encoding="utf-8"))
+
+
def load_brief_history_records(limit: int = 20) -> list[dict[str, Any]]:
history_dir = manager_brief_state_dir() / "history"
if not history_dir.exists():
@@ -221,6 +236,136 @@ def build_weekly_trend_report(payloads: list[dict[str, Any]], days: int = 7) ->
}
+def tier_rank(value: str | None) -> int:
+ return {
+ "low": 1,
+ "medium": 2,
+ "high": 3,
+ "critical": 4,
+ }.get((value or "low").lower(), 1)
+
+
+def find_change_for_company(
+ items: list[dict[str, Any]],
+ counterparty: str,
+ infobase: str | None = None,
+) -> dict[str, Any] | None:
+ fallback: dict[str, Any] | None = None
+ for item in items:
+ if str(item.get("company") or "") != counterparty:
+ continue
+ if infobase and str(item.get("infobase") or "") == infobase:
+ return item
+ if fallback is None:
+ fallback = item
+ return fallback
+
+
+def build_company_priority_context(summary_payload: dict[str, Any], infobase: str | None = None) -> dict[str, Any]:
+ card = summary_payload.get("card") or {}
+ counterparty = str(card.get("counterparty") or "")
+ current_infobase = infobase or str(card.get("infobase") or "")
+ latest_change = None
+ weekly_change = None
+ weekly_report = {"top_weekly_changes": []}
+
+ try:
+ latest_brief = load_latest_manager_brief()
+ latest_change = find_change_for_company(
+ extract_delta(latest_brief).get("top_changes", []),
+ counterparty,
+ current_infobase or None,
+ )
+ except HTTPException:
+ latest_brief = None
+
+ try:
+ weekly_report = build_weekly_trend_report(load_brief_history_payloads(limit=400), days=7)
+ weekly_change = find_change_for_company(
+ weekly_report.get("top_weekly_changes", []),
+ counterparty,
+ current_infobase or None,
+ )
+ except Exception:
+ weekly_report = {"top_weekly_changes": []}
+
+ current_tier = "low"
+ current_score = 0.0
+ current_reason = "сильных weekly/delta-триггеров не видно"
+ if latest_change:
+ current_tier = str(latest_change.get("priority_tier") or "low")
+ current_score = float(latest_change.get("priority_score") or 0)
+ current_reason = str(latest_change.get("priority_reason") or latest_change.get("summary") or current_reason)
+ elif weekly_change:
+ current_tier = str(weekly_change.get("priority_tier") or "low")
+ current_score = float(weekly_change.get("priority_score") or 0)
+ current_reason = str(weekly_change.get("priority_reason") or weekly_change.get("summary") or current_reason)
+ else:
+ severity = str(card.get("signal_severity") or "low")
+ current_tier = severity if severity in {"critical", "high", "medium", "low"} else "low"
+ current_score = float(card.get("signal_score") or 0)
+ if int(card.get("open_cases_total") or 0) > 0 or int(card.get("active_locks") or 0) > 0:
+ current_reason = "у компании уже есть operational pressure: кейсы/блокировки/сигналы"
+
+ evidence: list[str] = []
+ actions: list[str] = []
+ if latest_change:
+ evidence.append(f"Последний запуск: {latest_change.get('summary') or current_reason}")
+ if int(latest_change.get("open_cases_delta") or 0) > 0:
+ evidence.append(f"Кейсы выросли на +{int(latest_change.get('open_cases_delta') or 0)}.")
+ actions.append("Сразу разобрать новые открытые кейсы по компании.")
+ if int(latest_change.get("detections_delta") or 0) > 0:
+ evidence.append(f"Detections выросли на +{int(latest_change.get('detections_delta') or 0)}.")
+ if int(latest_change.get("active_locks_delta") or 0) > 0:
+ evidence.append(f"Блокировки выросли на +{int(latest_change.get('active_locks_delta') or 0)}.")
+ actions.append("Проверить, не застряла ли файловая база в busy/lock-контуре.")
+ if float(latest_change.get("forecast_delta") or 0) < 0:
+ evidence.append(f"Прогноз 30д просел на {fmt_number(latest_change.get('forecast_delta'))}.")
+ if weekly_change and weekly_change is not latest_change:
+ evidence.append(f"За неделю компания вошла в top changes: {weekly_change.get('priority_reason') or weekly_change.get('summary')}.")
+ if str(card.get("signal_severity") or "none") in {"critical", "high"}:
+ evidence.append(f"Текущая severity {card.get('signal_severity')} при score {fmt_number(card.get('signal_score'))}.")
+ if card.get("registry_match_mode") == "manual":
+ evidence.append("Привязка к реестру manual: интерпретацию владельца и юр. соответствия проверять отдельно.")
+ if int(card.get("open_cases_total") or 0) > 0:
+ evidence.append(f"Сейчас открытых кейсов {int(card.get('open_cases_total') or 0)}.")
+ if int(card.get("detections_total") or 0) > 0:
+ evidence.append(f"Сейчас active detections {int(card.get('detections_total') or 0)}.")
+ if int(card.get("active_locks") or 0) > 0:
+ evidence.append(f"Сейчас активных блокировок {int(card.get('active_locks') or 0)}.")
+ if int(card.get("days_since_last_activity") or 0) >= 7:
+ evidence.append(f"Компания молчит уже {int(card.get('days_since_last_activity') or 0)} дн.")
+ actions.append("Подтвердить, это плановая бизнес-пауза или выпадение из операционного ритма.")
+ if not actions:
+ actions.append("Сначала проверить последние сигналы и timeline компании, затем подтверждать бизнес-выводы.")
+ if int(card.get("open_cases_total") or 0) > 0 and "Сразу разобрать новые открытые кейсы по компании." not in actions:
+ actions.append("Разобрать открытые кейсы до обсуждения прогноза.")
+ if int(card.get("active_locks") or 0) > 0 and "Проверить, не застряла ли файловая база в busy/lock-контуре." not in actions:
+ actions.append("Проверить busy/lock-контур файловой базы.")
+
+ if tier_rank(current_tier) >= tier_rank("high"):
+ verdict = (
+ "Приоритет действительно высокий не из-за общей красноты портфеля, а из-за конкретных operational-сдвигов по этой компании."
+ )
+ elif tier_rank(current_tier) == tier_rank("medium"):
+ verdict = "Приоритет средний: явный сдвиг есть, но он ещё не выглядит аварийным."
+ else:
+ verdict = "Приоритет низкий: компания видна в портфеле, но жёсткого триггера на разбор первой очереди нет."
+
+ return {
+ "current_priority_tier": current_tier,
+ "current_priority_score": round(current_score, 2),
+ "current_priority_reason": current_reason,
+ "latest_change": latest_change,
+ "weekly_change": weekly_change,
+ "weekly_period_start": weekly_report.get("period_start"),
+ "weekly_period_end": weekly_report.get("period_end"),
+ "verdict": verdict,
+ "evidence": evidence[:8],
+ "actions": actions[:5],
+ }
+
+
def grafana_company_dashboard_url() -> str:
return os.getenv(
"AW_1C_MANAGER_BRIEF_GRAFANA_URL",
@@ -276,6 +421,7 @@ def render_manager_brief_html(payload: dict[str, Any]) -> str:
history_html_url = "/manager/briefs"
delta_html_url = "/manager/changes"
weekly_html_url = "/manager/trends/weekly"
+ weekly_digest_url = "/manager/digest/weekly"
problematic_1d_url = "/manager/problematic?days=1"
problematic_7d_url = "/manager/problematic?days=7"
json_url = "/api/1/analytics-1c/manager/brief/latest"
@@ -575,6 +721,7 @@ def render_manager_brief_html(payload: dict[str, Any]) -> str:
История brief
Что изменилось
Неделя
+ Weekly digest
Проблемные 1д
Проблемные 7д
History API
@@ -1004,6 +1151,7 @@ def render_brief_delta_html(payload: dict[str, Any]) -> str:
Текущий brief
История brief
Неделя
+ Weekly digest
Проблемные 1д
Проблемные 7д
@@ -1160,6 +1308,7 @@ def render_weekly_trend_html(report: dict[str, Any]) -> str:
Что изменилось
История brief
Проблемные 7д
+ Weekly digest
@@ -1213,6 +1362,156 @@ def render_weekly_trend_html(report: dict[str, Any]) -> str: