Files
chenxin 9dafb8fe72 ✨ 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 干净
2026-09-14 10:21:54 +08:00

229 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""灵台引擎 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())