feat: add configurable Bailian AI cell call runtime

This commit is contained in:
2026-09-14 23:10:39 +08:00
parent 9bade94ada
commit 5dd69b8779
36 changed files with 6455 additions and 30 deletions
+424
View File
@@ -0,0 +1,424 @@
#!/usr/bin/env python3
"""Run isolated and, when configured, real Bailian AI acceptance probes."""
from __future__ import annotations
import hashlib
import json
import re
import subprocess
import sys
import tempfile
import wave
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
CLI = ROOT / "scripts" / "test_ai_call.py"
MOCK_PROFILE = ROOT / "configs" / "ai-test.example.yaml"
REAL_PROFILE = ROOT / "configs" / "ai-test.bailian.example.yaml"
def _now() -> datetime:
return datetime.now(timezone.utc)
def _run_cli(kind: str, *extra: str) -> tuple[int, dict[str, Any]]:
with tempfile.TemporaryDirectory(prefix="agent-call-accept-") as directory:
output = Path(directory) / "report.json"
completed = subprocess.run(
[sys.executable, str(CLI), kind, "--output", str(output), *extra],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)
try:
report = json.loads(completed.stdout)
except json.JSONDecodeError as exc:
raise RuntimeError(
f"AI CLI emitted no JSON for {kind}: {completed.stderr[-500:]}"
) from exc
return completed.returncode, report
def _summary(kind: str, code: int, report: dict[str, Any]) -> dict[str, Any]:
result = report.get("result")
result = result if isinstance(result, dict) else {}
execution = report.get("execution")
execution = execution if isinstance(execution, dict) else {}
latest_execution = report.get("latest_execution")
latest_execution = latest_execution if isinstance(latest_execution, dict) else {}
recording = report.get("recording")
recording = recording if isinstance(recording, dict) else {}
return {
"kind": kind,
"exit_code": code,
"status": report.get("status"),
"mode": report.get("mode"),
"requested_mode": report.get("requested_mode"),
"provider_modes": report.get("provider_modes"),
"agent_version_id": report.get("agent_version_id"),
"agent_config_sha256": report.get("agent_config_sha256"),
"prompt_sha256": report.get("prompt_sha256"),
"llm_requested_model": report.get("llm", {}).get("requested_model"),
"llm_returned_model": report.get("llm", {}).get("provider_returned_model"),
"tts_requested_model": report.get("tts", {}).get("requested_model"),
"tts_returned_model": report.get("tts", {}).get("provider_returned_model"),
"tts_returned_model_verified": report.get("tts", {}).get(
"provider_returned_model_verified", False
),
"tts_model_evidence": report.get("tts", {}).get("model_evidence"),
"voice_sha256": hashlib.sha256(
str(report.get("tts", {}).get("voice", "")).encode()
).hexdigest(),
"asr_provider_ref": report.get("asr", {}).get("provider_ref"),
"asr_segment_count": len(result.get("asr_segments", [])),
"llm_first_token_ms": result.get("llm_first_token_ms"),
"tts_first_audio_ms": result.get("tts_first_audio_ms"),
"audio_sha256": report.get("audio_output", {}).get("sha256"),
"audio_bytes": report.get("audio_output", {}).get("bytes"),
"call_id": report.get("call_id")
or result.get("call_id")
or latest_execution.get("call_id")
or execution.get("call_id"),
"reason_code": report.get("reason_code")
or result.get("reason_code")
or latest_execution.get("reason_code")
or execution.get("reason_code"),
"recording_bytes": recording.get("bytes"),
"recording_valid": recording.get("valid_wav") or recording.get("valid_audio"),
}
def _wav(path: Path) -> None:
with wave.open(str(path), "wb") as output:
output.setnchannels(1)
output.setsampwidth(2)
output.setframerate(16000)
output.writeframes(b"\x00\x00" * 1600)
def _case(
case_id: str, status: str, actual: str, evidence: list[str], mode: str
) -> dict[str, Any]:
return {
"case_id": case_id,
"mode": mode,
"status": status,
"actual": actual,
"evidence": evidence,
}
def _configured() -> bool:
import os
return all(
os.environ.get(name)
for name in (
"BAILIAN_API_KEY",
"BAILIAN_BASE_URL",
"BAILIAN_WSS_BASE_URL",
"BAILIAN_TTS_VOICE",
)
)
def main() -> int:
date = _now().strftime("%Y%m%d")
evidence_dir = ROOT / "docs" / "evidence"
evidence_dir.mkdir(parents=True, exist_ok=True)
evidence_path = evidence_dir / f"llm-voice-acceptance-{date}.json"
regression = subprocess.run(
[sys.executable, "-m", "unittest", "discover", "-s", "tests", "-v"],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)
match = re.search(r"Ran (\d+) tests?", regression.stdout + regression.stderr)
test_count: int | None = None
if match:
try:
test_count = int(match.group(1))
except (TypeError, ValueError, OverflowError):
test_count = None
regression_summary = {"exit_code": regression.returncode, "test_count": test_count}
commands: list[dict[str, Any]] = []
mock_text_code, mock_text = _run_cli(
"text",
"--profile",
str(MOCK_PROFILE),
"--mode",
"mock",
"--input",
"这是隔离文本验收。",
)
commands.append(_summary("mock-text", mock_text_code, mock_text))
with tempfile.TemporaryDirectory(prefix="agent-call-accept-audio-") as directory:
source = Path(directory) / "input.wav"
target = Path(directory) / "response.wav"
_wav(source)
mock_audio_code, mock_audio = _run_cli(
"audio",
"--profile",
str(MOCK_PROFILE),
"--mode",
"mock",
"--input",
str(source),
"--output-audio",
str(target),
)
commands.append(_summary("mock-audio", mock_audio_code, mock_audio))
mock_call_code, mock_call = _run_cli(
"call",
"--profile",
str(MOCK_PROFILE),
"--mode",
"mock",
"--callee",
"18601013734",
"--allow-real-call",
"--max-duration",
"120",
)
commands.append(_summary("mock-call", mock_call_code, mock_call))
real_ready = _configured()
real_text_code = real_audio_code = 2
real_text: dict[str, Any] = {
"status": "BLOCKED",
"reason_code": "BAILIAN_ENV_REQUIRED",
"message": "BAILIAN_API_KEY, BAILIAN_BASE_URL, BAILIAN_WSS_BASE_URL and BAILIAN_TTS_VOICE are required",
}
real_audio: dict[str, Any] = dict(real_text)
if real_ready:
with tempfile.TemporaryDirectory(prefix="agent-call-real-accept-") as directory:
generated = Path(directory) / "tts.wav"
real_text_code, real_text = _run_cli(
"text",
"--profile",
str(REAL_PROFILE),
"--mode",
"real",
"--input",
"请只回答收到。",
"--output-audio",
str(generated),
)
if generated.is_file():
real_audio_code, real_audio = _run_cli(
"audio",
"--profile",
str(REAL_PROFILE),
"--mode",
"real",
"--input",
str(generated),
)
else:
real_audio = {
"status": "BLOCKED",
"reason_code": "REAL_TTS_AUDIO_MISSING",
"message": "real text probe did not produce an audio fixture",
}
commands.append(_summary("real-text", real_text_code, real_text))
commands.append(_summary("real-audio", real_audio_code, real_audio))
real_call_evidence_path = evidence_dir / f"real-cell-call-{date}.json"
if real_call_evidence_path.is_file():
try:
loaded_call = json.loads(
real_call_evidence_path.read_text(encoding="utf-8")
)
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
loaded_call = None
if isinstance(loaded_call, dict):
real_call = loaded_call
real_call_code = 0 if real_call.get("status") == "PASS" else 1
else:
real_call_code = 2
real_call = {
"status": "BLOCKED",
"reason_code": "REAL_CALL_EVIDENCE_INVALID",
"message": "real Cell evidence exists but is not a valid JSON object",
}
else:
real_call_code = 2
real_call = {
"status": "BLOCKED",
"reason_code": "REAL_CALL_REQUIRES_SEPARATE_AUTHORIZATION",
"message": "Acceptance automation never dials; use the separately authorized Cell runner.",
}
commands.append(_summary("real-call-evidence", real_call_code, real_call))
mock_ok = (
regression.returncode == 0
and mock_text_code == 0
and mock_audio_code == 0
and mock_call_code == 0
)
provider_ok = real_text_code == 0 and real_audio_code == 0
cases: list[dict[str, Any]] = []
internal_pass = {
"AI-01": "tenant-scoped immutable snapshot was published and reused",
"AI-02": "missing, cross-tenant, and same-version conflict checks are covered",
"AI-03": "Prompt, model/voice, secret, and audio-format validation is covered",
"AI-04": "version digest is persisted with the execution snapshot",
"AI-09": "opening and Prompt constraints are configuration-driven in the runtime tests",
"AI-10": "PCM16/PCMA conversion, frame alignment, and bounded queues passed",
"AI-11": "cancellation and late-chunk discard passed with isolated providers",
"AI-12": "silence/no-answer lifecycle remains bounded in the mock executor",
"AI-13": "hangup/timeout cleanup remains isolated in the mock executor",
"AI-14": "provider errors and real-mode fail-closed behavior are covered",
"AI-15": "ARI reconciliation regression remains green for the existing mock path",
"AI-16": "duplicate publication/execution recovery remains green",
"AI-17": "text, playback, and MQ result facts remain separate",
"AI-18": "recording checksum/upload recovery regression remains green",
"AI-19": "real Cell ARI/RTP worker, queue routing, durable claim ledger, and no-redial tests are covered",
"AI-21": "reports use hashes and do not include provider credentials or URLs",
}
for case_id, actual in internal_pass.items():
cases.append(
_case(
case_id,
"PASS" if mock_ok else "FAIL",
actual,
[
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py",
],
"isolated-mock" if case_id != "AI-19" else "real-cell-code",
)
)
for case_id, label in (
("AI-05", "real text stream"),
("AI-06", "real TTS"),
("AI-07", "real audio chain"),
):
report = real_text if case_id in {"AI-05", "AI-06"} else real_audio
code = real_text_code if case_id in {"AI-05", "AI-06"} else real_audio_code
status = (
"PASS"
if code == 0 and report.get("status") == "PASS"
else ("FAIL" if real_ready else "BLOCKED")
)
if case_id in {"AI-06", "AI-07"} and status == "PASS":
status = "BLOCKED"
label += "; machine audio passed; human listening is not registered"
cases.append(
_case(
case_id,
status,
f"{label} {'passed' if code == 0 else 'was not completed'}",
["docs/evidence/llm-voice-acceptance-" + date + ".json"],
"real-bailian" if real_ready else "real-not-configured",
)
)
cases.append(
_case(
"AI-08",
"FAIL" if real_call_code == 1 else "BLOCKED",
"real three-round phone evidence is incomplete: SIP may connect, but bidirectional RTP, three AI rounds, recording handoff, and human listening are required",
[
str(Path("docs") / "evidence" / real_call_evidence_path.name)
if real_call_evidence_path.is_file()
else "docs/LLM与音色可配置电话对话_开发与验收计划_v1.0.md"
],
"real-phone-evidence"
if real_call_evidence_path.is_file()
else "real-phone-not-run",
)
)
cases.append(
_case(
"AI-20",
"BLOCKED",
"one-command mock flow passed; full real call flow remains incomplete until real MQ/OSS and phone evidence pass",
[
"scripts/test_ai_call.py",
"docs/evidence/llm-voice-acceptance-" + date + ".json",
],
"mixed",
)
)
blockers = [
"The single authorized real call reached SIP 200 OK, but usable bidirectional RTP, three AI rounds, and a valid recording were not proven.",
"The call path used a temporary RabbitMQ test broker; production RabbitMQ/SaaS result and OSS recording handoff remain unverified.",
"Human listening and provider-specific latency/quality thresholds are not registered; machine-generated WAV evidence cannot replace listening.",
]
if not real_ready:
blockers.insert(
0, "Bailian environment is incomplete; real provider probes were not run."
)
elif not provider_ok:
blockers.insert(
0,
"At least one real Bailian text/audio probe failed; inspect its redacted command summary.",
)
report = {
"schema_version": "1.0",
"generated_at": _now().isoformat().replace("+00:00", "Z"),
"scope": "real-bailian-file-probes-plus-isolated-mock-plus-real-cell-attempts",
"overall_status": "INCOMPLETE",
"gate_status": "BLOCKED_BY_REAL_PHONE_MEDIA_MQ_OSS_AND_HUMAN_EVIDENCE",
"component_modes": {
"llm": "real-bailian" if real_ready else "blocked",
"tts": "real-bailian" if real_ready else "blocked",
"asr": "real-bailian" if real_ready else "blocked",
"sip_ari_rtp": "partial-real"
if real_call_evidence_path.is_file()
else "not-run",
"database": "isolated-sqlite",
"rabbitmq": "temporary-test-broker"
if real_call_evidence_path.is_file()
else "not-run-real-broker",
"oss": "not-run",
},
"real_provider_preflight": {
"environment_configured": real_ready,
"text_code": real_text_code,
"audio_code": real_audio_code,
},
"regression_tests": regression_summary,
"commands": commands,
"cases": cases,
"blockers": blockers,
"evidence_policy": "docs/evidence contains hashes/status only; raw protocol/audio evidence stays in ignored local paths",
"test_commands": [
"python3 -m unittest discover -s tests -v",
"python3 scripts/test_ai_call.py text --profile configs/ai-test.bailian.example.yaml --mode real --input '请只回答收到。'",
"python3 scripts/test_ai_call.py audio --profile configs/ai-test.bailian.example.yaml --mode real --input <real-or-authorized-wav>",
"python3 scripts/run_real_cell_call.py --profile configs/ai-test.bailian.example.yaml --allow-real-call --callee 15003164745",
],
}
evidence_path.write_text(
json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
print(
json.dumps(
{
"overall_status": report["overall_status"],
"evidence": str(evidence_path),
"cases": len(cases),
},
ensure_ascii=False,
)
)
return (
1
if not mock_ok or (real_ready and not provider_ok) or real_call_code == 1
else 2
)
if __name__ == "__main__":
raise SystemExit(main())