本次提交新增了渠道消息处理的完整核心模块,包含以下核心功能: 1. 新增会话围栏类,实现会话并发控制与过期清理 2. 新增媒体清理器,实现过期媒体文件自动清理 3. 新增熔断器组件,实现服务降级与故障隔离 4. 新增消息处理器,完成渠道消息的完整流转处理 5. 新增限流器组件,实现渠道级和账户级流量控制 6. 新增链路追踪模块,集成Langfuse实现调用链路监控 7. 新增指标统计模块,实现消息处理全链路指标采集 8. 新增统一消息模型,封装全渠道消息格式 9. 新增块回复流水线,实现流式回复的合并与去重 10. 新增本地媒体存储模块,实现媒体文件的本地管理 11. 新增回复分发器,实现回复内容的有序发送与延迟处理 12. 完善__init__.py导出所有核心模块与工具类
203 lines
6.9 KiB
Python
203 lines
6.9 KiB
Python
import logging
|
||
import time
|
||
from typing import Any
|
||
|
||
from yuxi.channel.message.models import DispatchResult, UnifiedMessage
|
||
from yuxi.services.langfuse_service import (
|
||
get_langfuse_client,
|
||
is_langfuse_enabled,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class ChannelTrace:
|
||
"""渠道消息处理链路的 Langfuse trace 包装器。
|
||
|
||
复用现有 langfuse_service 的 Langfuse 客户端,创建实际 Langfuse
|
||
span 以追踪消息处理每个阶段(分发、校验、规则匹配等)的耗时和状态。
|
||
"""
|
||
|
||
def __init__(self, msg: UnifiedMessage, dispatch_result: DispatchResult | None = None):
|
||
self._msg = msg
|
||
self._dispatch_result = dispatch_result
|
||
self._client = get_langfuse_client()
|
||
self._enabled = is_langfuse_enabled() and self._client is not None
|
||
self._trace_id: str | None = None
|
||
self._trace_obj: Any = None
|
||
self._trace_start_time: float | None = None
|
||
self._live_spans: dict[str, Any] = {}
|
||
|
||
@property
|
||
def trace_name(self) -> str:
|
||
peer_kind = self._msg.sender.kind.value
|
||
return f"channel:{self._msg.channel_type}:{peer_kind}"
|
||
|
||
@property
|
||
def trace_id(self) -> str | None:
|
||
return self._trace_id
|
||
|
||
@property
|
||
def span_count(self) -> int:
|
||
return len(self._live_spans)
|
||
|
||
async def start(self) -> "ChannelTrace":
|
||
if not self._enabled:
|
||
return self
|
||
|
||
try:
|
||
self._trace_start_time = time.monotonic()
|
||
self._trace_id = self._client.create_trace_id()
|
||
self._trace_obj = self._client.trace(
|
||
id=self._trace_id,
|
||
name=self.trace_name,
|
||
metadata=self._build_base_metadata(),
|
||
tags=self._build_base_tags(),
|
||
)
|
||
except Exception:
|
||
logger.exception("Failed to create channel trace: %s", self.trace_name)
|
||
self._enabled = False
|
||
|
||
return self
|
||
|
||
async def add_span(
|
||
self,
|
||
name: str,
|
||
metadata: dict[str, Any] | None = None,
|
||
input_data: Any = None,
|
||
output_data: Any = None,
|
||
level: str = "DEFAULT",
|
||
status_message: str | None = None,
|
||
parent_span_id: str | None = None,
|
||
) -> str | None:
|
||
"""创建 Langfuse span 并追踪其生命周期。
|
||
|
||
返回 span_id 供后续 end_span() 结束使用。
|
||
调用者必须在完成处理后调用 end_span() 或依赖 finish() 统一结束。
|
||
"""
|
||
if not self._enabled or self._trace_obj is None:
|
||
return None
|
||
|
||
try:
|
||
parent = self._live_spans.get(parent_span_id) if parent_span_id else self._trace_obj
|
||
span = parent.span(
|
||
name=name,
|
||
input=input_data,
|
||
output=output_data,
|
||
metadata=metadata or {},
|
||
level=level,
|
||
status_message=status_message,
|
||
)
|
||
span_id: str = span.id
|
||
self._live_spans[span_id] = span
|
||
return span_id
|
||
except Exception:
|
||
logger.exception("Failed to create span '%s' for trace: %s", name, self.trace_name)
|
||
return None
|
||
|
||
async def end_span(
|
||
self,
|
||
span_id: str,
|
||
output_data: Any = None,
|
||
status_message: str | None = None,
|
||
level: str | None = None,
|
||
metadata: dict[str, Any] | None = None,
|
||
) -> None:
|
||
"""结束指定 span,记录输出与最终状态。"""
|
||
span = self._live_spans.pop(span_id, None)
|
||
if span is None:
|
||
return
|
||
|
||
try:
|
||
update_kwargs: dict[str, Any] = {}
|
||
if output_data is not None:
|
||
update_kwargs["output"] = output_data
|
||
if level is not None:
|
||
update_kwargs["level"] = level
|
||
if status_message is not None:
|
||
update_kwargs["status_message"] = status_message
|
||
if metadata is not None:
|
||
update_kwargs["metadata"] = metadata
|
||
if update_kwargs:
|
||
span.update(**update_kwargs)
|
||
span.end()
|
||
except Exception:
|
||
logger.exception("Failed to end span '%s' for trace: %s", span_id, self.trace_name)
|
||
|
||
async def finish(self, error: str | None = None) -> None:
|
||
if not self._enabled or self._trace_obj is None:
|
||
return
|
||
|
||
try:
|
||
pending = dict(self._live_spans)
|
||
self._live_spans.clear()
|
||
for span_id, span in pending.items():
|
||
try:
|
||
span.end()
|
||
except Exception:
|
||
logger.exception("Failed to end pending span '%s'", span_id)
|
||
|
||
duration_ms = None
|
||
if self._trace_start_time is not None:
|
||
duration_ms = (time.monotonic() - self._trace_start_time) * 1000
|
||
|
||
metadata: dict[str, Any] = {
|
||
**self._build_base_metadata(),
|
||
"span_count": len(pending),
|
||
"dispatch_success": error is None,
|
||
**(self._build_dispatch_metadata() if self._dispatch_result else {}),
|
||
}
|
||
if duration_ms is not None:
|
||
metadata["duration_ms"] = round(duration_ms, 2)
|
||
|
||
self._trace_obj.update(
|
||
output=error or "success",
|
||
metadata=metadata,
|
||
)
|
||
except Exception:
|
||
logger.exception("Failed to finish channel trace: %s", self.trace_name)
|
||
|
||
def _build_base_metadata(self) -> dict[str, Any]:
|
||
return {
|
||
"source": "channel",
|
||
"channel_type": self._msg.channel_type,
|
||
"account_id": self._msg.account_id,
|
||
"peer_kind": self._msg.sender.kind.value,
|
||
"peer_id": self._msg.sender.id,
|
||
"msg_id": self._msg.msg_id,
|
||
"message_type": self._msg.message_type.value,
|
||
"feature": "channel_dispatch",
|
||
}
|
||
|
||
def _build_dispatch_metadata(self) -> dict[str, Any]:
|
||
if self._dispatch_result is None:
|
||
return {}
|
||
return {
|
||
"agent_config_id": str(self._dispatch_result.agent_config_id)
|
||
if self._dispatch_result.agent_config_id
|
||
else None,
|
||
"session_key": getattr(self._dispatch_result, "session_key", None),
|
||
"thread_id": self._dispatch_result.thread_id,
|
||
"matched_by": getattr(self._dispatch_result, "matched_by", None),
|
||
}
|
||
|
||
def _build_base_tags(self) -> list[str]:
|
||
tags = [
|
||
"yuxi",
|
||
"channel",
|
||
f"channel:{self._msg.channel_type}",
|
||
f"peer_kind:{self._msg.sender.kind.value}",
|
||
]
|
||
if self._dispatch_result and self._dispatch_result.agent_config_id:
|
||
tags.append(f"agent_config:{self._dispatch_result.agent_config_id}")
|
||
return tags
|
||
|
||
|
||
async def create_channel_trace(
|
||
msg: UnifiedMessage,
|
||
dispatch_result: DispatchResult | None = None,
|
||
) -> ChannelTrace:
|
||
trace = ChannelTrace(msg, dispatch_result)
|
||
await trace.start()
|
||
return trace
|