Files
Jyotisha/scripts/orchestrator_bridge.py
T
732642856 067b400625 v6.1.5: Integrate Ashtottari/Yogini/Kalachakra Dasha into report pipeline
- Register 3 dasha systems in technique_registry.json
- Extend orchestrator_bridge.py with inject_dasha_results()
- Add add_timing() to ThematicReportOrchestrator
- Dashas auto-convert to TimingAnchor and inject into all 5 themes
- Verified: Yogini + Kalachakra timing anchors flow into ThemeReport
2026-06-07 15:17:09 +08:00

571 lines
21 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.
#!/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:
pass
# 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:
pass
# 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:
pass
# 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)
# ── 端到端报告生成 ──
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: 注入推运结果(在所有主题分析之前)
dasha_results = self.inject_dasha_results(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,
}
# ── 内部辅助 ──
@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 _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():
"""演示桥接功能(使用模拟数据)。"""
from report_orchestrator import MockDataFactory
# 创建模拟星盘数据
chart_data = MockDataFactory.create_sample_chart()
# 创建 orchestrators
# NOTE: reading_orchestrator 需要 registry,这里用空 registry 演示
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()