369 lines
15 KiB
Python
369 lines
15 KiB
Python
#!/usr/bin/env python3
|
||
"""User-level VedAstro + strict-workflow entrypoint for Codex sessions."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
try:
|
||
from scripts.diagnose_vedastro_mode import build_report as build_runtime_mode_report
|
||
from scripts.local_env import load_local_env
|
||
except ModuleNotFoundError: # pragma: no cover - direct script execution
|
||
from diagnose_vedastro_mode import build_report as build_runtime_mode_report
|
||
from local_env import load_local_env
|
||
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
RUNNER = ROOT / "scripts" / "vedastro_official_capability_runner.py"
|
||
|
||
|
||
def _bool_env(name: str) -> bool:
|
||
return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}
|
||
|
||
|
||
def _int_env(name: str, default: int = 0) -> int:
|
||
raw = os.environ.get(name, "").strip()
|
||
if not raw:
|
||
return default
|
||
try:
|
||
return int(float(raw))
|
||
except ValueError:
|
||
return default
|
||
|
||
|
||
def _themes(raw: str) -> list[str]:
|
||
aliases = {
|
||
"relationship": "marriage",
|
||
"relationships": "marriage",
|
||
"finance": "wealth",
|
||
"money": "wealth",
|
||
"事业": "career",
|
||
"婚恋": "marriage",
|
||
"婚姻": "marriage",
|
||
"财富": "wealth",
|
||
"健康": "health",
|
||
"教育": "education",
|
||
"房产": "property",
|
||
"子女": "children",
|
||
"迁移": "migration",
|
||
"问卜": "prashna",
|
||
"校时": "rectification",
|
||
"应期": "timing",
|
||
}
|
||
values: list[str] = []
|
||
for item in raw.replace(",", ",").split(","):
|
||
key = aliases.get(item.strip().lower(), item.strip().lower())
|
||
if key and key not in values:
|
||
values.append(key)
|
||
return values or ["career", "marriage", "wealth"]
|
||
|
||
|
||
def _primary_route(question: str, themes: list[str]) -> str:
|
||
text = f"{question} {' '.join(themes)}".lower()
|
||
route_aliases = [
|
||
("career", ("career", "job", "work", "profession", "事业", "工作", "职业", "项目")),
|
||
("relationship", ("marriage", "relationship", "spouse", "partner", "婚恋", "婚姻", "伴侣")),
|
||
("finance", ("wealth", "finance", "money", "income", "财富", "金钱", "收入")),
|
||
]
|
||
for route, tokens in route_aliases:
|
||
if any(token in text for token in tokens):
|
||
return route
|
||
return "general"
|
||
|
||
|
||
def _case_from_args(args: argparse.Namespace, themes: list[str]) -> dict[str, Any]:
|
||
return {
|
||
"year": args.year,
|
||
"month": args.month,
|
||
"day": args.day,
|
||
"hour": args.hour,
|
||
"minute": args.minute,
|
||
"second": args.second,
|
||
"lat": args.lat,
|
||
"lon": args.lon,
|
||
"tz": args.tz,
|
||
"reference_date": args.reference_date,
|
||
"themes": themes,
|
||
"ayanamsa_policy": args.ayanamsa,
|
||
"node_policy": args.node_mode,
|
||
}
|
||
|
||
|
||
def _run_capability_catalog(case: dict[str, Any]) -> dict[str, Any]:
|
||
try:
|
||
completed = subprocess.run(
|
||
[
|
||
sys.executable,
|
||
str(RUNNER),
|
||
"--bundle",
|
||
"official_full_capability_catalog",
|
||
"--birth-json",
|
||
json.dumps(case, ensure_ascii=False),
|
||
],
|
||
cwd=ROOT,
|
||
text=True,
|
||
capture_output=True,
|
||
timeout=max(5.0, float(os.environ.get("VEDASTRO_TIMEOUT_SECONDS", "20") or 20)),
|
||
check=False,
|
||
env=os.environ.copy(),
|
||
)
|
||
except subprocess.TimeoutExpired:
|
||
return {
|
||
"available": False,
|
||
"status": "official_full_capability_catalog_timeout",
|
||
"summary": {},
|
||
"domain_routing": {},
|
||
"sample_outputs": {},
|
||
}
|
||
if completed.returncode != 0:
|
||
return {
|
||
"available": False,
|
||
"status": "official_full_capability_catalog_runtime_error",
|
||
"summary": {},
|
||
"domain_routing": {},
|
||
"dynamic_selection": {},
|
||
"stderr": (completed.stderr or "").strip(),
|
||
}
|
||
try:
|
||
return json.loads(completed.stdout)
|
||
except json.JSONDecodeError:
|
||
return {
|
||
"available": False,
|
||
"status": "official_full_capability_catalog_invalid_json",
|
||
"summary": {},
|
||
"domain_routing": {},
|
||
"dynamic_selection": {},
|
||
"stdout_excerpt": (completed.stdout or "").strip()[:500],
|
||
}
|
||
|
||
|
||
def _official_raw_requested(args: argparse.Namespace) -> bool:
|
||
return (
|
||
bool(getattr(args, "require_official_raw_response", False))
|
||
or _bool_env("VEDASTRO_REQUIRE_OFFICIAL_RAW_RESPONSE")
|
||
or _bool_env("VEDASTRO_GATEWAY_REQUIRE_OFFICIAL_RAW_RESPONSE")
|
||
)
|
||
|
||
|
||
def _run_official_full_snapshot(case: dict[str, Any]) -> dict[str, Any]:
|
||
stub = os.environ.get("VEDASTRO_OFFICIAL_FULL_SNAPSHOT_STUB", "").strip()
|
||
if stub:
|
||
try:
|
||
parsed = json.loads(stub)
|
||
except json.JSONDecodeError as exc:
|
||
return {
|
||
"available": False,
|
||
"status": "official_full_snapshot_stub_invalid_json",
|
||
"error": {"type": type(exc).__name__, "message": str(exc)},
|
||
}
|
||
return parsed if isinstance(parsed, dict) else {"available": False, "status": "official_full_snapshot_stub_not_object"}
|
||
|
||
try:
|
||
from scripts.vedastro_service_adapter import run_official_full_snapshot_for_case
|
||
except ModuleNotFoundError: # pragma: no cover - direct script execution
|
||
from vedastro_service_adapter import run_official_full_snapshot_for_case
|
||
|
||
try:
|
||
result = run_official_full_snapshot_for_case(case, case_id="user_chart")
|
||
except Exception as exc: # pragma: no cover - runtime boundary
|
||
return {
|
||
"available": False,
|
||
"status": "official_full_snapshot_runtime_error",
|
||
"error": {"type": type(exc).__name__, "message": str(exc)},
|
||
}
|
||
return result if isinstance(result, dict) else {"available": False, "status": "official_full_snapshot_invalid_result"}
|
||
|
||
|
||
def _extract_official_raw_response(snapshot: dict[str, Any]) -> dict[str, Any]:
|
||
raw = (
|
||
snapshot.get("official_raw_response")
|
||
or snapshot.get("raw_response")
|
||
or snapshot.get("vedastro_official_raw_response")
|
||
)
|
||
return raw if isinstance(raw, dict) else {}
|
||
|
||
|
||
def _snapshot_summary(snapshot: dict[str, Any], raw_response: dict[str, Any]) -> dict[str, Any]:
|
||
metadata = snapshot.get("source_metadata") if isinstance(snapshot.get("source_metadata"), dict) else {}
|
||
return {
|
||
"status": snapshot.get("status") or "not_requested",
|
||
"available": bool(snapshot.get("available")) or bool(raw_response),
|
||
"operation": snapshot.get("operation") or "official_full_snapshot",
|
||
"raw_response_available": bool(raw_response),
|
||
"artifact_path": metadata.get("artifact_path"),
|
||
"reason": snapshot.get("reason"),
|
||
"error": snapshot.get("error") if isinstance(snapshot.get("error"), dict) else {},
|
||
"boundary": "official_raw_response must be present before claiming VedAstro official cloud closure.",
|
||
}
|
||
|
||
|
||
def _strict_workflow_summary(route: str, catalog: dict[str, Any]) -> dict[str, Any]:
|
||
dynamic_selection = catalog.get("dynamic_selection") if isinstance(catalog.get("dynamic_selection"), dict) else {}
|
||
theme_for_route = {"relationship": "marriage", "finance": "wealth"}.get(route, route)
|
||
return {
|
||
"triggered": route in {"career", "relationship", "finance"},
|
||
"primary_route": route,
|
||
"routes_available": ["career", "relationship", "finance"],
|
||
"source": "strict_workflow_contract_summary",
|
||
"official_capability_selection": dynamic_selection.get(theme_for_route) or {},
|
||
"boundary": (
|
||
"This entrypoint triggers the strict workflow contract lane and passes VedAstro official catalog "
|
||
"selection metadata; it does not claim every official method was executed."
|
||
),
|
||
}
|
||
|
||
|
||
def _cache_and_queue_report() -> dict[str, Any]:
|
||
queue_enabled = (
|
||
_bool_env("VEDASTRO_FREE_TIER_QUEUE")
|
||
or _bool_env("VEDASTRO_FREE_TIER_QUEUE_ENABLED")
|
||
or _bool_env("VEDASTRO_ENABLE_FREE_TIER_QUEUE")
|
||
)
|
||
using_free_tier = not bool(os.environ.get("VEDASTRO_API_KEY", "").strip())
|
||
return {
|
||
"official_full_snapshot_cache_scope": "official_full_snapshot_semantic_cache",
|
||
"official_full_snapshot_cache_ttl_seconds": _int_env("VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS", 0),
|
||
"range_scan_cache_scope": "vedastro_range_scan_request_cache",
|
||
"range_scan_cache_ttl_seconds": _int_env("VEDASTRO_CACHE_TTL_SECONDS", 0),
|
||
"free_tier_queue_enabled": queue_enabled,
|
||
"free_tier_strategy": {
|
||
"using_free_tier": using_free_tier,
|
||
"queue_enabled": queue_enabled,
|
||
"guard_status": "within_free_tier_strategy" if using_free_tier else "premium_key_present",
|
||
},
|
||
"sample_limit": _int_env("VEDASTRO_FULL_CATALOG_SAMPLE_LIMIT", 0),
|
||
"artifact_root": "scratch/local/vedastro_adapter",
|
||
}
|
||
|
||
|
||
def build_report(args: argparse.Namespace) -> dict[str, Any]:
|
||
load_local_env(ROOT)
|
||
themes = _themes(args.themes)
|
||
case = _case_from_args(args, themes)
|
||
runtime = build_runtime_mode_report()
|
||
catalog = _run_capability_catalog(case)
|
||
snapshot = _run_official_full_snapshot(case) if _official_raw_requested(args) else {"status": "not_requested"}
|
||
raw_response = _extract_official_raw_response(snapshot)
|
||
route = _primary_route(args.question, themes)
|
||
report = {
|
||
"scope": "vedastro_user_entrypoint",
|
||
"schema_version": 1,
|
||
"input": {
|
||
"question": args.question,
|
||
"themes": themes,
|
||
"birth": {key: case[key] for key in ("year", "month", "day", "hour", "minute", "second", "lat", "lon", "tz")},
|
||
"reference_date": args.reference_date,
|
||
},
|
||
"runtime_mode": runtime,
|
||
"vedastro_official_full_snapshot": _snapshot_summary(snapshot, raw_response),
|
||
"official_capability_catalog": {
|
||
"status": catalog.get("status") or "blocked",
|
||
"available": bool(catalog.get("available")),
|
||
"summary": catalog.get("summary") or {},
|
||
"coverage": catalog.get("coverage") or {},
|
||
"domain_routing": catalog.get("domain_routing") or {},
|
||
"dynamic_selection": catalog.get("dynamic_selection") or {},
|
||
},
|
||
"cache_and_queue": _cache_and_queue_report(),
|
||
"strict_workflow": _strict_workflow_summary(route, catalog),
|
||
"honesty_boundary": {
|
||
"all_641_methods_executed": False,
|
||
"official_catalog_classified": bool(catalog.get("summary")),
|
||
"official_extended_ready": bool(runtime.get("official_ready")),
|
||
"rule": (
|
||
"Use official catalog domain routing and dynamic selection as evidence metadata. "
|
||
"Methods requiring user context/text/rectification remain blocked or secondary until inputs exist."
|
||
),
|
||
},
|
||
"user_commands": {
|
||
"json": (
|
||
"python3 scripts/vedastro_user_entrypoint.py --year YYYY --month MM --day DD "
|
||
"--hour HH --minute MM --lat LAT --lon LON --tz TZ --question '...' "
|
||
"--themes career,marriage,wealth --reference-date YYYY-MM-DD "
|
||
"--require-official-raw-response --format json "
|
||
"# includes official_full_capability_catalog and official_raw_response when verified"
|
||
),
|
||
"markdown": (
|
||
"python3 scripts/vedastro_user_entrypoint.py --year YYYY --month MM --day DD "
|
||
"--hour HH --minute MM --lat LAT --lon LON --tz TZ --question '...' --format markdown"
|
||
),
|
||
},
|
||
}
|
||
if raw_response:
|
||
report["official_raw_response"] = raw_response
|
||
return report
|
||
|
||
|
||
def render_markdown(report: dict[str, Any]) -> str:
|
||
runtime = report["runtime_mode"]
|
||
snapshot = report.get("vedastro_official_full_snapshot") or {}
|
||
catalog = report["official_capability_catalog"]
|
||
cache = report["cache_and_queue"]
|
||
strict = report["strict_workflow"]
|
||
summary = catalog.get("summary") or {}
|
||
lines = [
|
||
"# VedAstro 用户级入口",
|
||
"",
|
||
f"- runtime_mode: `{runtime.get('mode')}`",
|
||
f"- official_full_snapshot: `{snapshot.get('status')}`",
|
||
f"- official_raw_response_available: `{str(bool(snapshot.get('raw_response_available'))).lower()}`",
|
||
f"- official_ready: `{str(runtime.get('official_ready')).lower()}`",
|
||
f"- catalog_status: `{catalog.get('status')}`",
|
||
f"- catalog_method_count: `{summary.get('catalog_method_count', 0)}`",
|
||
f"- unknown_method_count: `{summary.get('unknown_method_count', 0)}`",
|
||
f"- misrouted_general_method_count: `{summary.get('misrouted_general_method_count', 0)}`",
|
||
f"- strict workflow triggered: `{str(strict.get('triggered')).lower()}`",
|
||
f"- primary_route: `{strict.get('primary_route')}`",
|
||
"",
|
||
"## Cache / Free-Tier Queue",
|
||
"",
|
||
f"- official_full_snapshot_cache_ttl_seconds: `{cache['official_full_snapshot_cache_ttl_seconds']}`",
|
||
f"- range_scan_cache_ttl_seconds: `{cache['range_scan_cache_ttl_seconds']}`",
|
||
f"- free_tier_queue_enabled: `{str(cache['free_tier_queue_enabled']).lower()}`",
|
||
"",
|
||
"## Boundary",
|
||
"",
|
||
"- 这个入口会启动 VedAstro official capability catalog 分类、动态主题选择和 strict workflow 合同摘要。",
|
||
"- 它不会把 641 项全部当作已执行;需要用户上下文、文本问题或校时画像的方法会保持 blocked/secondary。",
|
||
"- 如果 runtime 是 `fast_local_fallback`,解盘必须诚实降级,不得声称 official extended 已闭环。",
|
||
]
|
||
return "\n".join(lines) + "\n"
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
parser.add_argument("--year", type=int, required=True)
|
||
parser.add_argument("--month", type=int, required=True)
|
||
parser.add_argument("--day", type=int, required=True)
|
||
parser.add_argument("--hour", type=int, required=True)
|
||
parser.add_argument("--minute", type=int, required=True)
|
||
parser.add_argument("--second", type=int, default=0)
|
||
parser.add_argument("--lat", type=float, required=True)
|
||
parser.add_argument("--lon", type=float, required=True)
|
||
parser.add_argument("--tz", type=float, required=True)
|
||
parser.add_argument("--question", default="")
|
||
parser.add_argument("--themes", default="career,marriage,wealth")
|
||
parser.add_argument("--reference-date", required=True)
|
||
parser.add_argument("--ayanamsa", default="lahiri")
|
||
parser.add_argument("--node-mode", default="mean")
|
||
parser.add_argument("--require-official-raw-response", action="store_true")
|
||
parser.add_argument("--format", choices=["json", "markdown"], default="markdown")
|
||
args = parser.parse_args()
|
||
|
||
report = build_report(args)
|
||
if args.format == "json":
|
||
print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True))
|
||
else:
|
||
print(render_markdown(report), end="")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|