M0:灵台 Kernel 骨架(事件溯源/命台席位/瀑布总线/插件注册)与神识客户端边界,新增 POST /runs 占位闭环与测试,Alembic 基线合并为单一起点

This commit is contained in:
chenxin committed 2026-08-21 21:58:09 +08:00
1 parent 99e55fa0a8
commit 7ef3a15061
23 files changed
+2014 -1399

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
"""神识客户端:灵台唯一的模型出口边界。
两种实现可互换(Protocol):
- ``HttpShenshiClient``:经 HTTP+SSE 调用 Go 神识服务(SHENSHI_BASE_URL 配置时启用);
- ``EchoShenshi``:进程内回声桩,逐字回流输入,供本地开发与测试在无神识时跑通端到端。
领域模块禁止绕过本边界 import Provider SDK(docs/architecture/service-boundaries.md §4)。
"""
from __future__ import annotations
import json
from collections.abc import AsyncIterator
from typing import Protocol
import httpx
from app.shenshi_client.types import ShenshiChatRequest, ShenshiDelta, ShenshiError
class ShenshiClient(Protocol):
"""神识数据面契约:流式对话补全。"""
def stream_chat(self, request: ShenshiChatRequest) -> AsyncIterator[ShenshiDelta]: ...
class EchoShenshi:
"""进程内回声桩:把最后一条用户消息按固定粒度切分回流。
只用于 M0 骨架验证(登录 → 占位 Run → SSE 端到端),不模拟任何 Provider 行为。
"""
chunk_size = 8
async def stream_chat(self, request: ShenshiChatRequest) -> AsyncIterator[ShenshiDelta]:
prompt = next((m.content for m in reversed(request.messages) if m.role == "user"), "")
text = f"【回声桩】{prompt}"
for i in range(0, len(text), self.chunk_size):
yield ShenshiDelta(delta=text[i : i + self.chunk_size])
yield ShenshiDelta(
finish_reason="stop",
usage_prompt_tokens=max(len(prompt) // 4, 1),
usage_completion_tokens=max(len(text) // 4, 1),
)
class HttpShenshiClient:
"""HTTP+SSE 神识客户端。
认证:服务令牌经 ``Authorization: Bearer`` 透传(管理面 RBAC 不在此层)。
流解析:``data:`` 帧为 ShenshiDelta JSON,``[DONE]`` 结束;非 2xx 映射 ShenshiError。
"""
def __init__(self, base_url: str, service_token: str, timeout: float = 120.0) -> None:
self._base_url = base_url.rstrip("/")
self._headers = {"Authorization": f"Bearer {service_token}"} if service_token else {}
self._timeout = timeout
async def stream_chat(self, request: ShenshiChatRequest) -> AsyncIterator[ShenshiDelta]:
payload = request.model_dump(mode="json")
assert payload.get("stream") is True # 数据面当前只有流式形态
async with httpx.AsyncClient(trust_env=False, timeout=self._timeout) as client:
async with client.stream(
"POST",
f"{self._base_url}/v1/chat/completions",
json=payload,
headers=self._headers,
) as response:
if response.status_code >= 400:
body = (await response.aread()).decode("utf-8", errors="replace")
code = f"HJ-2{max(response.status_code - 400, 0):03d}"
try:
detail = json.loads(body)
code = detail.get("error", {}).get("code", code)
body = detail.get("error", {}).get("message", body)
except json.JSONDecodeError:
pass
raise ShenshiError(code, body)
async for line in response.aiter_lines():
if not line.startswith("data:"):
continue
data = line[len("data:") :].strip()
if data == "[DONE]":
return
if data:
yield ShenshiDelta.model_validate_json(data)
def build_shenshi_client(base_url: str, service_token: str) -> ShenshiClient:
"""按配置装配客户端:未配置神识地址时降级为回声桩(M0 骨架形态)。"""
if base_url:
return HttpShenshiClient(base_url, service_token)
return EchoShenshi()