mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
126 lines
3.8 KiB
Python
126 lines
3.8 KiB
Python
"""
|
||
Usage 事件定义与序列化工具(用于 Redis Streams)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import time
|
||
from dataclasses import dataclass
|
||
from enum import Enum
|
||
from typing import Any
|
||
|
||
import msgpack
|
||
from msgpack.exceptions import OutOfData
|
||
|
||
USAGE_EVENT_VERSION = 1
|
||
|
||
|
||
class UsageEventType(str, Enum):
|
||
STREAMING = "streaming"
|
||
COMPLETED = "completed"
|
||
FAILED = "failed"
|
||
CANCELLED = "cancelled"
|
||
|
||
|
||
def now_ms() -> int:
|
||
return int(time.time() * 1000)
|
||
|
||
|
||
def _sanitize_value(value: Any) -> Any:
|
||
if value is None or isinstance(value, (str, int, float, bool)):
|
||
return value
|
||
if isinstance(value, dict):
|
||
return {str(k): _sanitize_value(v) for k, v in value.items()}
|
||
if isinstance(value, list):
|
||
return [_sanitize_value(item) for item in value]
|
||
return str(value)
|
||
|
||
|
||
def sanitize_payload(data: dict[str, Any]) -> dict[str, Any]:
|
||
return {str(k): _sanitize_value(v) for k, v in data.items()}
|
||
|
||
|
||
def _decode_payload(raw: Any) -> dict[str, Any]:
|
||
"""兼容解码:优先 msgpack,回退旧 JSON。
|
||
|
||
统一将输入归一化为 bytes 后走单一解码路径:msgpack → JSON fallback。
|
||
str 输入来自 decode_responses=True + surrogateescape 的 Redis 客户端,
|
||
通过 surrogateescape 可无损还原回原始 bytes。
|
||
"""
|
||
if isinstance(raw, str):
|
||
raw = raw.encode("utf-8", errors="surrogateescape")
|
||
|
||
if not isinstance(raw, (bytes, bytearray, memoryview)):
|
||
raise ValueError("Invalid payload field in usage event")
|
||
|
||
payload_bytes = bytes(raw)
|
||
|
||
# 新格式:msgpack
|
||
try:
|
||
payload = msgpack.unpackb(payload_bytes, raw=False)
|
||
except (ValueError, OutOfData, TypeError):
|
||
# 兼容旧格式:JSON bytes(含 surrogateescape 还原后的纯 UTF-8 JSON)
|
||
try:
|
||
payload = json.loads(payload_bytes.decode("utf-8"))
|
||
except (UnicodeDecodeError, json.JSONDecodeError, TypeError) as exc:
|
||
raise ValueError("Invalid payload field in usage event") from exc
|
||
|
||
if not isinstance(payload, dict):
|
||
raise ValueError("Invalid payload field in usage event")
|
||
return payload
|
||
|
||
|
||
@dataclass
|
||
class UsageEvent:
|
||
event_type: UsageEventType
|
||
request_id: str
|
||
timestamp_ms: int
|
||
data: dict[str, Any]
|
||
|
||
def to_stream_fields(self) -> dict[str, bytes]:
|
||
"""序列化为 Redis Stream 字段。
|
||
|
||
该函数返回 bytes payload,要求读写 usage queue 的 Redis 客户端使用
|
||
decode_responses=True,并以 surrogateescape 做 UTF-8 编解码,
|
||
以保证 bytes <-> str 往返无损。
|
||
"""
|
||
payload = {
|
||
"v": USAGE_EVENT_VERSION,
|
||
"type": self.event_type.value,
|
||
"request_id": self.request_id,
|
||
"timestamp_ms": self.timestamp_ms,
|
||
# 兜底清洗,避免 metadata 中混入非 JSON 类型导致队列写入失败。
|
||
"data": sanitize_payload(self.data),
|
||
}
|
||
return {"payload": msgpack.packb(payload, use_bin_type=True)}
|
||
|
||
@classmethod
|
||
def from_stream_fields(cls, fields: dict[str, Any]) -> UsageEvent:
|
||
raw = fields.get("payload")
|
||
if not raw:
|
||
raise ValueError("Missing payload field in usage event")
|
||
payload = _decode_payload(raw)
|
||
event_type = UsageEventType(payload["type"])
|
||
return cls(
|
||
event_type=event_type,
|
||
request_id=payload["request_id"],
|
||
timestamp_ms=int(payload.get("timestamp_ms", 0)),
|
||
data=payload.get("data", {}) or {},
|
||
)
|
||
|
||
|
||
def build_usage_event(
|
||
*,
|
||
event_type: UsageEventType,
|
||
request_id: str,
|
||
data: dict[str, Any],
|
||
timestamp_ms: int | None = None,
|
||
) -> UsageEvent:
|
||
return UsageEvent(
|
||
event_type=event_type,
|
||
request_id=request_id,
|
||
timestamp_ms=timestamp_ms or now_ms(),
|
||
data=data,
|
||
)
|