Files
Jyotisha/scripts/orchestrator_bridge.py
2026-06-13 15:06:11 +08:00

1061 lines
47 KiB
Python
Raw Permalink 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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
orchestrator_bridge.py — 编排器桥接层
=====================================
打通 reading_orchestrator (技法执行层) 与 report_orchestrator (叙事生成层)。
职责:
1. 将 ReadingChapter 转换为 report_orchestrator 可消费的 TechniqueResult
2. 映射主题(ReadingTheme ↔ ThemeName
3. 统一冲突裁决的表达方式
4. 提供端到端的一体化报告生成入口
版本: v1.0 | 2026-06-07
"""
from typing import Dict, List, Any, Optional
from datetime import datetime
import json
# Import dasha systems (handle relative imports)
try:
from ashtottari_dasha import calculate_ashtottari_dasha
from yogini_dasha import calculate_yogini_dasha
from kalachakra_dasha import calculate_kalachakra_dasha
except ImportError:
from scripts.ashtottari_dasha import calculate_ashtottari_dasha
from scripts.yogini_dasha import calculate_yogini_dasha
from scripts.kalachakra_dasha import calculate_kalachakra_dasha
# Import both orchestrators (handle relative imports)
try:
from reading_orchestrator import (
ReadingOrchestrator, ReadingTheme, ReadingChapter,
TechniqueResult as ReadingTechniqueResult,
)
except ImportError:
from scripts.reading_orchestrator import (
ReadingOrchestrator, ReadingTheme, ReadingChapter,
TechniqueResult as ReadingTechniqueResult,
)
try:
from report_orchestrator import (
ThematicReportOrchestrator as ReportOrchestrator,
ThemeName, ThemeReport,
TechniqueResult as ReportTechniqueResult,
StrengthLevel, TimingAnchor,
)
except ImportError:
from scripts.report_orchestrator import (
ThematicReportOrchestrator as ReportOrchestrator,
ThemeName, ThemeReport,
TechniqueResult as ReportTechniqueResult,
StrengthLevel, TimingAnchor,
)
# ═══════════════════════════════════════════════════════════════
# 主题映射
# ═══════════════════════════════════════════════════════════════
THEME_MAPPING: Dict[ReadingTheme, ThemeName] = {
ReadingTheme.MARRIAGE: ThemeName.MARRIAGE,
ReadingTheme.CAREER: ThemeName.CAREER,
ReadingTheme.WEALTH: ThemeName.WEALTH,
ReadingTheme.HEALTH: ThemeName.HEALTH,
ReadingTheme.SPIRITUAL: ThemeName.SPIRITUALITY,
}
REVERSE_THEME_MAPPING: Dict[ThemeName, ReadingTheme] = {
v: k for k, v in THEME_MAPPING.items()
}
# ═══════════════════════════════════════════════════════════════
# 桥接转换器
# ═══════════════════════════════════════════════════════════════
class OrchestratorBridge:
"""
编排器桥接器
将 reading_orchestrator 的输出(ReadingChapter)转换为
report_orchestrator 的输入(TechniqueResult + ThemeReport)。
"""
def __init__(
self,
reading_orchestrator: ReadingOrchestrator,
report_orchestrator: ReportOrchestrator,
):
self.ro = reading_orchestrator
self.rpo = report_orchestrator
# ── ReadingChapter → ReportTechniqueResult ──
@staticmethod
def chapter_to_technique_results(chapter: ReadingChapter) -> List[ReportTechniqueResult]:
"""
将 ReadingChapter 拆解为 report_orchestrator 的 TechniqueResult 列表。
策略:
- findings → 每个 finding 对应一个 TechniqueResult
- conflicts → 每个 conflict 也作为一个 TechniqueResultsentiment=neutral,标注矛盾)
- timing → 如果时间锚定存在,也生成一个 TechniqueResult
"""
results: List[ReportTechniqueResult] = []
# 1. Findings → positive/negative TechniqueResult
for finding in chapter.findings:
sentiment = _infer_sentiment(finding)
strength = _infer_strength(finding, chapter.conflicts)
results.append(ReportTechniqueResult(
technique=f"finding_{len(results)}",
chart="D1", # default; can be refined
conclusion=finding,
sentiment=sentiment,
strength=strength,
))
# 2. Conflicts → neutral TechniqueResult with details
for conflict in chapter.conflicts:
results.append(ReportTechniqueResult(
technique="conflict_resolution",
chart="D1",
conclusion=conflict.narrative,
sentiment="neutral",
strength=StrengthLevel.MODERATE,
details={
"dimension": conflict.dimension,
"d1_finding": conflict.d1_finding,
"d9_finding": conflict.d9_finding,
"resolution": conflict.resolution,
},
))
# 3. Timing → TechniqueResult
if chapter.timing:
results.append(ReportTechniqueResult(
technique="timing_anchor",
chart="Dasha",
conclusion=chapter.timing,
sentiment="neutral",
strength=StrengthLevel.MODERATE,
))
return results
# ── 推运系统注入 ──
def inject_dasha_results(self, chart_data: Dict[str, Any]) -> Dict[str, Any]:
"""
计算并注入 Ashtottari / Yogini / Kalachakra Dasha 结果。
从 chart_data 提取 birth_info,调用三个推运模块,
将当前推运周期转换为 TimingAnchor 注入 report_orchestrator。
Returns:
三个推运系统的原始计算结果,供后续使用。
"""
# 从 chart_data 提取 birth_info
birth_info = self._extract_birth_info(chart_data)
if not birth_info:
return {}
results: Dict[str, Any] = {}
# 1. Ashtottari Dasha (条件性推运)
try:
ash = calculate_ashtottari_dasha(birth_info)
results["ashtottari"] = ash
if ash.get("applicable") and ash.get("current"):
self._add_dasha_timing_anchor(
system="Ashtottari",
current=ash["current"],
total_cycle=ash.get("total_cycle", 108),
)
except Exception as e:
import logging
logging.warning(f"[orchestrator] ashtottari dasha calculation failed: {e}")
# 2. Yogini Dasha (普遍适用)
try:
yog = calculate_yogini_dasha(birth_info)
results["yogini"] = yog
if yog.get("current"):
self._add_dasha_timing_anchor(
system="Yogini",
current=yog["current"],
total_cycle=yog.get("total_cycle", 36),
)
except Exception as e:
import logging
logging.warning(f"[orchestrator] yogini dasha calculation failed: {e}")
# 3. Kalachakra Dasha (条件性推运)
try:
kal = calculate_kalachakra_dasha(birth_info)
results["kalachakra"] = kal
if kal.get("current"):
self._add_dasha_timing_anchor(
system="Kalachakra",
current=kal["current"],
total_cycle=kal.get("total_cycle", 0),
)
except Exception as e:
import logging
logging.warning(f"[orchestrator] kalachakra dasha calculation failed: {e}")
# 4. 将推运结果也作为 TechniqueResult 注入所有主题
self._inject_dasha_technique_results(results)
return results
def _extract_birth_info(self, chart_data: Any) -> Optional[Dict[str, Any]]:
"""从 chart_data 提取推运计算所需的 birth_info。
支持 dict 和 dataclass 对象(如 BirthChartData)。
如果数据不足,返回 None(推运注入将静默跳过)。
"""
import dataclasses
# 统一转为 dict
if hasattr(chart_data, "__dataclass_fields__"):
data = dataclasses.asdict(chart_data)
elif isinstance(chart_data, dict):
data = chart_data
else:
return None
info: Dict[str, Any] = {}
# birth_datetime
birth_dt = data.get("birth_datetime")
if birth_dt is None:
chart = data.get("chart") or data.get("natal_chart")
if chart:
birth_dt = chart.get("birth_datetime")
if isinstance(birth_dt, str):
birth_dt = datetime.fromisoformat(birth_dt.replace("Z", "+00:00"))
info["birth_datetime"] = birth_dt
# moon_nakshatra_index
moon_nak = data.get("moon_nakshatra_index")
if moon_nak is None:
chart = data.get("chart") or data.get("natal_chart")
if chart:
nakshatras = chart.get("nakshatras") or chart.get("nakshatra")
if nakshatras and "Moon" in nakshatras:
moon_nak = nakshatras["Moon"].get("index")
if moon_nak is not None:
info["moon_nakshatra_index"] = int(moon_nak)
# is_shukla_paksha (从Tithi推断)
tithi = data.get("tithi")
if tithi is None:
chart = data.get("chart") or data.get("natal_chart")
if chart:
tithi = chart.get("tithi")
if tithi is not None:
tithi_num = int(tithi) if isinstance(tithi, (int, float, str)) else 15
info["is_shukla_paksha"] = 1 <= tithi_num <= 15
# lagna_rashi_index
lagna = data.get("ascendant") or data.get("lagna")
if lagna is None:
chart = data.get("chart") or data.get("natal_chart")
if chart:
lagna = chart.get("ascendant") or chart.get("lagna")
if lagna is not None:
if isinstance(lagna, str):
sign_names = [
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
]
if lagna in sign_names:
info["lagna_rashi_index"] = sign_names.index(lagna)
elif isinstance(lagna, dict):
sign = lagna.get("sign") or lagna.get("rashi")
if sign:
sign_names = [
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
]
if sign in sign_names:
info["lagna_rashi_index"] = sign_names.index(sign)
degree = lagna.get("degree")
if degree is not None:
info["lagna_rashi_index"] = int(degree / 30) % 12
# moon_pada (for Kalachakra)
moon_pada = data.get("moon_pada")
if moon_pada is None:
chart = data.get("chart") or data.get("natal_chart")
if chart:
nakshatras = chart.get("nakshatras") or chart.get("nakshatra")
if nakshatras and "Moon" in nakshatras:
moon_pada = nakshatras["Moon"].get("pada")
if moon_pada is not None:
info["moon_pada"] = int(moon_pada)
else:
info["moon_pada"] = 1
return info if info.get("birth_datetime") else None
def _add_dasha_timing_anchor(
self,
system: str,
current: Dict[str, Any],
total_cycle: int,
) -> None:
"""将单个推运当前周期转换为 TimingAnchor 并注入 report_orchestrator。"""
start_date = current.get("start_date", "")
end_date = current.get("end_date", "")
# 提取年份
try:
start_year = int(start_date[:4]) if isinstance(start_date, str) else datetime.now().year
end_year = int(end_date[:4]) if isinstance(end_date, str) else (start_year + 5)
except (ValueError, TypeError):
start_year = datetime.now().year
end_year = start_year + 5
# 确定行星名称
planet = current.get("planet") or current.get("yogini") or current.get("lord") or "Unknown"
# 构建激活描述
activation = f"{system}推运中,{planet}主运带来人生阶段的转换与业力展现"
if system == "Yogini":
activation = f"Yogini推运中,{planet}女神主导当前周期,影响心理与事件层面"
elif system == "Kalachakra":
rashi = current.get("rashi", "")
mode = current.get("mode", "")
activation = f"Kalachakra {mode}模式推运中,{planet}主宰{rashi}宫阶段"
anchor = TimingAnchor(
dasha_period=f"{system}-{planet}",
start_year=start_year,
end_year=end_year,
activation_description=activation,
is_current=True,
)
# 注入到所有5个主题
for tn in ThemeName:
self.rpo.add_timing(tn, anchor)
def _inject_dasha_technique_results(self, dasha_results: Dict[str, Any]) -> None:
"""将推运结果作为 TechniqueResult 注入所有主题。"""
for system, result in dasha_results.items():
if not result:
continue
current = result.get("current")
if not current:
continue
planet = current.get("planet") or current.get("yogini") or current.get("lord") or "Unknown"
system_zh = {"ashtottari": "Ashtottari推运", "yogini": "Yogini推运", "kalachakra": "Kalachakra推运"}.get(system, system)
conclusion = f"{system_zh}:当前处于{planet}主运周期"
if system == "ashtottari" and result.get("applicable") is False:
conclusion = f"{system_zh}:不适用(出生条件不符合)"
tech_result = ReportTechniqueResult(
technique=f"{system}_dasha",
chart="Dasha",
conclusion=conclusion,
sentiment="neutral",
strength=StrengthLevel.MODERATE,
details={
"system": system,
"current_period": current,
"total_cycle": result.get("total_cycle"),
"applicable": result.get("applicable", True),
},
)
for tn in ThemeName:
self.rpo.add_technique(tn, tech_result)
# ── full-reading.modules → ReportTechniqueResult ──
def inject_full_reading_modules(self, chart_data: Dict[str, Any]) -> Dict[str, Any]:
"""
将 `jyotish_engine.py full-reading` 的真实模块输出注入主题化报告。
v6.1.8 之前,桥接层主要依赖 reading_orchestrator 的 registry;当 registry
未注册真实执行函数时,主题报告容易退化为少量模拟/占位信息。本方法直接消费
full-reading.modules 中已经计算完成的模块,把 Yoga、分盘、Ashtakavarga、
Shadbala、特殊上升点、Vivah Saham、Transit、多系统 Dasha Convergence 等
转成 report_orchestrator 可叙事的 TechniqueResult。
"""
modules = _extract_modules(chart_data)
if not modules:
return {"injected": False, "reason": "no full-reading modules found", "counts": {}}
counts = {theme.value: 0 for theme in ThemeName}
def add(theme: ThemeName, result: ReportTechniqueResult) -> None:
self.rpo.add_technique(theme, result)
counts[theme.value] += 1
self._inject_yoga_module(modules, add)
self._inject_varga_modules(modules, add)
self._inject_strength_modules(modules, add)
self._inject_jaimini_relationship_modules(modules, add)
self._inject_special_points(modules, add)
self._inject_dasa_convergence_module(modules, add)
self._inject_transit_module(modules, add)
return {
"injected": True,
"source": "full-reading.modules",
"counts": counts,
"source_modules": sorted(k for k in modules.keys() if k in {
"yoga", "varga_full", "d9_navamsa_expanded", "ashtakavarga", "shadbala",
"jaimini", "rashi_tulya_navamsa", "special_lagnas", "vivah_saham",
"dasa_convergence", "transit_multi_reference",
"dasha", "yogini_dasha", "ashtottari_dasha", "kalachakra_dasha",
}),
}
def _inject_yoga_module(self, modules: Dict[str, Any], add) -> None:
yoga = modules.get("yoga") or {}
yogas = yoga.get("yogas") or yoga.get("detected_yogas") or []
if not isinstance(yogas, list):
return
added_per_theme = {theme: 0 for theme in ThemeName}
for y in yogas:
if not isinstance(y, dict):
continue
themes = _themes_for_yoga(y)
if not themes:
continue
strength = _strength_from_text(y.get("strength"), default=StrengthLevel.MODERATE)
sentiment = _sentiment_from_yoga(y)
effects = y.get("effects") or []
if isinstance(effects, list):
effect_text = "".join(str(e) for e in effects[:4])
else:
effect_text = str(effects)
conclusion = f"{y.get('name_cn') or y.get('name') or 'Yoga'}{y.get('combination', '检测到相关组合')}"
if effect_text:
conclusion += f";传统效应:{effect_text}"
for theme in themes:
if added_per_theme[theme] >= 8:
continue
add(theme, ReportTechniqueResult(
technique=f"yoga:{y.get('rule_id') or y.get('name') or 'unknown'}",
chart="D1",
conclusion=conclusion,
sentiment=sentiment,
strength=strength,
details={"source_module": "yoga", "raw": y},
))
added_per_theme[theme] += 1
def _inject_varga_modules(self, modules: Dict[str, Any], add) -> None:
d9 = modules.get("d9_navamsa_expanded") or {}
if isinstance(d9, dict) and d9:
asc = d9.get("Ascendant") or {}
venus = d9.get("Venus") or {}
jupiter = d9.get("Jupiter") or {}
parts = []
if asc:
parts.append(f"D9上升落{asc.get('sign_cn') or asc.get('sign')},主星{asc.get('lord', '未知')}")
if venus:
parts.append(f"Venus在D9落{venus.get('sign_cn') or venus.get('sign')},尊严={venus.get('dignity', '未知')}")
if jupiter:
parts.append(f"Jupiter在D9落{jupiter.get('sign_cn') or jupiter.get('sign')},尊严={jupiter.get('dignity', '未知')}")
add(ThemeName.MARRIAGE, ReportTechniqueResult(
technique="d9_navamsa_expanded",
chart="D9",
conclusion="".join(parts[:3]) or "D9 Navamsa 已完成展开",
sentiment=_sentiment_from_dignities([venus.get("dignity"), jupiter.get("dignity")]),
strength=StrengthLevel.STRONG,
details={"source_module": "d9_navamsa_expanded", "ascendant": asc, "venus": venus, "jupiter": jupiter},
))
varga = modules.get("varga_full") or {}
if not isinstance(varga, dict):
return
mapping = [
("D2_Hora", ThemeName.WEALTH, "D2", "Hora财富分盘"),
("D10_Dasamsa", ThemeName.CAREER, "D10", "Dashamsha事业分盘"),
("D20_Vimsamsa", ThemeName.SPIRITUALITY, "D20", "Vimsamsa灵性分盘"),
("D30_Trimsamsa", ThemeName.HEALTH, "D30", "Trimsamsa健康/灾厄分盘"),
("D60_Shashtyamsa", ThemeName.SPIRITUALITY, "D60", "Shashtyamsha业力分盘"),
]
for key, theme, chart, label in mapping:
data = varga.get(key) or {}
if not isinstance(data, dict) or not data:
continue
asc = data.get("Ascendant") or {}
dignity = data.get("_dignity") or {}
strong = [p for p, d in dignity.items() if str(d).lower() in {"exalted", "own sign", "moolatrikona"}]
weak = [p for p, d in dignity.items() if "debil" in str(d).lower()]
conclusion = f"{label}:上升落{asc.get('sign', '未知')};强势行星{strong[:3] or '未突出'};需留意行星{weak[:3] or '无明显落陷'}"
add(theme, ReportTechniqueResult(
technique=f"varga_full:{key}",
chart=chart,
conclusion=conclusion,
sentiment="positive" if len(strong) > len(weak) else ("negative" if weak else "neutral"),
strength=StrengthLevel.STRONG,
details={"source_module": "varga_full", "varga": key, "ascendant": asc, "dignity": dignity},
))
def _inject_strength_modules(self, modules: Dict[str, Any], add) -> None:
shadbala = modules.get("shadbala") or {}
if isinstance(shadbala, dict) and shadbala:
strongest = shadbala.get("strongest") or []
weakest = shadbala.get("weakest") or []
conclusion = f"Shadbala相对力量:最强={strongest};最弱={weakest}。该模块当前用于相对强弱排序。"
result = ReportTechniqueResult(
technique="shadbala_relative_strength",
chart="D1",
conclusion=conclusion,
sentiment="neutral",
strength=StrengthLevel.MODERATE,
details={"source_module": "shadbala", "strongest": strongest, "weakest": weakest, "status": shadbala.get("status")},
)
for theme in [ThemeName.CAREER, ThemeName.HEALTH, ThemeName.SPIRITUALITY]:
add(theme, result)
av = modules.get("ashtakavarga") or {}
if isinstance(av, dict) and av:
house_scores = av.get("house_scores") or av.get("house_scores_full") or {}
wealth_scores = _pick_house_scores(house_scores, [2, 11])
health_scores = _pick_house_scores(house_scores, [6, 8, 12])
career_scores = _pick_house_scores(house_scores, [10])
for theme, houses, label in [
(ThemeName.WEALTH, wealth_scores, "2/11宫财富"),
(ThemeName.HEALTH, health_scores, "6/8/12宫健康压力"),
(ThemeName.CAREER, career_scores, "10宫事业"),
]:
if not houses:
continue
avg = sum(houses.values()) / len(houses)
add(theme, ReportTechniqueResult(
technique=f"ashtakavarga:{label}",
chart="AV",
conclusion=f"Ashtakavarga {label}分值:{houses},均值约{avg:.1f}",
sentiment="positive" if avg >= 28 else ("negative" if avg <= 24 else "neutral"),
strength=StrengthLevel.MODERATE,
details={"source_module": "ashtakavarga", "scores": houses},
))
def _inject_jaimini_relationship_modules(self, modules: Dict[str, Any], add) -> None:
"""Inject relationship evidence from Jaimini DK and RTN modules.
v6.1.10 connects two previously underused P0 relationship layers:
`modules.jaimini.darakaraka` and `modules.rashi_tulya_navamsa`.
Both are deterministic full-reading outputs; the bridge translates them into
marriage/health/spirituality evidence so thematic reports can narrate them.
"""
jaimini = modules.get("jaimini") or {}
darakaraka = jaimini.get("darakaraka") if isinstance(jaimini, dict) else None
if isinstance(darakaraka, dict) and darakaraka and not darakaraka.get("error"):
score = darakaraka.get("marriage_quality_score")
score_text = f",质量评分约{score:.0%}" if isinstance(score, (int, float)) else ""
conclusion = (
f"Darakaraka{darakaraka.get('dk_planet', '未知')}{darakaraka.get('dk_sign', '未知')}"
f"{darakaraka.get('dk_house', '?')}宫,伴侣原型={darakaraka.get('core_profile', '未标注')}{score_text}"
)
if darakaraka.get("d9_sign"):
conclusion += f" D9中落{darakaraka.get('d9_sign')},尊严={darakaraka.get('d9_dignity', '未知')}"
add(ThemeName.MARRIAGE, ReportTechniqueResult(
technique="jaimini:darakaraka_deep_reader",
chart="D1/D9",
conclusion=conclusion,
sentiment="positive" if isinstance(score, (int, float)) and score >= 0.6 else ("negative" if isinstance(score, (int, float)) and score < 0.4 else "neutral"),
strength=StrengthLevel.STRONG,
details={"source_module": "jaimini.darakaraka", "raw": darakaraka},
))
rtn_module = modules.get("rashi_tulya_navamsa") or {}
rtn = rtn_module.get("analysis") if isinstance(rtn_module, dict) and isinstance(rtn_module.get("analysis"), dict) else rtn_module
if isinstance(rtn, dict) and rtn and not rtn.get("error"):
house_mapping = rtn.get("house_mapping") or {}
curse_yogas = rtn.get("curse_yogas") or []
strength_score = rtn.get("strength_score")
weakness_score = rtn.get("weakness_score")
marriage_planets = house_mapping.get(7) or house_mapping.get("7") or []
summary_parts = []
if marriage_planets:
summary_parts.append(f"RTN第7宫映射行星={marriage_planets}")
if curse_yogas:
summary_parts.append(f"检测到{len(curse_yogas)}个凶星合相命名")
if isinstance(strength_score, (int, float)) and isinstance(weakness_score, (int, float)):
summary_parts.append(f"隐藏力量={strength_score:.0%},弱点={weakness_score:.0%}")
conclusion = "".join(summary_parts) or "Rashi Tulya Navamsa 已完成 D1/D9 映射分析。"
sentiment = "negative" if curse_yogas or (isinstance(weakness_score, (int, float)) and weakness_score > strength_score) else "positive"
add(ThemeName.MARRIAGE, ReportTechniqueResult(
technique="rashi_tulya_navamsa:relationship_mapping",
chart="D1/D9",
conclusion=conclusion,
sentiment=sentiment,
strength=StrengthLevel.MODERATE,
details={"source_module": "rashi_tulya_navamsa", "raw": rtn},
))
if curse_yogas:
add(ThemeName.HEALTH, ReportTechniqueResult(
technique="rashi_tulya_navamsa:curse_yogas",
chart="D1/D9",
conclusion=f"RTN凶星合相提示健康/危机压力:{[c.get('name') for c in curse_yogas[:3]]}",
sentiment="negative",
strength=StrengthLevel.MODERATE,
details={"source_module": "rashi_tulya_navamsa", "curse_yogas": curse_yogas},
))
guna = rtn.get("guna_narrative")
if guna:
add(ThemeName.SPIRITUALITY, ReportTechniqueResult(
technique="rashi_tulya_navamsa:guna_balance",
chart="D1/D9",
conclusion=f"RTN Gunas{guna}",
sentiment="neutral",
strength=StrengthLevel.MODERATE,
details={"source_module": "rashi_tulya_navamsa", "guna_balance": rtn.get("guna_balance")},
))
def _inject_special_points(self, modules: Dict[str, Any], add) -> None:
special = modules.get("special_lagnas") or {}
if isinstance(special, dict):
upapada = special.get("Upapada_Lagna") or {}
a10 = special.get("A10_Karma_Pada") or {}
arudha = special.get("Arudha_Lagna") or {}
if upapada:
add(ThemeName.MARRIAGE, ReportTechniqueResult(
technique="special_lagnas:upapada_lagna",
chart="D1",
conclusion=f"Upapada Lagna落{upapada.get('sign', '未知')}{upapada.get('house', '?')}宫,提示婚姻的社会呈现与伴侣形象。",
sentiment="neutral",
strength=StrengthLevel.STRONG,
details={"source_module": "special_lagnas", "upapada_lagna": upapada},
))
if a10:
add(ThemeName.CAREER, ReportTechniqueResult(
technique="special_lagnas:a10_karma_pada",
chart="D1",
conclusion=f"A10/Karma Pada落{a10.get('sign', '未知')}{a10.get('house', '?')}宫,用于观察事业名声与业力方向。",
sentiment="neutral",
strength=StrengthLevel.STRONG,
details={"source_module": "special_lagnas", "a10_karma_pada": a10},
))
if arudha:
add(ThemeName.CAREER, ReportTechniqueResult(
technique="special_lagnas:arudha_lagna",
chart="D1",
conclusion=f"Arudha Lagna落{arudha.get('sign', '未知')}{arudha.get('house', '?')}宫,显示公众形象与外界评价。",
sentiment="neutral",
strength=StrengthLevel.MODERATE,
details={"source_module": "special_lagnas", "arudha_lagna": arudha},
))
vivah = modules.get("vivah_saham") or {}
if isinstance(vivah, dict) and vivah:
point = vivah.get("vivah_saham") or vivah
add(ThemeName.MARRIAGE, ReportTechniqueResult(
technique="vivah_saham",
chart="Tajika",
conclusion=f"Vivah Saham婚姻敏感点落{point.get('sign', vivah.get('saham_sign', '未知'))}{point.get('house', vivah.get('saham_house', '?'))}宫;适合与Transit触发联合判断婚恋窗口。",
sentiment="neutral",
strength=StrengthLevel.MODERATE,
details={"source_module": "vivah_saham", "raw": vivah},
))
def _inject_dasa_convergence_module(self, modules: Dict[str, Any], add) -> None:
convergence = modules.get("dasa_convergence") or {}
if not isinstance(convergence, dict) or not convergence:
return
# 从 full-reading 的 Vimshottari current antar 注入真实时间锚,避免报告退回模拟 TimingAnchorBuilder。
systems_summary = convergence.get("systems_summary") or {}
vim = systems_summary.get("vimshottari") or {}
antar = vim.get("antar") or {}
if isinstance(antar, dict):
start_year = _year_from_date(antar.get("start"), datetime.now().year)
end_year = _year_from_date(antar.get("end"), start_year + 3)
maha = vim.get("maha") or "Unknown"
antar_lord = antar.get("lord") or "Unknown"
anchor = TimingAnchor(
dasha_period=f"Vimshottari-{maha}-{antar_lord}",
start_year=start_year,
end_year=end_year,
activation_description="full-reading 五系统 Dasa Convergence 已完成,当前 Vimshottari 主/副运作为主题报告时间锚。",
is_current=bool(antar.get("is_current", True)),
)
for tn in ThemeName:
self.rpo.add_timing(tn, anchor)
domain_map = {
"marriage_partnership": ThemeName.MARRIAGE,
"career_status": ThemeName.CAREER,
"wealth_family": ThemeName.WEALTH,
"health_service": ThemeName.HEALTH,
"fortune_dharma": ThemeName.SPIRITUALITY,
"transformation": ThemeName.SPIRITUALITY,
}
domains = convergence.get("domain_activations") or {}
for domain, data in domains.items():
theme = domain_map.get(domain)
if not theme or not isinstance(data, dict):
continue
activations = data.get("activations") or []
systems = sorted({a.get("system") for a in activations if isinstance(a, dict) and a.get("system")})
conclusion = f"Dasa Convergence{domain} 获得{data.get('system_count', len(systems))}个系统激活({', '.join(systems) or '未列出'}),收敛等级{data.get('convergence_level', '未知')},概率{data.get('probability', '未标注')}"
add(theme, ReportTechniqueResult(
technique=f"dasa_convergence:{domain}",
chart="Dasha",
conclusion=conclusion,
sentiment="positive" if data.get("system_count", 0) >= 2 else "neutral",
strength=StrengthLevel.STRONG if data.get("system_count", 0) >= 3 else StrengthLevel.MODERATE,
details={"source_module": "dasa_convergence", "domain": domain, "raw": data},
))
def _inject_transit_module(self, modules: Dict[str, Any], add) -> None:
transit = modules.get("transit_multi_reference") or {}
if not isinstance(transit, dict) or not transit:
return
target_date = transit.get("target_date")
analysis = transit.get("transit_analysis") or {}
for planet in ["Jupiter", "Saturn", "Rahu", "Ketu"]:
pdata = analysis.get(planet)
if not isinstance(pdata, dict):
continue
house_map = pdata.get("house_from_ref") or {}
lagna_house = (house_map.get("Lagna") or {}).get("house")
if not lagna_house:
continue
themes = _themes_for_transit_house(int(lagna_house))
for theme in themes:
add(theme, ReportTechniqueResult(
technique=f"transit_multi_reference:{planet}",
chart="Transit",
conclusion=f"{target_date or '当前'}真实过境:{planet}从Lagna参考点落第{lagna_house}宫,并需结合Chandra/Arudha/Navamsa多参考点校验。",
sentiment="positive" if planet == "Jupiter" and lagna_house in [2, 5, 7, 9, 10, 11] else ("negative" if planet in ["Saturn", "Rahu", "Ketu"] and lagna_house in [6, 8, 12] else "neutral"),
strength=StrengthLevel.MODERATE,
details={"source_module": "transit_multi_reference", "planet": planet, "raw": pdata},
))
# ── 端到端报告生成 ──
def generate_full_report(
self,
chart_data: Dict[str, Any],
themes: Optional[List[ReadingTheme]] = None,
) -> Dict[str, Any]:
"""
端到端报告生成:从星盘数据 → ReadingChapter → ThemeReport。
Args:
chart_data: 星盘完整数据
themes: 要分析的主题列表,None=全部5大主题
Returns:
{
"reading_chapters": Dict[ReadingTheme, List[ReadingChapter]],
"theme_reports": Dict[ThemeName, ThemeReport],
"unified_narrative": str,
"dasha_results": Dict[str, Any],
}
"""
if themes is None:
themes = [
ReadingTheme.MARRIAGE,
ReadingTheme.CAREER,
ReadingTheme.WEALTH,
ReadingTheme.HEALTH,
ReadingTheme.SPIRITUAL,
]
# Step 0: 注入推运结果与 full-reading 真实模块(在所有主题分析之前)
dasha_results = self.inject_dasha_results(chart_data)
full_reading_injection = self.inject_full_reading_modules(chart_data)
reading_chapters: Dict[str, List[ReadingChapter]] = {}
theme_reports: Dict[str, Any] = {}
for rt in themes:
# Step 1: reading_orchestrator 执行技法
chapters = self.ro.analyze(chart_data, rt)
reading_chapters[rt.value] = chapters
# Step 2: 映射到 ThemeName
tn = THEME_MAPPING.get(rt)
if not tn:
continue
# Step 3: 将 chapters 转换为 TechniqueResults 并喂给 report_orchestrator
for ch in chapters:
tech_results = self.chapter_to_technique_results(ch)
for tr in tech_results:
self.rpo.add_technique(tn, tr)
# Step 4: 生成 ThemeReport
report = self.rpo.generate_report(tn)
theme_reports[tn.value] = report.to_dict()
# Step 5: 生成统一叙事
unified = self._build_unified_narrative(theme_reports)
return {
"reading_chapters": {
k: [self._chapter_to_dict(ch) for ch in v]
for k, v in reading_chapters.items()
},
"theme_reports": theme_reports,
"unified_narrative": unified,
"dasha_results": dasha_results,
"full_reading_injection": full_reading_injection,
}
# ── 内部辅助 ──
@staticmethod
def _build_unified_narrative(theme_reports: Dict[str, Any]) -> str:
"""将多个 ThemeReport 拼接为统一的人生叙事。"""
parts = []
order = ["marriage", "career", "wealth", "health", "spirituality"]
for theme_key in order:
if theme_key in theme_reports:
r = theme_reports[theme_key]
parts.append(f"\n## {r.get('summary', theme_key)}")
parts.append(r.get("narrative", ""))
if r.get("recommendations"):
parts.append("\n**建议:**")
for rec in r["recommendations"]:
parts.append(f"- {rec}")
return "\n".join(parts)
@staticmethod
def _chapter_to_dict(ch: ReadingChapter) -> Dict[str, Any]:
return {
"title": ch.title,
"subtitle": ch.subtitle,
"priority": ch.priority,
"techniques_used": ch.techniques_used,
"findings": ch.findings,
"conflicts": [
{
"dimension": c.dimension,
"d1_finding": c.d1_finding,
"d9_finding": c.d9_finding,
"resolution": c.resolution,
"narrative": c.narrative,
}
for c in ch.conflicts
],
"narrative": ch.narrative,
"actionable": ch.actionable,
"timing": ch.timing,
}
# ═══════════════════════════════════════════════════════════════
# 辅助函数
# ═══════════════════════════════════════════════════════════════
def _extract_modules(chart_data: Any) -> Dict[str, Any]:
"""Return full-reading modules from either a full report dict or a modules dict."""
if not isinstance(chart_data, dict):
return {}
modules = chart_data.get("modules")
if isinstance(modules, dict):
return modules
# Allow passing modules directly in tests.
known = {
"yoga", "varga_full", "d9_navamsa_expanded", "ashtakavarga", "shadbala",
"jaimini", "rashi_tulya_navamsa", "special_lagnas", "vivah_saham",
"dasa_convergence", "transit_multi_reference",
"dasha", "yogini_dasha", "ashtottari_dasha", "kalachakra_dasha",
}
if any(k in chart_data for k in known):
return chart_data
return {}
def _strength_from_text(value: Any, default: StrengthLevel = StrengthLevel.MODERATE) -> StrengthLevel:
text = str(value or "").lower()
if any(x in text for x in ["", "strong", "high", ""]):
return StrengthLevel.STRONG
if any(x in text for x in ["", "weak", "low"]):
return StrengthLevel.WEAK
return default
def _sentiment_from_yoga(yoga: Dict[str, Any]) -> str:
category = str(yoga.get("category") or yoga.get("name") or yoga.get("name_cn") or "").lower()
negative_tokens = ["darid", "dharidhra", "kapata", "arista", "dosha", "curse", "poverty", "roga", "mrityu", "", "", "", "", ""]
positive_tokens = ["raja", "dhana", "lakshmi", "gaja", "kesari", "amala", "yoga", "财富", "", "", "成就"]
if any(t in category for t in negative_tokens):
return "negative"
if any(t in category for t in positive_tokens):
return "positive"
return "neutral"
def _themes_for_yoga(yoga: Dict[str, Any]) -> List[ThemeName]:
text = " ".join(str(yoga.get(k, "")) for k in ["name", "name_cn", "category", "rule_id", "combination"])
effects = yoga.get("effects") or []
if isinstance(effects, list):
text += " " + " ".join(str(e) for e in effects)
lower = text.lower()
themes: List[ThemeName] = []
keyword_map = [
(ThemeName.WEALTH, ["dhana", "lakshmi", "wealth", "money", "income", "财富", "", "收入", "资源"]),
(ThemeName.CAREER, ["raja", "career", "profession", "status", "authority", "power", "事业", "权力", "地位", "名声", "成就"]),
(ThemeName.MARRIAGE, ["marriage", "spouse", "venus", "vivah", "partner", "", "配偶", "伴侣", "金星"]),
(ThemeName.HEALTH, ["arista", "roga", "disease", "health", "illness", "saturn-mars", "", "健康", "", ""]),
(ThemeName.SPIRITUALITY, ["moksha", "jupiter", "ketu", "spiritual", "dharma", "智慧", "灵性", "木星", "解脱"]),
]
for theme, keywords in keyword_map:
if any(k in lower or k in text for k in keywords):
themes.append(theme)
if not themes and yoga.get("category") in ["raja", "bvr"]:
themes.append(ThemeName.CAREER)
return themes[:3]
def _sentiment_from_dignities(values: List[Any]) -> str:
text = " ".join(str(v or "").lower() for v in values)
if any(k in text for k in ["exalted", "own", "moola", "friend"]):
return "positive"
if any(k in text for k in ["debil", "enemy"]):
return "negative"
return "neutral"
def _pick_house_scores(house_scores: Any, houses: List[int]) -> Dict[int, float]:
if not isinstance(house_scores, dict):
return {}
picked: Dict[int, float] = {}
for house in houses:
value = None
for key in [house, str(house), f"house_{house}", f"H{house}"]:
if key in house_scores:
value = house_scores[key]
break
if isinstance(value, dict):
value = value.get("score") or value.get("bindus") or value.get("sav")
if isinstance(value, (int, float)):
picked[house] = float(value)
return picked
def _year_from_date(value: Any, default: int) -> int:
if isinstance(value, int):
return value
if isinstance(value, str) and len(value) >= 4:
try:
return int(value[:4])
except ValueError:
return default
return default
def _themes_for_transit_house(house: int) -> List[ThemeName]:
mapping = {
1: [ThemeName.HEALTH],
2: [ThemeName.WEALTH],
5: [ThemeName.WEALTH, ThemeName.SPIRITUALITY],
6: [ThemeName.HEALTH, ThemeName.CAREER],
7: [ThemeName.MARRIAGE],
8: [ThemeName.HEALTH, ThemeName.SPIRITUALITY],
9: [ThemeName.SPIRITUALITY, ThemeName.CAREER],
10: [ThemeName.CAREER],
11: [ThemeName.WEALTH, ThemeName.CAREER],
12: [ThemeName.HEALTH, ThemeName.SPIRITUALITY],
}
return mapping.get(house, [])
def _infer_sentiment(text: str) -> str:
"""从 finding 文本推断 sentiment。"""
negative_keywords = [
"阻碍", "困难", "挑战", "风险", "不利", "", "", "", "",
"冲突", "矛盾", "破坏", "损失", "障碍", "延迟", "问题",
]
positive_keywords = [
"有利", "", "", "", "助力", "支持", "机遇", "突破",
"成就", "成功", "和谐", "稳定", "增益", "提升",
]
text_lower = text.lower()
neg_score = sum(1 for w in negative_keywords if w in text_lower)
pos_score = sum(1 for w in positive_keywords if w in text_lower)
if neg_score > pos_score:
return "negative"
if pos_score > neg_score:
return "positive"
return "neutral"
def _infer_strength(text: str, conflicts: List[Any]) -> StrengthLevel:
"""从 finding 文本和冲突情况推断 strength。"""
strong_keywords = ["非常", "极强", "显著", "明确", "主导", "绝对"]
weak_keywords = ["轻微", "略有", "潜在", "可能", "模糊", "微弱"]
text_lower = text.lower()
if any(w in text_lower for w in strong_keywords):
return StrengthLevel.STRONG
if any(w in text_lower for w in weak_keywords):
return StrengthLevel.WEAK
if conflicts:
return StrengthLevel.MODERATE
return StrengthLevel.MODERATE
# ═══════════════════════════════════════════════════════════════
# CLI / 测试
# ═══════════════════════════════════════════════════════════════
def demo():
"""演示桥接功能。
默认使用 MockDataFactory;如果传入一个 full-reading JSON 路径,则直接消费
`modules` 真实结果,验证 v6.1.8 的真实模块接线。
"""
import sys
from report_orchestrator import MockDataFactory
# 创建星盘数据:优先使用 full-reading JSON,否则使用模拟数据
if len(sys.argv) > 1:
with open(sys.argv[1], "r", encoding="utf-8") as f:
chart_data = json.load(f)
else:
chart_data = MockDataFactory.create_sample_chart()
# 创建 orchestrators
# NOTE: reading_orchestrator 需要 registry,这里用空 registry 演示;真实证据由 full-reading 注入层提供
class DummyRegistry:
def get(self, name):
return None
ro = ReadingOrchestrator(DummyRegistry())
rpo = ReportOrchestrator(chart_data)
# 桥接
bridge = OrchestratorBridge(ro, rpo)
result = bridge.generate_full_report(chart_data)
print("=" * 60)
print("Orchestrator Bridge Demo")
print("=" * 60)
print(f"\n生成主题报告数: {len(result['theme_reports'])}")
for theme, report in result["theme_reports"].items():
print(f"\n [{theme}]")
print(f" 总结: {report.get('summary', 'N/A')}")
print(f" 强度: {report.get('strength', 'N/A')}")
print(f" 证据数: {len(report.get('evidence', []))}")
print("\n" + "=" * 60)
print("统一叙事(前500字):")
print("=" * 60)
print(result["unified_narrative"][:500] + "...")
if __name__ == "__main__":
demo()