Add VedAstro user entrypoint

This commit is contained in:
732642856
2026-07-02 23:17:47 +08:00
parent 21212a1b9f
commit ec4598c0d7
3 changed files with 513 additions and 0 deletions
+279
View File
@@ -0,0 +1,279 @@
#!/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]:
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(),
)
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 _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]:
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": _bool_env("VEDASTRO_FREE_TIER_QUEUE")
or _bool_env("VEDASTRO_FREE_TIER_QUEUE_ENABLED")
or _bool_env("VEDASTRO_ENABLE_FREE_TIER_QUEUE"),
"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)
route = _primary_route(args.question, themes)
return {
"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,
"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 --format json "
"# includes official_full_capability_catalog"
),
"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"
),
},
}
def render_markdown(report: dict[str, Any]) -> str:
runtime = report["runtime_mode"]
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_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("--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())