Add active rectification API workflow
This commit is contained in:
@@ -344,6 +344,14 @@ async function computeRectificationGate(payload) {
|
|||||||
return postJson('/api/rectification_gate', payload);
|
return postJson('/api/rectification_gate', payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function computeActiveRectificationQuestions(payload) {
|
||||||
|
return postJson('/api/active_rectification_questions', payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function computeActiveRectificationScore(payload) {
|
||||||
|
return postJson('/api/active_rectification_score', payload);
|
||||||
|
}
|
||||||
|
|
||||||
async function computeCaseValidation(payload) {
|
async function computeCaseValidation(payload) {
|
||||||
return postJson('/api/case_validation', payload);
|
return postJson('/api/case_validation', payload);
|
||||||
}
|
}
|
||||||
@@ -512,6 +520,8 @@ window.JyotishAPI = {
|
|||||||
computeYogas,
|
computeYogas,
|
||||||
computeAspects,
|
computeAspects,
|
||||||
computeRectificationGate,
|
computeRectificationGate,
|
||||||
|
computeActiveRectificationQuestions,
|
||||||
|
computeActiveRectificationScore,
|
||||||
computeCaseValidation,
|
computeCaseValidation,
|
||||||
getRealCaseRevalidation,
|
getRealCaseRevalidation,
|
||||||
computeDivisionalYoga,
|
computeDivisionalYoga,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ async function postJson(path, payload, { requireModernChart = false } = {}) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
activeApiBase = base;
|
activeApiBase = base;
|
||||||
|
if (data?.mode === 'async_submitted') return pollAsyncJob(data, { base });
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastAttempt = `${base}${path}`;
|
lastAttempt = `${base}${path}`;
|
||||||
@@ -60,6 +61,22 @@ async function postJson(path, payload, { requireModernChart = false } = {}) {
|
|||||||
throw lastError || new Error(buildAPIRecoveryMessage(path, '本地 API 未连接', lastAttempt));
|
throw lastError || new Error(buildAPIRecoveryMessage(path, '本地 API 未连接', lastAttempt));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function pollAsyncJob(job, { base = activeApiBase, timeoutMs = 120000, intervalMs = 500 } = {}) {
|
||||||
|
if (!job?.poll_path || !job?.access_token) throw new Error('Async job response missing poll capability');
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
const resp = await fetch(`${base}${job.poll_path}`, {
|
||||||
|
headers: { Authorization: `Bearer ${job.access_token}` },
|
||||||
|
});
|
||||||
|
const data = await parseApiResponse(resp);
|
||||||
|
if (!resp.ok) throw new Error(buildAPIRecoveryMessage(job.poll_path, data?.error || `Job poll failed (${resp.status})`));
|
||||||
|
if (data.status === 'completed') return data.result || data;
|
||||||
|
if (data.status === 'failed') throw new Error(data.error || 'Async job failed');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
||||||
|
}
|
||||||
|
throw new Error(buildAPIRecoveryMessage(job.poll_path, 'Async job timed out'));
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchJson(path) {
|
async function fetchJson(path) {
|
||||||
let lastError = null;
|
let lastError = null;
|
||||||
let lastAttempt = null;
|
let lastAttempt = null;
|
||||||
@@ -327,6 +344,14 @@ async function computeRectificationGate(payload) {
|
|||||||
return postJson('/api/rectification_gate', payload);
|
return postJson('/api/rectification_gate', payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function computeActiveRectificationQuestions(payload) {
|
||||||
|
return postJson('/api/active_rectification_questions', payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function computeActiveRectificationScore(payload) {
|
||||||
|
return postJson('/api/active_rectification_score', payload);
|
||||||
|
}
|
||||||
|
|
||||||
async function computeCaseValidation(payload) {
|
async function computeCaseValidation(payload) {
|
||||||
return postJson('/api/case_validation', payload);
|
return postJson('/api/case_validation', payload);
|
||||||
}
|
}
|
||||||
@@ -495,12 +520,15 @@ window.JyotishAPI = {
|
|||||||
computeYogas,
|
computeYogas,
|
||||||
computeAspects,
|
computeAspects,
|
||||||
computeRectificationGate,
|
computeRectificationGate,
|
||||||
|
computeActiveRectificationQuestions,
|
||||||
|
computeActiveRectificationScore,
|
||||||
computeCaseValidation,
|
computeCaseValidation,
|
||||||
getRealCaseRevalidation,
|
getRealCaseRevalidation,
|
||||||
computeDivisionalYoga,
|
computeDivisionalYoga,
|
||||||
computeKakshya,
|
computeKakshya,
|
||||||
computeBhavaBala,
|
computeBhavaBala,
|
||||||
computeTransitTriggers,
|
computeTransitTriggers,
|
||||||
|
pollAsyncJob,
|
||||||
// AI 解读
|
// AI 解读
|
||||||
aiReading,
|
aiReading,
|
||||||
aiFullReading,
|
aiFullReading,
|
||||||
|
|||||||
@@ -1035,6 +1035,8 @@ API_COMMAND_MAP = {
|
|||||||
'yoga': '/api/yogas',
|
'yoga': '/api/yogas',
|
||||||
'aspects': '/api/aspects',
|
'aspects': '/api/aspects',
|
||||||
'rectification': '/api/rectification_gate',
|
'rectification': '/api/rectification_gate',
|
||||||
|
'active-rectification-questions': '/api/active_rectification_questions',
|
||||||
|
'active-rectification-score': '/api/active_rectification_score',
|
||||||
'case-validation': '/api/case_validation',
|
'case-validation': '/api/case_validation',
|
||||||
'divisional-yoga': '/api/divisional_yoga',
|
'divisional-yoga': '/api/divisional_yoga',
|
||||||
'deep-varga-avastha': '/api/deep_varga_avastha',
|
'deep-varga-avastha': '/api/deep_varga_avastha',
|
||||||
@@ -1066,6 +1068,8 @@ TECHNIQUE_EXAMPLE_ENDPOINTS = {
|
|||||||
'/api/pancha_mahapurusha',
|
'/api/pancha_mahapurusha',
|
||||||
'/api/prashna',
|
'/api/prashna',
|
||||||
'/api/rectification_gate',
|
'/api/rectification_gate',
|
||||||
|
'/api/active_rectification_questions',
|
||||||
|
'/api/active_rectification_score',
|
||||||
'/api/relationship',
|
'/api/relationship',
|
||||||
'/api/remedies',
|
'/api/remedies',
|
||||||
'/api/sade_sati',
|
'/api/sade_sati',
|
||||||
@@ -1478,6 +1482,12 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
|||||||
elif path == '/api/rectification_gate':
|
elif path == '/api/rectification_gate':
|
||||||
result = self._compute_rectification_gate(body)
|
result = self._compute_rectification_gate(body)
|
||||||
self._json(result)
|
self._json(result)
|
||||||
|
elif path == '/api/active_rectification_questions':
|
||||||
|
result = self._compute_active_rectification_questions(body)
|
||||||
|
self._json(result)
|
||||||
|
elif path == '/api/active_rectification_score':
|
||||||
|
result = self._compute_active_rectification_score(body)
|
||||||
|
self._json(result)
|
||||||
elif path == '/api/case_validation':
|
elif path == '/api/case_validation':
|
||||||
result = self._compute_case_validation(body)
|
result = self._compute_case_validation(body)
|
||||||
self._json(result)
|
self._json(result)
|
||||||
@@ -6327,6 +6337,46 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _compute_active_rectification_questions(self, body):
|
||||||
|
birth_time = body.get('birth_time')
|
||||||
|
if not isinstance(birth_time, str) or not birth_time.strip():
|
||||||
|
raise BadRequest('birth_time must be a string')
|
||||||
|
uncertainty_minutes = self._get_int(body, 'uncertainty_minutes', 30)
|
||||||
|
if not 1 <= uncertainty_minutes <= 180:
|
||||||
|
raise BadRequest('uncertainty_minutes must be between 1 and 180')
|
||||||
|
step_minutes = self._get_int(body, 'step_minutes', 1)
|
||||||
|
if not 1 <= step_minutes <= 30:
|
||||||
|
raise BadRequest('step_minutes must be between 1 and 30')
|
||||||
|
try:
|
||||||
|
module = _load_local_module('active_rectification_questions')
|
||||||
|
result = module.build_questionnaire(
|
||||||
|
birth_time.strip(),
|
||||||
|
uncertainty_minutes=uncertainty_minutes,
|
||||||
|
step_minutes=step_minutes,
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
raise BadRequest('birth_time must be YYYY-MM-DD HH:MM') from e
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'endpoint': 'active_rectification_questions',
|
||||||
|
**result,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _compute_active_rectification_score(self, body):
|
||||||
|
questionnaire = body.get('questionnaire')
|
||||||
|
if not isinstance(questionnaire, dict):
|
||||||
|
raise BadRequest('questionnaire must be an object')
|
||||||
|
answers = body.get('answers')
|
||||||
|
if not isinstance(answers, dict):
|
||||||
|
raise BadRequest('answers must be an object')
|
||||||
|
module = _load_local_module('active_rectification_questions')
|
||||||
|
result = module.score_answers(questionnaire, answers)
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'endpoint': 'active_rectification_score',
|
||||||
|
**result,
|
||||||
|
}
|
||||||
|
|
||||||
def _compute_case_validation(self, body):
|
def _compute_case_validation(self, body):
|
||||||
planets, _, _ = self._normalized_planets_from_body(body)
|
planets, _, _ = self._normalized_planets_from_body(body)
|
||||||
current_md = body.get('current_md', body.get('dasha_lord', ''))
|
current_md = body.get('current_md', body.get('dasha_lord', ''))
|
||||||
@@ -7087,6 +7137,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
|||||||
'/api/pancha_mahapurusha': self._compute_pmc,
|
'/api/pancha_mahapurusha': self._compute_pmc,
|
||||||
'/api/prashna': self._compute_prashna,
|
'/api/prashna': self._compute_prashna,
|
||||||
'/api/rectification_gate': self._compute_rectification_gate,
|
'/api/rectification_gate': self._compute_rectification_gate,
|
||||||
|
'/api/active_rectification_questions': self._compute_active_rectification_questions,
|
||||||
|
'/api/active_rectification_score': self._compute_active_rectification_score,
|
||||||
'/api/relationship': self._compute_relationship,
|
'/api/relationship': self._compute_relationship,
|
||||||
'/api/remedies': self._compute_remedies,
|
'/api/remedies': self._compute_remedies,
|
||||||
'/api/sade_sati': self._compute_sade_sati,
|
'/api/sade_sati': self._compute_sade_sati,
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
SCRIPTS = Path(__file__).resolve().parents[1] / "scripts"
|
||||||
|
if str(SCRIPTS) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SCRIPTS))
|
||||||
|
|
||||||
|
from jyotish_api_server import BadRequest, JyotishAPIHandler # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _handler() -> JyotishAPIHandler:
|
||||||
|
return JyotishAPIHandler.__new__(JyotishAPIHandler)
|
||||||
|
|
||||||
|
|
||||||
|
def test_active_rectification_questions_api_builds_choice_workflow() -> None:
|
||||||
|
result = _handler()._compute_active_rectification_questions(
|
||||||
|
{
|
||||||
|
"birth_time": "1993-04-17 14:49",
|
||||||
|
"uncertainty_minutes": 30,
|
||||||
|
"step_minutes": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["success"] is True
|
||||||
|
assert result["endpoint"] == "active_rectification_questions"
|
||||||
|
assert result["scope"] == "active_birth_time_rectification_questionnaire"
|
||||||
|
assert result["candidate_scan"]["start"] == "1993-04-17 14:19"
|
||||||
|
assert result["candidate_scan"]["end"] == "1993-04-17 15:19"
|
||||||
|
assert result["candidate_scan"]["candidate_count"] == 61
|
||||||
|
assert result["questions"]
|
||||||
|
assert {option["key"] for option in result["questions"][0]["options"]} == {"A", "B", "C", "D"}
|
||||||
|
assert "dynamic_candidate_cluster_scoring" in result["workflow"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_active_rectification_score_api_returns_rankings_and_next_questions() -> None:
|
||||||
|
questionnaire = _handler()._compute_active_rectification_questions(
|
||||||
|
{"birth_time": "1993-04-17 14:49", "uncertainty_minutes": 30}
|
||||||
|
)
|
||||||
|
scored = _handler()._compute_active_rectification_score(
|
||||||
|
{
|
||||||
|
"questionnaire": questionnaire,
|
||||||
|
"answers": {
|
||||||
|
"education_environment_shift": "A",
|
||||||
|
"residence_relocation_shift": "B",
|
||||||
|
"relationship_or_partner_entry": "D",
|
||||||
|
"career_responsibility_pressure": "A",
|
||||||
|
"research_tool_expression_shift": "C",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert scored["success"] is True
|
||||||
|
assert scored["endpoint"] == "active_rectification_score"
|
||||||
|
assert scored["scope"] == "active_birth_time_rectification_scoring"
|
||||||
|
assert scored["answered_count"] == 5
|
||||||
|
assert scored["candidate_cluster_rankings"]
|
||||||
|
assert scored["next_round_questions"]
|
||||||
|
assert scored["candidate_cluster_rankings"][0]["score"] >= scored["candidate_cluster_rankings"][-1]["score"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_active_rectification_questions_api_validates_request() -> None:
|
||||||
|
with pytest.raises(BadRequest, match="birth_time must be a string"):
|
||||||
|
_handler()._compute_active_rectification_questions({})
|
||||||
|
|
||||||
|
with pytest.raises(BadRequest, match="uncertainty_minutes must be between 1 and 180"):
|
||||||
|
_handler()._compute_active_rectification_questions(
|
||||||
|
{"birth_time": "1993-04-17 14:49", "uncertainty_minutes": 0}
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(BadRequest, match="step_minutes must be between 1 and 30"):
|
||||||
|
_handler()._compute_active_rectification_questions(
|
||||||
|
{"birth_time": "1993-04-17 14:49", "step_minutes": 31}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_active_rectification_score_api_validates_payload() -> None:
|
||||||
|
with pytest.raises(BadRequest, match="questionnaire must be an object"):
|
||||||
|
_handler()._compute_active_rectification_score({"answers": {}})
|
||||||
|
|
||||||
|
with pytest.raises(BadRequest, match="answers must be an object"):
|
||||||
|
_handler()._compute_active_rectification_score({"questionnaire": {}})
|
||||||
@@ -1397,6 +1397,8 @@ def test_api_bridge_exports_productized_backend_actions() -> None:
|
|||||||
"computeYogas",
|
"computeYogas",
|
||||||
"computeAspects",
|
"computeAspects",
|
||||||
"computeRectificationGate",
|
"computeRectificationGate",
|
||||||
|
"computeActiveRectificationQuestions",
|
||||||
|
"computeActiveRectificationScore",
|
||||||
"computeCaseValidation",
|
"computeCaseValidation",
|
||||||
"computeDivisionalYoga",
|
"computeDivisionalYoga",
|
||||||
"computeKakshya",
|
"computeKakshya",
|
||||||
|
|||||||
Reference in New Issue
Block a user