Files
Jyotisha/scripts/vedastro_user_entrypoint.py
T

369 lines
15 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
"""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", "5") or 5)),
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())