From cdd0f51da51dd8fb464697a0313cbe358919aa97 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Thu, 2 Jul 2026 20:12:43 +0800 Subject: [PATCH] Classify VedAstro capability catalog domains --- scripts/vedastro_evidence_orchestrator.py | 37 +++++ .../vedastro_official_capability_runner.py | 97 ++++++++++++- scripts/vedastro_service_adapter.py | 5 +- tests/test_vedastro_evidence_orchestrator.py | 67 +++++++++ ...est_vedastro_official_capability_runner.py | 131 ++++++++++++++++++ .../test_vedastro_service_adapter_executor.py | 4 +- 6 files changed, 331 insertions(+), 10 deletions(-) diff --git a/scripts/vedastro_evidence_orchestrator.py b/scripts/vedastro_evidence_orchestrator.py index a1aa804b..56e77e3f 100644 --- a/scripts/vedastro_evidence_orchestrator.py +++ b/scripts/vedastro_evidence_orchestrator.py @@ -31,6 +31,12 @@ ROUTE_DOMAIN_MAP = { "career": ["career"], "finance": ["wealth"], "wealth": ["wealth"], + "health": ["health"], + "education": ["education"], + "property": ["property"], + "children": ["children"], + "migration": ["migration"], + "prashna": ["prashna"], "rectification": ["marriage", "career", "wealth"], "timing": ["career", "marriage", "wealth"], "general": ["career", "marriage", "wealth"], @@ -64,6 +70,36 @@ ROUTE_THEME_REQUIREMENTS = { "requires_dual_dasha": True, "required_local_supplements": ["wealth_structure_explainer", "narayana_dasha", "functional_benefic_malefic"], }, + "health": { + "route": "health", + "requires_dual_dasha": True, + "required_local_supplements": ["d30_health_axis", "sixth_eighth_twelfth_houses", "functional_benefic_malefic"], + }, + "education": { + "route": "education", + "requires_dual_dasha": True, + "required_local_supplements": ["d24_learning_axis", "fifth_ninth_houses", "functional_benefic_malefic"], + }, + "property": { + "route": "property", + "requires_dual_dasha": True, + "required_local_supplements": ["d4_property_axis", "fourth_house", "functional_benefic_malefic"], + }, + "children": { + "route": "children", + "requires_dual_dasha": True, + "required_local_supplements": ["d7_children_axis", "fifth_house", "functional_benefic_malefic"], + }, + "migration": { + "route": "migration", + "requires_dual_dasha": True, + "required_local_supplements": ["d4_d9_foreign_axis", "ninth_twelfth_houses", "functional_benefic_malefic"], + }, + "prashna": { + "route": "prashna", + "requires_dual_dasha": False, + "required_local_supplements": ["prashna_chart", "question_context_required", "functional_benefic_malefic"], + }, "overview": { "route": "overview", "requires_dual_dasha": True, @@ -120,6 +156,7 @@ def orchestrate_vedastro_evidence( window_start, window_end = (start_date, end_date) if start_date and end_date else _default_window(reference_date) case = _normalize_case(birth_payload) case["reference_date"] = str(reference_date or window_start)[:10] + case["themes"] = list(domains) domain_reports: dict[str, Any] = {} evidence_ledger: list[dict[str, Any]] = [] top_events_by_domain: dict[str, Any] = {} diff --git a/scripts/vedastro_official_capability_runner.py b/scripts/vedastro_official_capability_runner.py index e0748c67..aacf59db 100644 --- a/scripts/vedastro_official_capability_runner.py +++ b/scripts/vedastro_official_capability_runner.py @@ -22,7 +22,21 @@ CATALOG_STUB_ENV = "VEDASTRO_OFFICIAL_CAPABILITY_CATALOG_STUB" PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu", "Ascendant"] HOUSES = [f"House{i}" for i in range(1, 13)] DEFAULT_SIGIL_SAMPLE_LIMIT = int(os.environ.get("VEDASTRO_FULL_CATALOG_SAMPLE_LIMIT", "0") or 0) -DOMAIN_ORDER = ["career", "marriage", "wealth", "rectification", "timing", "general"] +DOMAIN_ORDER = [ + "career", + "marriage", + "wealth", + "health", + "education", + "property", + "children", + "migration", + "prashna", + "rectification", + "timing", + "general", + "unknown", +] DEFAULT_DYNAMIC_THEMES = ["career", "marriage", "wealth", "rectification", "timing"] POLICY_BUCKETS = { "needs_user_context": "needs_user_context_methods", @@ -74,7 +88,25 @@ def _domain_routing_for_method(method: str, capability: dict[str, Any], paramete if any(token in text for token in ("wealth", "money", "finance", "income", "gain", "house2", "house11", "ashtakvarga")): domains.add("wealth") priority = "high" if priority == "low" else priority - if any(token in text for token in ("birth", "rectification", "appearance", "body", "height", "shape", "complexion")): + if any(token in text for token in ("health", "illness", "disease", "medical", "medicine", "hospital", "accident", "injury", "surgery")): + domains.add("health") + priority = "high" if any(token in text for token in ("health", "disease", "illness", "accident")) else priority + if any(token in text for token in ("education", "school", "college", "degree", "study", "studies", "learning", "exam")): + domains.add("education") + priority = "high" if any(token in text for token in ("education", "degree", "exam")) else priority + if any(token in text for token in ("property", "vehicle", "home", "house4", "house 4", "land", "realestate", "real estate", "residence")): + domains.add("property") + priority = "high" if any(token in text for token in ("property", "vehicle", "land")) else priority + if any(token in text for token in ("children", "child", "progeny", "putra", "pregnancy", "fertility")): + domains.add("children") + priority = "high" if any(token in text for token in ("children", "progeny", "pregnancy")) else priority + if any(token in text for token in ("foreign", "travel", "migration", "relocation", "abroad", "immigration", "journey")): + domains.add("migration") + priority = "high" if any(token in text for token in ("migration", "relocation", "abroad")) else priority + if any(token in text for token in ("prashna", "horary", "questiontext", "question text", "muhurta")): + domains.add("prashna") + priority = "high" if any(token in text for token in ("prashna", "horary")) else priority + if any(token in text for token in ("rectification", "appearance", "body", "height", "shape", "complexion")): domains.add("rectification") if priority == "low": priority = "medium" @@ -83,9 +115,6 @@ def _domain_routing_for_method(method: str, capability: dict[str, Any], paramete if priority == "low": priority = "medium" - if not domains: - domains.add("general") - if parameter_strategy in {"requires_user_context", "requires_user_text", "requires_rectification_profile"}: execution_policy = { "requires_user_context": "needs_user_context", @@ -97,14 +126,46 @@ def _domain_routing_for_method(method: str, capability: dict[str, Any], paramete else: execution_policy = "auto" + if not domains: + domains.add("unknown" if execution_policy == "blocked" else "general") + + blocked_reason = None + if execution_policy == "blocked": + blocked_reason = parameter_strategy + elif execution_policy == "needs_user_context": + blocked_reason = "requires_additional_user_context" + elif execution_policy == "needs_user_text": + blocked_reason = "requires_user_text_or_question" + elif execution_policy == "needs_rectification_profile": + blocked_reason = "requires_rectification_profile" + ordered_domains = [domain for domain in DOMAIN_ORDER if domain in domains] return { "domains": ordered_domains, "execution_policy": execution_policy, "priority": priority, + "adjudicator_use": _adjudicator_use(execution_policy, priority), + "confidence_role": _confidence_role(execution_policy, priority), + "blocked_reason": blocked_reason, } +def _adjudicator_use(execution_policy: str, priority: str) -> str: + if execution_policy == "auto": + return "primary_candidate" if priority == "high" else "secondary_context" + if execution_policy in {"needs_user_context", "needs_user_text", "needs_rectification_profile"}: + return "secondary_context" + return "not_used" + + +def _confidence_role(execution_policy: str, priority: str) -> str: + if execution_policy == "auto": + return "confidence_support" if priority == "high" else "background_reference" + if execution_policy in {"needs_user_context", "needs_user_text", "needs_rectification_profile"}: + return "confidence_cap_until_context_available" + return "blocked" + + def _build_domain_routing(method_statuses: dict[str, Any]) -> dict[str, Any]: routing: dict[str, dict[str, Any]] = {} for method, status in method_statuses.items(): @@ -161,6 +222,13 @@ def _requested_dynamic_themes(payload: dict[str, Any]) -> list[str]: "婚恋": "marriage", "婚姻": "marriage", "财富": "wealth", + "健康": "health", + "教育": "education", + "房产": "property", + "子女": "children", + "迁移": "migration", + "问卜": "prashna", + "卜卦": "prashna", "校时": "rectification", "应期": "timing", } @@ -217,6 +285,9 @@ def _capability_reference(method: str, status: dict[str, Any], theme: str) -> di "status": status.get("status"), "execution_policy": status.get("execution_policy"), "priority": status.get("priority"), + "adjudicator_use": status.get("adjudicator_use"), + "confidence_role": status.get("confidence_role"), + "blocked_reason": status.get("blocked_reason"), "domains": status.get("domains") or [], "bucket": status.get("bucket"), "signature": status.get("signature"), @@ -537,7 +608,7 @@ def _full_catalog_method_payload(method: str, capability: dict[str, Any], case: return None, "requires_user_context" if any(name in lowered for name in ("bodyheight", "bodyshape", "hair", "lips", "nose", "complexion", "faceshape", "constitution", "personality")): return None, "requires_rectification_profile" - if any(name in lowered for name in ("rawtextdata", "birthdatarawtext", "inputtext", "textinput", "query", "fullname", "personfullname", "address", "locationname", "ipaddress")): + if any(name in lowered for name in ("rawtextdata", "birthdatarawtext", "inputtext", "textinput", "query", "questiontext", "question", "fullname", "personfullname", "address", "locationname", "ipaddress")): return None, "requires_user_text" return None, "unsupported_signature" @@ -624,6 +695,18 @@ def run_full_capability_catalog(birth_payload: dict[str, Any]) -> dict[str, Any] domain_routing = _build_domain_routing(method_statuses) requested_themes = _requested_dynamic_themes(birth_payload) dynamic_selection = _build_dynamic_selection(method_statuses, domain_routing, requested_themes) + unknown_method_count = sum( + 1 + for status in method_statuses.values() + if isinstance(status, dict) and status.get("domains") == ["unknown"] + ) + misrouted_general_method_count = sum( + 1 + for status in method_statuses.values() + if isinstance(status, dict) + and "general" in (status.get("domains") or []) + and len(status.get("domains") or []) > 1 + ) return { "runner": "vedastro_official_capability_runner", @@ -639,6 +722,8 @@ def run_full_capability_catalog(birth_payload: dict[str, Any]) -> dict[str, Any] "ok_method_count": ok_count, "unsupported_method_count": unsupported_count, "blocked_method_count": blocked_count, + "unknown_method_count": unknown_method_count, + "misrouted_general_method_count": misrouted_general_method_count, "sample_limit": sample_limit, "domain_routing_count": len(domain_routing), "dynamic_selection_theme_count": len(dynamic_selection), diff --git a/scripts/vedastro_service_adapter.py b/scripts/vedastro_service_adapter.py index e8b88524..71752352 100644 --- a/scripts/vedastro_service_adapter.py +++ b/scripts/vedastro_service_adapter.py @@ -2417,6 +2417,7 @@ def _run_official_full_snapshot_case(case: dict[str, Any], case_id: str = "user_ "reference_date": case.get("reference_date") or case.get("today") or case.get("transit_date") or case.get("current_date"), "dasha_levels": case.get("dasha_levels"), "dasha_precision_hours": case.get("dasha_precision_hours"), + "themes": case.get("themes") or case.get("theme"), } manifest = _official_full_snapshot_manifest(user_case, case_id) budget_started_at = time.monotonic() @@ -2441,7 +2442,8 @@ def _run_official_full_snapshot_case(case: dict[str, Any], case_id: str = "user_ and official_python_bundle.get("status") != "official_snapshot_budget_exhausted" ): official_python_bundle = _try_official_python_bridge_snapshot_bundle(user_case) - if time.monotonic() - budget_started_at < _timeout_seconds(): + budget_exhausted = official_python_bundle.get("status") == "official_snapshot_budget_exhausted" + if not budget_exhausted and time.monotonic() - budget_started_at < _timeout_seconds(): official_full_capability_catalog = _try_official_full_capability_catalog_bundle(user_case) else: official_full_capability_catalog = { @@ -2470,7 +2472,6 @@ def _run_official_full_snapshot_case(case: dict[str, Any], case_id: str = "user_ ) endpoint = os.environ.get("VEDASTRO_API_ENDPOINT", "").strip() network_enabled = os.environ.get(ALLOW_NETWORK_ENV, "").strip().lower() in {"1", "true", "yes"} - budget_exhausted = official_python_bundle.get("status") == "official_snapshot_budget_exhausted" if budget_exhausted and endpoint and network_enabled and _is_official_public_endpoint(endpoint): result = { "backend": "vedastro_service_adapter_candidate", diff --git a/tests/test_vedastro_evidence_orchestrator.py b/tests/test_vedastro_evidence_orchestrator.py index af771ebe..e636aced 100644 --- a/tests/test_vedastro_evidence_orchestrator.py +++ b/tests/test_vedastro_evidence_orchestrator.py @@ -190,3 +190,70 @@ def test_vedastro_orchestrator_surfaces_daily_windows_by_domain(monkeypatch) -> assert result["daily_windows_by_domain"]["career"][0]["date"] == "2026-07-18" assert result["top_daily_window_by_domain"]["career"]["score"] == 5 + + +def test_vedastro_orchestrator_passes_non_core_themes_to_official_catalog(monkeypatch) -> None: + from scripts import vedastro_evidence_orchestrator as orchestrator + + seen_snapshot_cases = [] + seen_scan_domains = [] + + def fake_snapshot(case, *, case_id="user_chart"): + seen_snapshot_cases.append(case) + return { + "status": "partial", + "available": True, + "section_statuses": {}, + "source_metadata": { + "official_full_capability_catalog": { + "status": "partial", + "summary": {"catalog_method_count": 641, "unknown_method_count": 0}, + "domain_routing": { + "health": {"method_count": 3, "auto_method_count": 1, "high_priority_methods": ["HealthProblemEvent"]}, + }, + "dynamic_selection": { + "health": { + "requested_theme": "health", + "selected_methods": [ + { + "method": "HealthProblemEvent", + "citation_id": "vedastro:health:HealthProblemEvent", + "execution_policy": "auto", + } + ], + "report_reference": { + "theme": "health", + "citation_ids": ["vedastro:health:HealthProblemEvent"], + "auto_count": 1, + }, + } + }, + }, + }, + } + + def fake_scan(case, domain, start_date, end_date, case_id): + seen_scan_domains.append(domain) + return { + "status": "unsupported_range_scan_domain", + "available": False, + "reason": f"Unsupported range scan domain: {domain}", + "event_count": 0, + "evidence_ledger": [], + } + + monkeypatch.setattr(orchestrator, "run_official_full_snapshot_for_case", fake_snapshot) + monkeypatch.setattr(orchestrator, "run_range_scan_for_case", fake_scan) + + result = orchestrator.orchestrate_vedastro_evidence( + {"year": REDACTED_YEAR, "month": 4, "day": 17, "hour": 14, "minute": 49, "lat": 36.42, "lon": 114.2, "tz": 8}, + route="health", + reference_date="2026-06-30", + ) + + assert seen_snapshot_cases[0]["themes"] == ["health"] + assert seen_scan_domains == ["health"] + assert result["source_metadata"]["official_report_references"]["health"]["citation_ids"] == [ + "vedastro:health:HealthProblemEvent" + ] + assert result["source_metadata"]["domain_statuses"]["health"] == "unsupported_range_scan_domain" diff --git a/tests/test_vedastro_official_capability_runner.py b/tests/test_vedastro_official_capability_runner.py index 3ebb3838..25719fd7 100644 --- a/tests/test_vedastro_official_capability_runner.py +++ b/tests/test_vedastro_official_capability_runner.py @@ -374,3 +374,134 @@ def test_official_capability_runner_builds_dynamic_theme_selection_and_citations assert marriage["needs_user_context_methods"][0]["method"] == "MatchReport" assert marriage["needs_user_context_methods"][0]["citation_id"] == "vedastro:marriage:MatchReport" assert marriage["report_reference"]["needs_user_context_count"] == 1 + + +def test_full_catalog_classification_marks_non_core_domains_and_unknowns() -> None: + completed = subprocess.run( + [ + sys.executable, + "scripts/vedastro_official_capability_runner.py", + "--bundle", + "official_full_capability_catalog", + "--birth-json", + json.dumps( + { + "year": REDACTED_YEAR, + "month": 4, + "day": 17, + "hour": 14, + "minute": 49, + "lat": 36.42, + "lon": 114.2, + "tz": 8, + "reference_date": "2026-06-29", + "themes": ["health", "education", "property", "children", "migration", "prashna"], + } + ), + ], + cwd=ROOT, + text=True, + capture_output=True, + timeout=120, + check=False, + env={ + **dict(**__import__("os").environ), + "VEDASTRO_OFFICIAL_CAPABILITY_CATALOG_STUB": json.dumps( + { + "available": True, + "status": "ok", + "capabilities": [ + { + "method": "HealthProblemEvent", + "signature": "(birthTime, startTime, endTime)", + "bucket": "event_range", + "parameter_names": ["birthTime", "startTime", "endTime"], + "callable": True, + }, + { + "method": "EducationDegreeYoga", + "signature": "(birthTime)", + "bucket": "birth_time_only", + "parameter_names": ["birthTime"], + "callable": True, + }, + { + "method": "PropertyHouseVehicleResult", + "signature": "(birthTime)", + "bucket": "birth_time_only", + "parameter_names": ["birthTime"], + "callable": True, + }, + { + "method": "ChildrenProgenyPromise", + "signature": "(birthTime)", + "bucket": "birth_time_only", + "parameter_names": ["birthTime"], + "callable": True, + }, + { + "method": "ForeignTravelMigrationEvent", + "signature": "(birthTime, startTime, endTime)", + "bucket": "event_range", + "parameter_names": ["birthTime", "startTime", "endTime"], + "callable": True, + }, + { + "method": "PrashnaHoraryJudgement", + "signature": "(queryTime, questionText)", + "bucket": "query_text", + "parameter_names": ["queryTime", "questionText"], + "callable": True, + }, + { + "method": "OpaqueExperimentalCalculator", + "signature": "(complexPayload)", + "bucket": "opaque", + "parameter_names": ["complexPayload"], + "callable": True, + }, + ], + "buckets": { + "event_range": {"count": 2, "examples": ["HealthProblemEvent", "ForeignTravelMigrationEvent"]}, + "birth_time_only": {"count": 3, "examples": ["EducationDegreeYoga"]}, + "query_text": {"count": 1, "examples": ["PrashnaHoraryJudgement"]}, + "opaque": {"count": 1, "examples": ["OpaqueExperimentalCalculator"]}, + }, + } + ), + "VEDASTRO_OFFICIAL_CAPABILITY_RUNNER_STUB": json.dumps( + { + "HealthProblemEvent": {"available": True, "status": "ok", "result": {}}, + "EducationDegreeYoga": {"available": True, "status": "ok", "result": {}}, + "PropertyHouseVehicleResult": {"available": True, "status": "ok", "result": {}}, + "ChildrenProgenyPromise": {"available": True, "status": "ok", "result": {}}, + "ForeignTravelMigrationEvent": {"available": True, "status": "ok", "result": {}}, + } + ), + }, + ) + + assert completed.returncode == 0, completed.stderr or completed.stdout + report = json.loads(completed.stdout) + required_fields = {"domains", "execution_policy", "adjudicator_use", "confidence_role", "blocked_reason"} + for status in report["method_statuses"].values(): + assert required_fields.issubset(status) + + assert "health" in report["method_statuses"]["HealthProblemEvent"]["domains"] + assert "education" in report["method_statuses"]["EducationDegreeYoga"]["domains"] + assert "property" in report["method_statuses"]["PropertyHouseVehicleResult"]["domains"] + assert "children" in report["method_statuses"]["ChildrenProgenyPromise"]["domains"] + assert "migration" in report["method_statuses"]["ForeignTravelMigrationEvent"]["domains"] + assert "prashna" in report["method_statuses"]["PrashnaHoraryJudgement"]["domains"] + assert report["method_statuses"]["PrashnaHoraryJudgement"]["execution_policy"] == "needs_user_text" + assert report["method_statuses"]["PrashnaHoraryJudgement"]["adjudicator_use"] == "secondary_context" + assert report["method_statuses"]["OpaqueExperimentalCalculator"]["domains"] == ["unknown"] + assert report["method_statuses"]["OpaqueExperimentalCalculator"]["execution_policy"] == "blocked" + assert report["method_statuses"]["OpaqueExperimentalCalculator"]["blocked_reason"] == "unsupported_signature" + + for domain in ("health", "education", "property", "children", "migration", "prashna"): + assert domain in report["domain_routing"] + assert domain in report["dynamic_selection"] + assert "general" not in report["domain_routing"] + assert report["summary"]["unknown_method_count"] == 1 + assert report["summary"]["misrouted_general_method_count"] == 0 diff --git a/tests/test_vedastro_service_adapter_executor.py b/tests/test_vedastro_service_adapter_executor.py index cf387950..f35bf484 100644 --- a/tests/test_vedastro_service_adapter_executor.py +++ b/tests/test_vedastro_service_adapter_executor.py @@ -166,7 +166,7 @@ def test_vedastro_official_snapshot_stops_when_foreground_budget_is_exhausted(mo assert result["status"] == "official_snapshot_budget_exhausted" assert result["available"] is False assert result["source_metadata"]["official_python_bundle"]["status"] == "official_snapshot_budget_exhausted" - assert result["source_metadata"]["official_full_capability_catalog"]["status"] == "official_full_capability_catalog_timeout" + assert result["source_metadata"]["official_full_capability_catalog"]["status"] == "official_full_capability_catalog_skipped_budget_exhausted" def test_vedastro_official_snapshot_skips_bridge_after_runner_consumes_budget(monkeypatch) -> None: @@ -226,7 +226,7 @@ def test_vedastro_official_snapshot_skips_bridge_after_runner_consumes_budget(mo assert bridge_called["value"] is False assert result["status"] == "official_snapshot_budget_exhausted" assert result["source_metadata"]["official_python_bundle"]["status"] == "official_snapshot_budget_exhausted" - assert result["source_metadata"]["official_full_capability_catalog"]["summary"]["catalog_method_count"] == 641 + assert result["source_metadata"]["official_full_capability_catalog"]["status"] == "official_full_capability_catalog_skipped_budget_exhausted" def test_vedastro_official_snapshot_budget_does_not_mask_mock_rest_endpoint(monkeypatch) -> None: