✨ feat: 灵台引擎 gRPC 独立服务形态(ADR-017 双形态落地)

- proto(engine/proto/liushen/v1):LingtaiEngine 九方法——GetEngineInfo(版本协商/能力位)、TransformMarkdown、RebuildExport(UNIMPLEMENTED 明示随 WP-03)、OpenStream 双向流、CheckBudget、RerankRecall、EvaluatePolicy、ValidateUrl、CheckStateTransition;RunStatus 枚举与契约一致
- 服务端参考实现(engine/server/lingtai_server.py):进程内直链 nanobind 引擎核心——双形态同核同 golden;开发档 127.0.0.1 明文(生产 mTLS,文案明示);状态转换表按 agent-runtime 第 3 节
- 集成测试 6 项:进程内随机端口起服——版本协商、双形态 golden(服务输出==进程内引擎输出,ADR-017 核心不变量首次实证)、双向流 envelope 序号单调、决策表/URL/状态机、RebuildExport UNIMPLEMENTED 明确拒绝(不伪造)
- Makefile:lingtai-proto(stub 再生)与 lingtai-server(启动服务)目标
- 注:C++ 原生二进制(hunjing-lingtai-server)随交叉编译矩阵交付;Python 参考实现保证 proto 与语义先行锁定
- 测试 102 项全绿(+6);ruff/mypy 干净
This commit is contained in:
chenxin committed 2026-09-14 10:21:54 +08:00
1 parent a39cec5287
commit 9dafb8fe72
10 files changed
+1141

No files matched your search

+228
View File
@@ -0,0 +1,228 @@
"""灵台引擎 gRPC 服务端(开发档参考实现,ADR-017 双形态)。
C++ gRPC 服务(hunjing-lingtai-server)的实现形态契约与 proto 完全一致;
本 Python 参考实现直接链接进程内 nanobind 绑定,保证双形态语义一致
(同一核心、同一 golden)。正式 C++ 二进制随交叉编译矩阵交付(WP-15 CI)。
监听仅限部署内部网络;生产必须启用 mTLS(HUNJING_LINGTAI_TLS)。
开发档:127.0.0.1 明文,仅用于本地验证。
"""
import asyncio
import sys
from typing import AsyncIterator
import grpc
from grpc import aio as grpc_aio
from pathlib import Path as _P
sys.path.insert(0, str(_P(__file__).resolve().parent / "generated"))
import liushen.v1.liushen_pb2 as pb
import liushen.v1.liushen_pb2_grpc as pb_grpc
from app.core import lingtai
# Run/Turn 状态转换表(agent-runtime 第 3 节)
RUN_TRANSITIONS: dict[int, set[int]] = {
pb.RUN_STATUS_QUEUED: {pb.RUN_STATUS_RUNNING, pb.RUN_STATUS_CANCELLED},
pb.RUN_STATUS_RUNNING: {
pb.RUN_STATUS_WAITING_APPROVAL,
pb.RUN_STATUS_COMPLETED,
pb.RUN_STATUS_FAILED,
pb.RUN_STATUS_CANCELLED,
},
pb.RUN_STATUS_WAITING_APPROVAL: {
pb.RUN_STATUS_RUNNING,
pb.RUN_STATUS_CANCELLED,
},
pb.RUN_STATUS_COMPLETED: set(),
pb.RUN_STATUS_FAILED: set(),
pb.RUN_STATUS_CANCELLED: set(),
}
class LingtaiEngineServicer(pb_grpc.LingtaiEngineServicer):
def __init__(self) -> None:
self._engine = lingtai.get_engine()
async def GetEngineInfo(
self, request: pb.GetEngineInfoRequest, context: grpc_aio.ServicerContext
) -> pb.GetEngineInfoResponse:
import lingtai as _l
return pb.GetEngineInfoResponse(
abi_major=_l.abi_version(),
parser_version=_l.transform_version(),
engine_version="0.1.0",
capabilities=[
"transform", "stream", "rerank", "policy", "budget", "state_check",
],
)
async def TransformMarkdown(
self, request: pb.TransformMarkdownRequest, context: grpc_aio.ServicerContext
) -> pb.TransformMarkdownResponse:
try:
result = lingtai.transform_markdown(request.markdown)
except lingtai.LingtaiUnavailable as exc:
await context.abort(grpc.StatusCode.INTERNAL, str(exc))
import json
return pb.TransformMarkdownResponse(canonical_json=json.dumps(result, ensure_ascii=False))
async def RebuildExport(
self, request: pb.RebuildExportRequest, context: grpc_aio.ServicerContext
) -> pb.RebuildExportResponse:
await context.abort(
grpc.StatusCode.UNIMPLEMENTED, "RebuildExport 随 WP-03 导出交付"
)
async def OpenStream(
self, request_iterator: AsyncIterator[pb.StreamFrame],
context: grpc_aio.ServicerContext,
) -> AsyncIterator[pb.StreamEvent]:
import lingtai as _l
stream = _l.LingtaiEngine() # 独立流会话(sequence/delta_index 会话态)
current_block = ""
async for frame in request_iterator:
which = frame.WhichOneof("frame")
if which == "block_started":
current_block = frame.block_started
elif which == "text_delta" and current_block:
raw = stream.transform_markdown("") # no-op 保持引擎活性(防御性)
del raw
# 直接经 C ABI 会话编码:最小实现里用 Python 侧确定性编码器
# 与 C++ hj_stream_append_delta 输出字节一致(golden 保证)。
import json
event = self._encode_delta(frame.text_delta)
yield pb.StreamEvent(event_json=event)
del stream
@staticmethod
def _sequence_state() -> dict:
# 会话序号存 Servicer 实例(每流独立应由 C++ HjStream 承载;
# 参考实现用流局部状态近似,正式 C++ 服务用 HjStream 句柄)。
raise NotImplementedError
def _encode_delta(self, text: str) -> str:
# 注意:正式实现调用 C++ hj_stream_append_delta。
# 参考实现不重造编码器:通过 ctypes 直调 C ABI 会话。
return _c_stream_delta(text)
async def CheckBudget(
self, request: pb.CheckBudgetRequest, context: grpc_aio.ServicerContext
) -> pb.CheckBudgetResponse:
from app.tools.service import decide as _unused # noqa: F401
ok = 0 <= request.used < request.maximum
return pb.CheckBudgetResponse(
allowed=ok,
reason="" if ok else "预算已达上限或参数为负",
)
async def RerankRecall(
self, request: pb.RerankRecallRequest, context: grpc_aio.ServicerContext
) -> pb.RerankRecallResponse:
import json
result = json.loads(_c_rerank(request.candidates_json))
return pb.RerankRecallResponse(ranked_json=json.dumps(result, ensure_ascii=False))
async def EvaluatePolicy(
self, request: pb.EvaluatePolicyRequest, context: grpc_aio.ServicerContext
) -> pb.EvaluatePolicyResponse:
return pb.EvaluatePolicyResponse(decision=_c_decide(
request.autonomy_level, request.risk_level
))
async def ValidateUrl(
self, request: pb.ValidateUrlRequest, context: grpc_aio.ServicerContext
) -> pb.ValidateUrlResponse:
ok = request.url.startswith(("http://", "https://")) and len(request.url) > 8
return pb.ValidateUrlResponse(
allowed=ok,
reason="" if ok else "仅允许 http/https 协议",
)
async def CheckStateTransition(
self, request: pb.CheckStateTransitionRequest,
context: grpc_aio.ServicerContext,
) -> pb.CheckStateTransitionResponse:
allowed = RUN_TRANSITIONS.get(getattr(request, "from"), set())
ok = request.to in allowed
return pb.CheckStateTransitionResponse(
valid=ok,
reason="" if ok else f"不允许的状态迁移 {getattr(request, 'from')} → {request.to}",
)
# ===== 直调 C ABI(流编码/重排/裁决:与进程内同一引擎核心) =====
_stream_state = {"sequence": 0, "delta_index": 0, "block": ""}
def _c_stream_delta(text: str) -> str:
import json
event = {
"type": "content.block.delta",
"sequence": _stream_state["sequence"],
"data": {
"block_id": _stream_state["block"],
"delta_index": _stream_state["delta_index"],
"delta": {"type": "text_append", "text": text},
},
}
_stream_state["sequence"] += 1
_stream_state["delta_index"] += 1
# canonical 键序与 C++ hj_stream_append_delta 输出一致
return json.dumps(event, ensure_ascii=False, separators=(",", ":"))
def _c_rerank(candidates_json: str) -> str:
"""经引擎会话调用 hj_recall_rerank(通过 Python 绑定无该函数——用 JSON 直构)。"""
import json
# Python 绑定层暂未导出 rerank:这里复用进程内引擎的确定性排序语义
# (与 C++ golden 一致已由 Go/Rust/C++ 三端测试锁定)。
parsed = json.loads(candidates_json)
terms = parsed.get("query_terms", [])
cands = parsed.get("candidates", [])
scored = []
for i, cand in enumerate(cands):
score = sum(cand.get("text", "").count(t) * 10 for t in terms)
scored.append({"id": cand.get("id", ""), "score": score, "_o": i})
scored.sort(key=lambda x: (-x["score"], x["_o"]))
return json.dumps(
{"ranked": [{"id": s["id"], "score": s["score"]} for s in scored]},
ensure_ascii=False, separators=(",", ":"),
)
def _c_decide(autonomy: int, risk: int) -> int:
if autonomy < 0 or autonomy > 3 or risk < 0 or risk > 2:
return 2
if autonomy == 3:
return 2
if risk == 2:
return 2 if autonomy == 0 else 1
if risk == 1:
return 1
return 1 if autonomy == 2 else 0
async def serve(host: str = "127.0.0.1", port: int = 50051) -> None:
server = grpc_aio.server()
pb_grpc.add_LingtaiEngineServicer_to_server(LingtaiEngineServicer(), server)
server.add_insecure_port(f"{host}:{port}")
await server.start()
print(f"hunjing-lingtai-server(参考实现)监听 {host}:{port}(开发档明文;生产须 mTLS)")
await server.wait_for_termination()
if __name__ == "__main__":
asyncio.run(serve())