Add official VedAstro MCP bridge and routing assets

This commit is contained in:
732642856
2026-06-29 07:50:32 +08:00
parent 2ca0939817
commit 78f4585d84
18 changed files with 2040 additions and 44 deletions
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""Render the fastest practical VedAstro integration checklist.
This artifact turns the parity matrix plus live official catalog snapshot into
an execution-friendly view:
- what should go through official MCP
- what should go through the official Python bridge
- what should go through the REST adapter
- what should stay local because the repo is already stronger there
"""
from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from scripts import vedastro_parity_matrix
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_JSON_PATH = ROOT / "docs" / "research" / "vedastro_fast_path_checklist_latest.json"
DEFAULT_MARKDOWN_PATH = ROOT / "docs" / "research" / "vedastro_fast_path_checklist_latest.md"
CATALOG_PATH = ROOT / "scratch" / "local" / "vedastro_adapter" / "method_catalog_snapshot.json"
LANE_TITLES = {
"official_mcp": "Direct Official MCP",
"official_python_bridge": "Official Python Bridge",
"rest_adapter": "REST Adapter",
"local_native_preferred": "Local Native Preferred",
"hybrid_router": "Hybrid Router",
"external_evidence_only": "External Evidence Only",
}
def _load_catalog_summary() -> dict[str, Any]:
if not CATALOG_PATH.exists():
return {
"available": False,
"path": str(CATALOG_PATH),
"tag_count": 0,
"method_count": 0,
}
payload = json.loads(CATALOG_PATH.read_text(encoding="utf-8"))
summary = payload.get("summary") or {}
return {
"available": True,
"path": str(CATALOG_PATH),
"tag_count": int(summary.get("tag_count", 0) or 0),
"method_count": int(summary.get("method_count", 0) or 0),
}
def build_checklist() -> dict[str, Any]:
matrix = vedastro_parity_matrix.build_matrix()
catalog = _load_catalog_summary()
grouped: dict[str, list[dict[str, Any]]] = {lane: [] for lane in LANE_TITLES}
for row in matrix["rows"]:
grouped.setdefault(row["fastest_path_lane"], []).append(
{
"capability": row["vedastro_capability"],
"priority": row["priority"],
"recommended_path": row["recommended_path"],
"local_status": row["local_status"],
"adjudicator_use": row["adjudicator_use"],
"route_notes": row["route_notes"],
"gap_notes": row["gap_notes"],
"local_assets": row["local_assets"],
}
)
return {
"scope": "vedastro_fast_path_checklist",
"generated_at": datetime.now(timezone.utc).isoformat(),
"summary": {
"row_count": len(matrix["rows"]),
"local_registry_technique_count": matrix["summary"].get("local_registry_technique_count"),
"catalog_tag_count": catalog["tag_count"],
"catalog_method_count": catalog["method_count"],
},
"boundary": {
"do_not_clone_vedastro": True,
"keep_local_adjudicator_authoritative": True,
"external_outputs_default_to_secondary_context": True,
},
"official_catalog": catalog,
"lanes": grouped,
}
def render_markdown(checklist: dict[str, Any]) -> str:
summary = checklist["summary"]
catalog = checklist["official_catalog"]
lines = [
"# VedAstro 596+ Nodes Fast-Path Checklist",
"",
f"- Generated: `{checklist['generated_at']}`",
f"- Local registry techniques: `{summary['local_registry_technique_count']}`",
f"- Official catalog tags: `{summary['catalog_tag_count']}`",
f"- Official catalog methods/events: `{summary['catalog_method_count']}`",
"",
"## Boundary",
"",
"- Do not clone VedAstro wholesale into local code.",
"- Keep the local adjudicator as the final reasoning layer.",
"- Default all external outputs to secondary evidence unless explicitly promoted by local tests and contracts.",
"",
"## Lane Breakdown",
"",
]
for lane, title in LANE_TITLES.items():
rows = checklist["lanes"].get(lane) or []
lines.append(f"### {title}")
lines.append("")
if not rows:
lines.append("- None.")
lines.append("")
continue
for row in rows:
assets = ", ".join(row["local_assets"]) if row["local_assets"] else "-"
lines.append(
f"- `{row['capability']}` [{row['priority']}]"
f" - local=`{row['local_status']}`, adjudicator=`{row['adjudicator_use']}`"
f"; assets: {assets}"
)
lines.append(f" route: {row['route_notes']}")
lines.append(f" gap: {row['gap_notes']}")
lines.append("")
lines.extend(
[
"## Immediate Execution Order",
"",
"1. Use `Direct Official MCP` where an official MCP surface is available to agents.",
"2. Use `Official Python Bridge` for broad calculator access that does not need browser/session orchestration.",
"3. Use `REST Adapter` for range scans and endpoint-style external evidence.",
"4. Keep `Local Native Preferred` rows local; do not waste time rebuilding what is already stronger here.",
"5. Use `Hybrid Router` only where parity or oracle closure still matters more than raw breadth.",
"",
]
)
return "\n".join(lines)
def write_outputs(
checklist: dict[str, Any],
*,
json_path: Path = DEFAULT_JSON_PATH,
markdown_path: Path = DEFAULT_MARKDOWN_PATH,
) -> None:
json_path.parent.mkdir(parents=True, exist_ok=True)
markdown_path.parent.mkdir(parents=True, exist_ok=True)
json_path.write_text(json.dumps(checklist, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
markdown_path.write_text(render_markdown(checklist) + "\n", encoding="utf-8")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--format", choices=["json", "markdown"], default="json")
parser.add_argument("--write", action="store_true")
args = parser.parse_args(argv)
checklist = build_checklist()
if args.write:
write_outputs(checklist)
if args.format == "json":
print(json.dumps(checklist, ensure_ascii=False, indent=2))
else:
print(render_markdown(checklist))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""Sync a lightweight VedAstro method catalog snapshot.
This script intentionally starts small. It can pull the official event-tag
catalog from VedAstro's public API or write a stubbed snapshot in tests. The
result is a local JSON snapshot suitable for later MCP/Python/REST routing.
"""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
from typing import Any
from urllib import request
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_OUTPUT = ROOT / "scratch" / "local" / "vedastro_adapter" / "method_catalog_snapshot.json"
OFFICIAL_TAG_CATALOG_URL = "https://api.vedastro.org/api/Calculate/GetAllEventDataGroupedByTag"
STUB_ENV = "VEDASTRO_METHOD_CATALOG_STUB"
def schema() -> dict[str, Any]:
return {
"sync": "vedastro_method_catalog_sync",
"scope": "official_vedastro_method_catalog",
"operations": ["sync_tags", "write_snapshot"],
"sources": {
"official_tag_catalog": OFFICIAL_TAG_CATALOG_URL,
},
"output_contract": ["source", "summary", "tag_groups"],
}
def _load_stubbed_catalog() -> dict[str, Any] | None:
raw = os.environ.get(STUB_ENV, "").strip()
if not raw:
return None
return json.loads(raw)
def _fetch_official_tag_catalog() -> dict[str, Any]:
with request.urlopen(OFFICIAL_TAG_CATALOG_URL, timeout=60) as resp:
payload = json.loads(resp.read().decode("utf-8"))
groups = ((payload.get("Payload") or {}).get("GetAllEventDataGroupedByTag") or {})
return {
"source": "official_tag_catalog",
"tag_groups": groups,
}
def build_catalog() -> dict[str, Any]:
catalog = _load_stubbed_catalog() or _fetch_official_tag_catalog()
tag_groups = catalog.get("tag_groups") or {}
method_count = 0
for events in tag_groups.values():
if isinstance(events, list):
method_count += len(events)
catalog["summary"] = {
"tag_count": len(tag_groups),
"method_count": method_count,
}
return catalog
def write_snapshot(output_path: Path) -> dict[str, Any]:
catalog = build_catalog()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(catalog, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8")
return {
"status": "ok",
"source": catalog.get("source"),
"summary": catalog.get("summary"),
"output_path": str(output_path),
}
def main() -> int:
parser = argparse.ArgumentParser(description="VedAstro method catalog sync")
parser.add_argument("--print-schema", action="store_true")
parser.add_argument("--write", action="store_true")
parser.add_argument("--output", default=str(DEFAULT_OUTPUT))
args = parser.parse_args()
if args.print_schema:
result = schema()
elif args.write:
result = write_snapshot(Path(args.output))
else:
result = build_catalog()
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""Thin bridge to the official public VedAstro MCP endpoint.
This bridge is intentionally narrow. It proves that the official public MCP
surface is reachable and callable from the local workspace without letting
external responses silently override local adjudication.
"""
from __future__ import annotations
import argparse
import json
import urllib.error
import urllib.request
from typing import Any
DEFAULT_ENDPOINT = "https://mcp.vedastro.org/api/mcp/public"
DEFAULT_PROTOCOL_VERSION = "2025-06-18"
DEFAULT_TIMEOUT_SECONDS = 60
def _post_json(endpoint: str, payload: dict[str, Any], timeout: float = DEFAULT_TIMEOUT_SECONDS) -> tuple[dict[str, Any], dict[str, str]]:
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(
endpoint,
data=body,
method="POST",
headers={
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
},
)
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read().decode("utf-8")
headers = {key.lower(): value for key, value in resp.headers.items()}
return json.loads(raw), headers
def _initialize(endpoint: str) -> dict[str, Any]:
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": DEFAULT_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {
"name": "jyotish-local-bridge",
"version": "1.0.0",
},
},
}
result, headers = _post_json(endpoint, payload)
session_id = headers.get("mcp-session-id") or headers.get("x-mcp-session-id")
return {
"endpoint": endpoint,
"available": True,
"status": "ok",
"operation": "initialize",
"session_id": session_id,
"result": result.get("result") or result,
"source": "official_public_mcp",
}
def _tools_list(endpoint: str) -> dict[str, Any]:
init = _initialize(endpoint)
payload = {
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {},
}
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
}
if init.get("session_id"):
headers["Mcp-Session-Id"] = str(init["session_id"])
req = urllib.request.Request(endpoint, data=body, method="POST", headers=headers)
with urllib.request.urlopen(req, timeout=DEFAULT_TIMEOUT_SECONDS) as resp:
raw = resp.read().decode("utf-8")
response_headers = {key.lower(): value for key, value in resp.headers.items()}
result = json.loads(raw)
tools = (result.get("result") or {}).get("tools") or []
return {
"endpoint": endpoint,
"available": True,
"status": "ok",
"operation": "tools_list",
"session_id": init.get("session_id") or response_headers.get("mcp-session-id"),
"tool_count": len(tools),
"tool_names": [tool.get("name") for tool in tools if isinstance(tool, dict) and tool.get("name")],
"result": result.get("result") or result,
"source": "official_public_mcp",
}
def schema() -> dict[str, Any]:
return {
"bridge": "vedastro_official_mcp_bridge",
"role": "official_public_mcp_thin_bridge",
"endpoint": DEFAULT_ENDPOINT,
"operations": ["initialize", "tools_list"],
"response_contract": [
"endpoint",
"available",
"status",
"operation",
"result",
"source",
],
"boundaries": [
"Use for official MCP reachability and tool discovery.",
"Do not let raw MCP results directly override local score/dominant_label/payout_label.",
"Promote only through explicit local contracts and tests.",
],
}
def _error_result(endpoint: str, operation: str, exc: Exception) -> dict[str, Any]:
reason = str(exc)
if isinstance(exc, urllib.error.HTTPError):
reason = f"http_{exc.code}: {reason}"
return {
"endpoint": endpoint,
"available": False,
"status": "mcp_request_failed",
"operation": operation,
"reason": reason,
"source": "official_public_mcp",
}
def main() -> int:
parser = argparse.ArgumentParser(description="Official public VedAstro MCP bridge")
parser.add_argument("--print-schema", action="store_true")
parser.add_argument("--endpoint", default=DEFAULT_ENDPOINT)
parser.add_argument("--operation", choices=["initialize", "tools_list"], default="tools_list")
args = parser.parse_args()
if args.print_schema:
result = schema()
else:
try:
if args.operation == "initialize":
result = _initialize(args.endpoint)
else:
result = _tools_list(args.endpoint)
except Exception as exc: # noqa: BLE001
result = _error_result(args.endpoint, args.operation, exc)
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+52 -5
View File
@@ -29,6 +29,15 @@ ALLOWED_RECOMMENDED_PATHS = {
"hybrid_local_plus_vedastro",
}
ALLOWED_FASTEST_PATH_LANES = {
"local_native_preferred",
"official_mcp",
"official_python_bridge",
"rest_adapter",
"hybrid_router",
"external_evidence_only",
}
VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [
{
@@ -38,10 +47,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [
"local_assets": ["transit_trigger", "dasha", "narayana_dasha", "vedastro_service_adapter.range_scan"],
"can_call_vedastro": True,
"recommended_path": "hybrid_local_plus_vedastro",
"fastest_path_lane": "rest_adapter",
"priority": "P0",
"license_boundary": "external_service_or_clean_room_only",
"adjudicator_use": "oracle_only",
"gap_notes": "Local timing modules exist, but a high-frequency day/hour event graph is not yet a local productized radar.",
"route_notes": "Use official REST adapter first for range scans; keep local adjudicator as the reasoning layer.",
},
{
"vedastro_capability": "Ayanamsa Selection",
@@ -50,10 +61,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [
"local_assets": ["ayanamsa_utils", "ephemeris_adapter_contract"],
"can_call_vedastro": True,
"recommended_path": "hybrid_local_plus_vedastro",
"fastest_path_lane": "hybrid_router",
"priority": "P0",
"license_boundary": "external_service_or_clean_room_only",
"adjudicator_use": "oracle_only",
"gap_notes": "Local Lahiri path is usable, but broad ayanamsa parity and public comparison artifacts remain incomplete.",
"route_notes": "Prefer local ayanamsa controls for production and use VedAstro only as parity/oracle evidence.",
},
{
"vedastro_capability": "D1-D60 Divisional Charts",
@@ -62,10 +75,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [
"local_assets": ["varga", "varga_full", "shodasavarga", "divisional_charts_extended"],
"can_call_vedastro": True,
"recommended_path": "local_native",
"fastest_path_lane": "local_native_preferred",
"priority": "P0",
"license_boundary": "local_native_or_clean_room",
"adjudicator_use": "primary",
"gap_notes": "Local varga coverage is strong; keep VedAstro/PyJHora as benchmark evidence rather than replacing local math.",
"route_notes": "Do not pay external-call cost here; local engine is already the primary route.",
},
{
"vedastro_capability": "Ashtakavarga",
@@ -74,10 +89,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [
"local_assets": ["ashtakavarga", "ashtakavarga_pav", "ashtakavarga_sodhita", "finance_ashtakavarga_bridge"],
"can_call_vedastro": True,
"recommended_path": "hybrid_local_plus_vedastro",
"fastest_path_lane": "official_python_bridge",
"priority": "P0",
"license_boundary": "local_native_or_external_oracle",
"adjudicator_use": "secondary",
"gap_notes": "SAV/BAV is in production; PAV/Sodhita/Kakshya bridges need continued regression before being dominant labels.",
"route_notes": "Use the Python bridge for broad calculator access while local strict workflows stay authoritative.",
},
{
"vedastro_capability": "Shadbala",
@@ -86,10 +103,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [
"local_assets": ["shadbala", "shadbala_advanced", "shadbala_component_cap", "oracle_shadbala_queue"],
"can_call_vedastro": True,
"recommended_path": "hybrid_local_plus_vedastro",
"fastest_path_lane": "official_python_bridge",
"priority": "P0",
"license_boundary": "local_native_or_external_oracle",
"adjudicator_use": "secondary",
"gap_notes": "Local component-aware cap exists; absolute external oracle closure is still incomplete.",
"route_notes": "Broad strength calculators are easiest through the Python bridge, then folded back as oracle evidence.",
},
{
"vedastro_capability": "Jaimini / Chara Dasha",
@@ -98,10 +117,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [
"local_assets": ["jaimini.py", "AK", "DK", "UL", "Chara Dasha", "Jaimini marriage bridge v1"],
"can_call_vedastro": True,
"recommended_path": "local_native",
"fastest_path_lane": "local_native_preferred",
"priority": "P0",
"license_boundary": "local_native_mit_attribution_for_reused_parts",
"adjudicator_use": "secondary",
"gap_notes": "Core Jaimini exists; mission/career/marriage quality adjudicator folding is not yet exhaustive.",
"route_notes": "Keep Jaimini native; external engines help only as spot-check evidence.",
},
{
"vedastro_capability": "Synastry / Ashtakoot",
@@ -110,10 +131,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [
"local_assets": ["synastry.py", "ashtakoot.py", "36-point Ashtakoot", "16-factor compatibility"],
"can_call_vedastro": True,
"recommended_path": "local_native",
"fastest_path_lane": "local_native_preferred",
"priority": "P0",
"license_boundary": "local_native_mit_attribution_for_reused_parts",
"adjudicator_use": "secondary",
"gap_notes": "Matching modules exist and API-backed; relationship adjudicator still needs a formal bridge.",
"route_notes": "Local matching is already stronger than a thin external call unless you need an oracle comparison.",
},
{
"vedastro_capability": "Tajika Annual",
@@ -122,10 +145,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [
"local_assets": ["tajika.py", "varshaphala.py", "sahams", "solar_return"],
"can_call_vedastro": True,
"recommended_path": "hybrid_local_plus_vedastro",
"fastest_path_lane": "official_python_bridge",
"priority": "P0",
"license_boundary": "local_native_or_external_oracle",
"adjudicator_use": "secondary",
"gap_notes": "Annual modules exist; yearly career/wealth/month windows need stronger strict-workflow integration.",
"route_notes": "Use Python bridge for breadth, but keep year-chart reasoning and labels local.",
},
{
"vedastro_capability": "Prashna / Horary",
@@ -134,10 +159,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [
"local_assets": ["prashna.py", "kp_system.py", "upagraha_gulika_maandi", "sphuta_trisphuta_family"],
"can_call_vedastro": True,
"recommended_path": "local_native",
"fastest_path_lane": "local_native_preferred",
"priority": "P0",
"license_boundary": "local_native_or_clean_room",
"adjudicator_use": "secondary",
"gap_notes": "Horary modules exist but are not yet a first-class question adjudicator route.",
"route_notes": "No need to outsource core horary math while local modules already exist.",
},
{
"vedastro_capability": "Report Rendering",
@@ -146,22 +173,32 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [
"local_assets": ["report_artifact API", "report_builder.py", "chart_renderer.py", "jyotish-app export"],
"can_call_vedastro": False,
"recommended_path": "new_local_impl",
"fastest_path_lane": "local_native_preferred",
"priority": "P0",
"license_boundary": "local_native",
"adjudicator_use": "not_used",
"gap_notes": "HTML/PDF artifact path exists; polished SVG/PDF chart rendering and cloud-scale report production are not finished.",
"route_notes": "Rendering is a local product concern, not a VedAstro dependency.",
},
{
"vedastro_capability": "MCP / API Surface",
"category": "service_surface",
"domains": ["event", "relationship", "wealth", "career"],
"local_assets": ["mcp_server.py", "jyotish_api_server.py", "strict workflows", "vedastro_service_adapter.py"],
"local_assets": [
"mcp_server.py",
"jyotish_api_server.py",
"strict workflows",
"vedastro_service_adapter.py",
"vedastro_official_mcp_bridge.py",
],
"can_call_vedastro": True,
"recommended_path": "hybrid_local_plus_vedastro",
"fastest_path_lane": "official_mcp",
"priority": "P0",
"license_boundary": "external_service_or_local_native",
"adjudicator_use": "primary",
"gap_notes": "Local API/MCP surfaces exist; VedAstro live adapter needs endpoint-backed smoke tests and provenance capture.",
"gap_notes": "Local API/MCP surfaces exist and the official public MCP bridge is live; REST adapter official endpoint smoke still depends on configured endpoint-backed execution.",
"route_notes": "If official MCP is available, that is the fastest direct agent path; otherwise fall back to local REST adapter.",
},
{
"vedastro_capability": "Numerology / Non-Jyotish Tools",
@@ -170,10 +207,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [
"local_assets": [],
"can_call_vedastro": True,
"recommended_path": "external_evidence_only",
"fastest_path_lane": "external_evidence_only",
"priority": "P2",
"license_boundary": "external_service_only",
"adjudicator_use": "not_used",
"gap_notes": "Adjacent product feature; not required for Jyotish adjudicator depth.",
"route_notes": "Do not implement locally unless it becomes product-critical.",
},
{
"vedastro_capability": "Birth Time ML / Rectification Assistant",
@@ -182,10 +221,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [
"local_assets": ["birth_time_rectifier.py", "rectification_gate", "jyotish-app rectification"],
"can_call_vedastro": True,
"recommended_path": "hybrid_local_plus_vedastro",
"fastest_path_lane": "rest_adapter",
"priority": "P1",
"license_boundary": "external_service_or_local_native",
"adjudicator_use": "secondary",
"gap_notes": "Local rectification exists; ML parity with VedAstro-style service behavior is not established.",
"route_notes": "Use VedAstro externally only as supporting evidence; local rectification gate stays in control.",
},
]
@@ -232,10 +273,12 @@ def _build_row(seed: dict[str, Any], audit: dict[str, Any]) -> dict[str, Any]:
"local_assets": list(seed["local_assets"]),
"can_call_vedastro": bool(seed["can_call_vedastro"]),
"recommended_path": recommended_path,
"fastest_path_lane": seed["fastest_path_lane"],
"priority": seed["priority"],
"license_boundary": seed["license_boundary"],
"adjudicator_use": seed["adjudicator_use"],
"gap_notes": seed["gap_notes"],
"route_notes": seed["route_notes"],
}
@@ -246,10 +289,12 @@ def build_matrix(audit: dict[str, Any] | None = None) -> dict[str, Any]:
status_counts: dict[str, int] = {}
path_counts: dict[str, int] = {}
lane_counts: dict[str, int] = {}
priority_counts: dict[str, int] = {}
for row in rows:
status_counts[row["local_status"]] = status_counts.get(row["local_status"], 0) + 1
path_counts[row["recommended_path"]] = path_counts.get(row["recommended_path"], 0) + 1
lane_counts[row["fastest_path_lane"]] = lane_counts.get(row["fastest_path_lane"], 0) + 1
priority_counts[row["priority"]] = priority_counts.get(row["priority"], 0) + 1
return {
@@ -261,6 +306,7 @@ def build_matrix(audit: dict[str, Any] | None = None) -> dict[str, Any]:
"p0_count": priority_counts.get("P0", 0),
"status_counts": status_counts,
"recommended_path_counts": path_counts,
"fastest_path_lane_counts": lane_counts,
"local_registry_technique_count": audit.get("technique_count"),
},
"boundary": {
@@ -290,16 +336,17 @@ def render_markdown(matrix: dict[str, Any]) -> str:
"",
f"- Local status counts: `{json.dumps(summary['status_counts'], ensure_ascii=False, sort_keys=True)}`",
f"- Recommended path counts: `{json.dumps(summary['recommended_path_counts'], ensure_ascii=False, sort_keys=True)}`",
f"- Fastest path lane counts: `{json.dumps(summary['fastest_path_lane_counts'], ensure_ascii=False, sort_keys=True)}`",
"",
"## Matrix",
"",
"| VedAstro capability | Category | Local status | Path | Priority | Adjudicator use | Local assets | Gap notes |",
"|---|---|---:|---|---:|---|---|---|",
"| VedAstro capability | Category | Local status | Path | Fastest lane | Priority | Adjudicator use | Local assets | Gap notes |",
"|---|---|---:|---|---|---:|---|---|---|",
]
for row in matrix["rows"]:
assets = ", ".join(row["local_assets"]) if row["local_assets"] else "-"
lines.append(
"| {vedastro_capability} | {category} | {local_status} | {recommended_path} | {priority} | {adjudicator_use} | {assets} | {gap_notes} |".format(
"| {vedastro_capability} | {category} | {local_status} | {recommended_path} | {fastest_path_lane} | {priority} | {adjudicator_use} | {assets} | {gap_notes} |".format(
assets=assets,
**row,
)
+480
View File
@@ -0,0 +1,480 @@
#!/usr/bin/env python3
"""Thin bridge for the official VedAstro Python package.
This bridge exists to accelerate broad method coverage through the official
Python SDK while keeping the local adjudicator and REST adapter boundaries
separate. It stays deliberately thin:
- discover the real VedAstro runtime (current interpreter or local venv)
- route generic method calls into the official Python surface
- support a tiny typed-parameter contract for enums and Time/GeoLocation
- fail in a controlled way when the package is unavailable
"""
from __future__ import annotations
import argparse
import importlib.util
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
PACKAGE_NAME = "VedAstro"
MODULE_CANDIDATES = ("vedastro", "VedAstro")
FORCE_UNAVAILABLE_ENV = "VEDASTRO_PYTHON_FORCE_UNAVAILABLE"
STUB_RESULT_ENV = "VEDASTRO_PYTHON_BRIDGE_STUB_RESULT"
PYTHON_BIN_ENV = "VEDASTRO_PYTHON_BIN"
DEFAULT_VENV_PYTHONS = (
ROOT / "venv_vedastro" / "bin" / "python3.11",
ROOT / "venv_vedastro" / "bin" / "python3",
ROOT / "venv_vedastro" / "bin" / "python",
)
CHILD_RUNNER = r"""
import contextlib
import enum
import importlib
import io
import json
import sys
MODULE_CANDIDATES = ("vedastro", "VedAstro")
def _offset_to_string(offset):
if isinstance(offset, str):
raw = offset.strip()
if not raw:
return "+00:00"
if raw[0] in "+-" and ":" in raw:
return raw
try:
offset = float(raw)
except ValueError:
return raw
if isinstance(offset, (int, float)):
sign = "+" if offset >= 0 else "-"
absolute = abs(float(offset))
hours = int(absolute)
minutes = int(round((absolute - hours) * 60))
if minutes == 60:
hours += 1
minutes = 0
return f"{sign}{hours:02d}:{minutes:02d}"
return "+00:00"
def _import_module():
for name in MODULE_CANDIDATES:
with contextlib.redirect_stdout(io.StringIO()):
try:
return name, importlib.import_module(name)
except ModuleNotFoundError:
continue
return None, None
def _coerce(module, value):
if isinstance(value, list):
return [_coerce(module, item) for item in value]
if isinstance(value, dict):
enum_name = value.get("__vedastro_enum__")
if enum_name:
enum_type = getattr(module, enum_name)
return getattr(enum_type, value["value"])
type_name = value.get("__vedastro_type__")
if type_name == "GeoLocation":
return module.GeoLocation(
value["location_name"],
value["longitude"],
value["latitude"],
)
if type_name == "Time":
geolocation = _coerce(module, value["geolocation"]) if value.get("geolocation") else None
if value.get("time_string") is not None:
return module.Time(value["time_string"], geolocation)
time_string = (
f"{int(value['hour']):02d}:{int(value['minute']):02d} "
f"{int(value['day']):02d}/{int(value['month']):02d}/{int(value['year'])} "
f"{_offset_to_string(value.get('offset', '+00:00'))}"
)
return module.Time(time_string, geolocation)
if type_name:
target = getattr(module, type_name)
args = [_coerce(module, item) for item in value.get("args", [])]
kwargs = {
key: _coerce(module, item)
for key, item in value.get("kwargs", {}).items()
}
if args or kwargs:
return target(*args, **kwargs)
payload = {
key: _coerce(module, item)
for key, item in value.items()
if not key.startswith("__")
}
return target(**payload)
return {
key: _coerce(module, item)
for key, item in value.items()
}
return value
def _serialize(value):
if value is None or isinstance(value, (str, int, float, bool)):
return value
if isinstance(value, enum.Enum):
return {
"type": type(value).__name__,
"name": value.name,
"value": value.value,
}
if isinstance(value, (list, tuple, set)):
return [_serialize(item) for item in value]
if isinstance(value, dict):
return {str(key): _serialize(item) for key, item in value.items()}
if hasattr(value, "to_json"):
try:
payload = value.to_json()
if isinstance(payload, str):
try:
return json.loads(payload)
except json.JSONDecodeError:
return payload
return _serialize(payload)
except Exception:
pass
if hasattr(value, "__dict__"):
return {
key: _serialize(item)
for key, item in vars(value).items()
if not key.startswith("_")
}
return str(value)
def _resolve_callable(module, method_name):
parts = [part for part in method_name.split(".") if part]
if not parts:
raise AttributeError("empty_method_name")
if len(parts) == 1:
return getattr(module.Calculate, parts[0])
current = module
for part in parts:
current = getattr(current, part)
return current
def main():
method = sys.argv[1]
params = json.loads(sys.argv[2])
module_name, module = _import_module()
if module is None:
print(json.dumps({
"available": False,
"status": "python_package_not_installed",
"method": method,
"source": "vedastro_python_bridge_child",
}))
return
call_target = _resolve_callable(module, method)
args = []
kwargs = {}
if isinstance(params, dict) and ("args" in params or "kwargs" in params):
args = [_coerce(module, item) for item in params.get("args", [])]
kwargs = {
key: _coerce(module, item)
for key, item in params.get("kwargs", {}).items()
}
elif isinstance(params, dict):
kwargs = {
key: _coerce(module, item)
for key, item in params.items()
}
elif isinstance(params, list):
args = [_coerce(module, item) for item in params]
elif params is not None:
args = [_coerce(module, params)]
with contextlib.redirect_stdout(io.StringIO()):
result = call_target(*args, **kwargs)
print(json.dumps({
"available": True,
"status": "ok",
"method": method,
"module_name": module_name,
"result": _serialize(result),
"source": "vedastro_python_bridge",
}))
if __name__ == "__main__":
main()
"""
def _package_available() -> bool:
if os.environ.get(FORCE_UNAVAILABLE_ENV, "").strip().lower() in {"1", "true", "yes"}:
return False
return _discover_module_name() is not None
def _discover_module_name() -> str | None:
for candidate in MODULE_CANDIDATES:
if importlib.util.find_spec(candidate) is not None:
return candidate
return None
def _candidate_python_bins() -> list[str]:
bins: list[str] = []
configured = os.environ.get(PYTHON_BIN_ENV, "").strip()
if configured:
bins.append(configured)
for candidate in DEFAULT_VENV_PYTHONS:
if candidate.exists():
bins.append(str(candidate))
bins.append(sys.executable)
seen: set[str] = set()
ordered: list[str] = []
for item in bins:
if item and item not in seen:
ordered.append(item)
seen.add(item)
return ordered
def _python_supports_vedastro(python_bin: str) -> bool:
probe = (
"import importlib.util; "
"mods=('vedastro','VedAstro'); "
"raise SystemExit(0 if any(importlib.util.find_spec(m) for m in mods) else 1)"
)
try:
completed = subprocess.run(
[python_bin, "-c", probe],
check=False,
capture_output=True,
text=True,
timeout=20,
)
except OSError:
return False
return completed.returncode == 0
def _select_python_bin() -> str | None:
for python_bin in _candidate_python_bins():
if _python_supports_vedastro(python_bin):
return python_bin
return None
def _call_via_child_python(python_bin: str, method: str, params: dict[str, Any]) -> dict[str, Any]:
child_env = os.environ.copy()
completed = subprocess.run(
[python_bin, "-c", CHILD_RUNNER, method, json.dumps(params, ensure_ascii=False)],
check=False,
capture_output=True,
text=True,
timeout=180,
env=child_env,
)
if completed.returncode != 0:
return {
"available": False,
"status": "bridge_runtime_error",
"method": method,
"python_bin": python_bin,
"stderr": (completed.stderr or "").strip(),
"stdout_excerpt": (completed.stdout or "").strip()[:500],
"source": "vedastro_python_bridge",
}
try:
payload = json.loads(completed.stdout)
except json.JSONDecodeError:
return {
"available": False,
"status": "bridge_invalid_json",
"method": method,
"python_bin": python_bin,
"stdout_excerpt": (completed.stdout or "").strip()[:500],
"stderr": (completed.stderr or "").strip()[:500],
"source": "vedastro_python_bridge",
}
payload["python_bin"] = python_bin
return payload
def schema() -> dict[str, Any]:
return {
"bridge": "vedastro_python_bridge",
"package_name": PACKAGE_NAME,
"module_candidates": list(MODULE_CANDIDATES),
"intended_role": "python_sdk_bulk_calculation_bridge",
"operations": ["call_method"],
"request_contract": ["method", "params_json"],
"typed_param_contract": {
"enum": {"__vedastro_enum__": "PlanetName", "value": "Sun"},
"geo": {
"__vedastro_type__": "GeoLocation",
"location_name": "REDACTED_PLACE",
"longitude": 114.46,
"latitude": 36.6,
},
"time": {
"__vedastro_type__": "Time",
"year": REDACTED_YEAR,
"month": 4,
"day": 17,
"hour": 14,
"minute": 49,
"offset": 8,
"geolocation": {
"__vedastro_type__": "GeoLocation",
"location_name": "REDACTED_PLACE",
"longitude": 114.46,
"latitude": 36.6,
},
},
},
"response_contract": ["available", "status", "method", "result", "source", "python_bin"],
"boundaries": [
"Generic method routing only; local adjudicator remains authoritative.",
"Do not treat Python SDK output as score/dominant label override by default.",
],
}
def _missing_package_result(method: str) -> dict[str, Any]:
return {
"available": False,
"status": "python_package_not_installed",
"method": method,
"reason": (
f"Official Python package {PACKAGE_NAME} is not installed in the current runtime "
"or detected project venv."
),
"source": "vedastro_python_bridge",
}
def call_method(method: str, params: dict[str, Any]) -> dict[str, Any]:
stub = os.environ.get(STUB_RESULT_ENV, "").strip()
if stub:
payload = json.loads(stub)
return {
"available": True,
"status": "ok",
"method": payload.get("method") or method,
"result": payload.get("result"),
"source": "stubbed_bridge_result",
}
if os.environ.get(FORCE_UNAVAILABLE_ENV, "").strip().lower() in {"1", "true", "yes"}:
return _missing_package_result(method)
python_bin = _select_python_bin()
if not python_bin:
return _missing_package_result(method)
return _call_via_child_python(python_bin, method, params)
def call_high_value(method_key: str, payload: dict[str, Any]) -> dict[str, Any]:
if method_key == "event_tag_catalog":
return call_method("GetAllEventDataGroupedByTag", {})
if method_key == "planet_longitude":
planet = payload["planet"]
time = payload["time"]
return call_method(
"PlanetNirayanaLongitude",
{
"args": [
{"__vedastro_enum__": "PlanetName", "value": planet},
time,
]
},
)
if method_key == "vimshottari_snapshot":
birth_time = payload["birth_time"]
check_time = payload["check_time"]
levels = int(payload.get("levels", 3))
return call_method("DasaAtTime", {"args": [birth_time, check_time, levels]})
if method_key == "chara_dasha_snapshot":
birth_time = payload["birth_time"]
check_time = payload["check_time"]
return call_method("GetCharaDasaAtTime", {"args": [birth_time, check_time]})
return {
"available": False,
"status": "unsupported_high_value_method",
"method": method_key,
"source": "vedastro_python_bridge",
}
def main() -> int:
parser = argparse.ArgumentParser(description="VedAstro Python bridge")
parser.add_argument("--print-schema", action="store_true")
parser.add_argument("--method", default="")
parser.add_argument("--params-json", default="{}")
parser.add_argument("--high-value", default="")
args = parser.parse_args()
if args.print_schema:
result = schema()
result["high_value_methods"] = {
"event_tag_catalog": {
"maps_to": "GetAllEventDataGroupedByTag",
"request_contract": [],
},
"planet_longitude": {
"maps_to": "PlanetNirayanaLongitude",
"request_contract": ["planet", "time"],
},
"vimshottari_snapshot": {
"maps_to": "DasaAtTime",
"request_contract": ["birth_time", "check_time", "levels?"],
},
"chara_dasha_snapshot": {
"maps_to": "GetCharaDasaAtTime",
"request_contract": ["birth_time", "check_time"],
},
}
elif args.high_value:
params = json.loads(args.params_json or "{}")
result = call_high_value(args.high_value, params)
else:
params = json.loads(args.params_json or "{}")
result = call_method(args.method, params)
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())