Files
Jyotisha/scripts/unified_consultation_orchestrator.py
T
2026-07-01 19:15:35 +08:00

241 lines
9.4 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Shared orchestration contract for skill/MCP and web/API surfaces."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
@dataclass(frozen=True)
class RouteDefinition:
question_type: str
primary_theme: str
focus_techniques: list[str]
display_label: str
class UnifiedConsultationOrchestrator:
"""Normalizes user intent and exposes a surface-agnostic workflow contract."""
NAME = "UnifiedConsultationOrchestrator"
SOURCE_PRIORITY = {
"mode": "vedastro_official_snapshot_first",
"priority": [
"vedastro_official_snapshot",
"local_supplemental_modules",
"local_fallback_only_when_official_blocked",
],
"boundary": (
"Official VedAstro raw evidence is preferred; local modules supplement, "
"cross-check, and fallback when official calls are blocked."
),
}
_THEME_ALIASES = {
"relationship": "marriage",
"marriage": "marriage",
"finance": "wealth",
"money": "wealth",
"wealth": "wealth",
"career": "career",
"health": "health",
"spirituality": "spirituality",
"事业": "career",
"婚恋": "marriage",
"婚姻": "marriage",
"感情": "marriage",
"财富": "wealth",
"财运": "wealth",
"健康": "health",
"灵性": "spirituality",
}
_DEFAULT_THEMES = ["career", "marriage", "wealth"]
_ALLOWED_THEMES = {"career", "marriage", "wealth", "health", "spirituality"}
_ROUTE_DEFINITIONS = {
"career": RouteDefinition(
question_type="career",
primary_theme="career",
focus_techniques=["D10", "Dasha", "Shadbala", "Transit", "Narayana Dasha"],
display_label="career",
),
"relationship": RouteDefinition(
question_type="relationship",
primary_theme="marriage",
focus_techniques=["D9", "UL Upapada", "Dasha", "Nakshatra", "Vivah Saham"],
display_label="relationship",
),
"finance": RouteDefinition(
question_type="finance",
primary_theme="wealth",
focus_techniques=["D2", "D11", "Dasha", "Shadbala", "Ashtakavarga"],
display_label="finance",
),
"timing": RouteDefinition(
question_type="timing",
primary_theme="career",
focus_techniques=["Dasha", "Transit", "Double Transit", "Gochara"],
display_label="timing",
),
"general": RouteDefinition(
question_type="general",
primary_theme="career",
focus_techniques=["D1", "D9", "Dasha", "Yoga", "Shadbala", "Ashtakavarga"],
display_label="general",
),
}
_SYNC_STEPS_BY_ROUTE = {
"career": ["compute_chart", "run_rectification_gate", "run_thematic_report"],
"relationship": ["compute_chart", "run_rectification_gate", "run_thematic_report"],
"finance": ["compute_chart", "run_rectification_gate", "run_thematic_report"],
"timing": ["compute_chart", "run_rectification_gate", "run_thematic_report"],
"general": ["compute_chart", "run_rectification_gate", "run_thematic_report"],
}
_ASYNC_CANDIDATES = [
"historical_event_backtest",
"official_event_radar_expansion",
"extended_prompt_pack_refresh",
]
def normalize_themes(self, raw: Any) -> list[str]:
if raw in (None, "", "all"):
values = list(self._DEFAULT_THEMES)
elif isinstance(raw, str):
values = [raw]
elif isinstance(raw, list):
values = raw
else:
raise ValueError("theme/themes must be a string, list, or all")
normalized: list[str] = []
for value in values:
key = self._THEME_ALIASES.get(str(value).strip().lower(), str(value).strip().lower())
if key not in self._ALLOWED_THEMES:
raise ValueError(f"Unknown theme: {value}")
if key not in normalized:
normalized.append(key)
return normalized or list(self._DEFAULT_THEMES)
def resolve_route(self, question: str, themes: list[str] | None = None) -> dict[str, Any]:
text = (question or "").lower()
normalized_themes = themes or list(self._DEFAULT_THEMES)
domain_tokens = {
"career": ("career", "job", "work", "promotion", "business", "profession", "事业", "工作", "升职", "生意"),
"relationship": ("marriage", "married", "wedding", "relationship", "love", "spouse", "partner", "divorce", "婚恋", "婚姻", "感情", "配偶", "恋爱", "结婚", "marry"),
"finance": ("money", "wealth", "finance", "investment", "property", "income", "财务", "财富", "投资", "房产", "收入"),
}
first_hits: list[tuple[int, str]] = []
for route_name, tokens in domain_tokens.items():
indexes = [text.find(token) for token in tokens if token in text]
indexes = [idx for idx in indexes if idx >= 0]
if indexes:
first_hits.append((min(indexes), route_name))
if first_hits:
route_name = sorted(first_hits, key=lambda item: item[0])[0][1]
route = self._ROUTE_DEFINITIONS[route_name]
elif not text.strip():
route = self._ROUTE_DEFINITIONS["general"]
elif any(token in text for token in ("when", "timing", "event", "prediction", "future", "应期", "预测", "何时", "将来")):
route = self._ROUTE_DEFINITIONS["timing"]
elif "career" in normalized_themes:
route = self._ROUTE_DEFINITIONS["career"]
elif "marriage" in normalized_themes:
route = self._ROUTE_DEFINITIONS["relationship"]
elif "wealth" in normalized_themes:
route = self._ROUTE_DEFINITIONS["finance"]
else:
route = self._ROUTE_DEFINITIONS["general"]
return {
"question_type": route.question_type,
"primary_theme": route.primary_theme,
"focus_techniques": list(route.focus_techniques),
"display_label": route.display_label,
}
def shared_contract(
self,
*,
entry_mode: str,
question: str,
themes: list[str],
route_packet: dict[str, Any],
surface: str,
) -> dict[str, Any]:
return {
"name": self.NAME,
"surface": surface,
"entry_mode": entry_mode,
"question": question or "",
"themes": list(themes),
"route": dict(route_packet),
"source_priority": {
"mode": self.SOURCE_PRIORITY["mode"],
"priority": list(self.SOURCE_PRIORITY["priority"]),
"boundary": self.SOURCE_PRIORITY["boundary"],
},
"shared_capabilities": [
"theme_normalization",
"question_routing",
"vedastro_official_priority",
"rectification_gate_reuse",
"thematic_report_reuse",
],
}
def runtime_planner(
self,
*,
entry_mode: str,
question: str,
themes: list[str],
route_packet: dict[str, Any],
events: list[dict[str, Any]] | None,
surface: str,
high_rigor: bool,
) -> dict[str, Any]:
route_name = route_packet.get("question_type") or "general"
sync_steps = list(self._SYNC_STEPS_BY_ROUTE.get(route_name, self._SYNC_STEPS_BY_ROUTE["general"]))
if entry_mode == "rectification":
sync_steps = [step for step in sync_steps if step != "run_rectification_gate"]
sync_steps.insert(0, "run_rectification_gate")
if high_rigor and "run_historical_event_backtest" not in sync_steps and events:
sync_steps.append("run_historical_event_backtest")
async_candidates = list(self._ASYNC_CANDIDATES)
if not events:
async_candidates = [step for step in async_candidates if step != "historical_event_backtest"]
return {
"planner_name": "UnifiedConsultationRuntimePlanner",
"surface": surface,
"entry_mode": entry_mode,
"high_rigor": bool(high_rigor),
"route": dict(route_packet),
"question_context": {
"question": question or "",
"themes": list(themes),
"event_count": len(events or []),
},
"sync_steps": sync_steps,
"async_candidates": async_candidates,
"source_priority": {
"mode": self.SOURCE_PRIORITY["mode"],
"priority": list(self.SOURCE_PRIORITY["priority"]),
"boundary": self.SOURCE_PRIORITY["boundary"],
},
"reuse_contract": {
"chart": "compute_chart",
"rectification": "rectification_gate",
"thematic_report": "thematic_report",
"historical_backtest": "historical_event_backtest",
},
"boundary": (
"This runtime planner unifies entry routing and module reuse. It does not imply that every VedAstro "
"catalog method executes on every request; route-relevant official evidence is still subject to live "
"availability, cache policy, and async limits."
),
}