sycommon-python-lib 0.1.46__py3-none-any.whl → 0.1.57b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sycommon/config/Config.py +29 -4
- sycommon/config/LangfuseConfig.py +15 -0
- sycommon/config/RerankerConfig.py +1 -0
- sycommon/config/SentryConfig.py +13 -0
- sycommon/database/async_base_db_service.py +36 -0
- sycommon/database/async_database_service.py +96 -0
- sycommon/llm/__init__.py +0 -0
- sycommon/llm/embedding.py +204 -0
- sycommon/llm/get_llm.py +37 -0
- sycommon/llm/llm_logger.py +126 -0
- sycommon/llm/llm_tokens.py +119 -0
- sycommon/llm/struct_token.py +192 -0
- sycommon/llm/sy_langfuse.py +103 -0
- sycommon/llm/usage_token.py +117 -0
- sycommon/logging/async_sql_logger.py +65 -0
- sycommon/logging/kafka_log.py +200 -434
- sycommon/logging/logger_levels.py +23 -0
- sycommon/middleware/context.py +2 -0
- sycommon/middleware/exception.py +10 -16
- sycommon/middleware/timeout.py +2 -1
- sycommon/middleware/traceid.py +179 -51
- sycommon/notice/__init__.py +0 -0
- sycommon/notice/uvicorn_monitor.py +200 -0
- sycommon/rabbitmq/rabbitmq_client.py +267 -290
- sycommon/rabbitmq/rabbitmq_pool.py +277 -465
- sycommon/rabbitmq/rabbitmq_service.py +23 -891
- sycommon/rabbitmq/rabbitmq_service_client_manager.py +211 -0
- sycommon/rabbitmq/rabbitmq_service_connection_monitor.py +73 -0
- sycommon/rabbitmq/rabbitmq_service_consumer_manager.py +285 -0
- sycommon/rabbitmq/rabbitmq_service_core.py +117 -0
- sycommon/rabbitmq/rabbitmq_service_producer_manager.py +238 -0
- sycommon/sentry/__init__.py +0 -0
- sycommon/sentry/sy_sentry.py +35 -0
- sycommon/services.py +144 -115
- sycommon/synacos/feign.py +18 -7
- sycommon/synacos/feign_client.py +26 -8
- sycommon/synacos/nacos_client_base.py +119 -0
- sycommon/synacos/nacos_config_manager.py +107 -0
- sycommon/synacos/nacos_heartbeat_manager.py +144 -0
- sycommon/synacos/nacos_service.py +65 -769
- sycommon/synacos/nacos_service_discovery.py +157 -0
- sycommon/synacos/nacos_service_registration.py +270 -0
- sycommon/tools/env.py +62 -0
- sycommon/tools/merge_headers.py +117 -0
- sycommon/tools/snowflake.py +238 -23
- {sycommon_python_lib-0.1.46.dist-info → sycommon_python_lib-0.1.57b1.dist-info}/METADATA +18 -11
- sycommon_python_lib-0.1.57b1.dist-info/RECORD +89 -0
- sycommon_python_lib-0.1.46.dist-info/RECORD +0 -59
- {sycommon_python_lib-0.1.46.dist-info → sycommon_python_lib-0.1.57b1.dist-info}/WHEEL +0 -0
- {sycommon_python_lib-0.1.46.dist-info → sycommon_python_lib-0.1.57b1.dist-info}/entry_points.txt +0 -0
- {sycommon_python_lib-0.1.46.dist-info → sycommon_python_lib-0.1.57b1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from langchain_core.callbacks import AsyncCallbackHandler
|
|
3
|
+
from langchain_core.outputs.llm_result import LLMResult
|
|
4
|
+
from sycommon.logging.kafka_log import SYLogger
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TokensCallbackHandler(AsyncCallbackHandler):
|
|
8
|
+
"""
|
|
9
|
+
继承AsyncCallbackHandler的Token统计处理器
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
super().__init__()
|
|
14
|
+
self.input_tokens = 0
|
|
15
|
+
self.output_tokens = 0
|
|
16
|
+
self.total_tokens = 0
|
|
17
|
+
self.usage_metadata = {}
|
|
18
|
+
self.reset()
|
|
19
|
+
|
|
20
|
+
def reset(self):
|
|
21
|
+
"""重置Token统计数据"""
|
|
22
|
+
self.input_tokens = 0
|
|
23
|
+
self.output_tokens = 0
|
|
24
|
+
self.total_tokens = 0
|
|
25
|
+
self.usage_metadata = {
|
|
26
|
+
"input_tokens": 0,
|
|
27
|
+
"output_tokens": 0,
|
|
28
|
+
"total_tokens": 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# ========== 同步回调方法(兼容签名) ==========
|
|
32
|
+
def on_llm_end(
|
|
33
|
+
self,
|
|
34
|
+
response: LLMResult,
|
|
35
|
+
**kwargs: Any,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""同步LLM调用结束时的回调"""
|
|
38
|
+
self._parse_token_usage(response)
|
|
39
|
+
|
|
40
|
+
# ========== 异步回调方法(兼容签名) ==========
|
|
41
|
+
async def on_llm_end(
|
|
42
|
+
self,
|
|
43
|
+
response: LLMResult,
|
|
44
|
+
**kwargs: Any,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""异步LLM调用结束时的回调"""
|
|
47
|
+
self._parse_token_usage(response)
|
|
48
|
+
|
|
49
|
+
def _parse_token_usage(self, response: LLMResult) -> None:
|
|
50
|
+
"""
|
|
51
|
+
通用Token解析逻辑,不依赖特定类结构
|
|
52
|
+
兼容各种LLM响应格式
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
# 情况1: 标准LangChain响应(有llm_output属性)
|
|
56
|
+
if response.llm_output:
|
|
57
|
+
llm_output = response.llm_output
|
|
58
|
+
self._parse_from_llm_output(llm_output)
|
|
59
|
+
|
|
60
|
+
# 情况2: 包含generations的响应
|
|
61
|
+
elif response.generations:
|
|
62
|
+
self._parse_from_generations(response.generations)
|
|
63
|
+
|
|
64
|
+
# 计算总Token
|
|
65
|
+
if self.total_tokens <= 0:
|
|
66
|
+
self.total_tokens = self.input_tokens + self.output_tokens
|
|
67
|
+
|
|
68
|
+
# 更新metadata
|
|
69
|
+
self.usage_metadata = {
|
|
70
|
+
"input_tokens": self.input_tokens,
|
|
71
|
+
"output_tokens": self.output_tokens,
|
|
72
|
+
"total_tokens": self.total_tokens
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
SYLogger.debug(
|
|
76
|
+
f"Token统计成功 - 输入: {self.input_tokens}, 输出: {self.output_tokens}")
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
SYLogger.warning(f"Token解析失败: {str(e)}", exc_info=True)
|
|
80
|
+
self.reset()
|
|
81
|
+
|
|
82
|
+
def _parse_from_llm_output(self, llm_output: dict) -> None:
|
|
83
|
+
"""从llm_output字典解析Token信息"""
|
|
84
|
+
if not isinstance(llm_output, dict):
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
# OpenAI标准格式
|
|
88
|
+
if 'token_usage' in llm_output:
|
|
89
|
+
token_usage = llm_output['token_usage']
|
|
90
|
+
self.input_tokens = token_usage.get(
|
|
91
|
+
'prompt_tokens', token_usage.get('input_tokens', 0))
|
|
92
|
+
self.output_tokens = token_usage.get(
|
|
93
|
+
'completion_tokens', token_usage.get('output_tokens', 0))
|
|
94
|
+
self.total_tokens = token_usage.get('total_tokens', 0)
|
|
95
|
+
|
|
96
|
+
# 直接包含Token信息
|
|
97
|
+
else:
|
|
98
|
+
self.input_tokens = llm_output.get(
|
|
99
|
+
'input_tokens', llm_output.get('prompt_tokens', 0))
|
|
100
|
+
self.output_tokens = llm_output.get(
|
|
101
|
+
'output_tokens', llm_output.get('completion_tokens', 0))
|
|
102
|
+
self.total_tokens = token_usage.get('total_tokens', 0)
|
|
103
|
+
|
|
104
|
+
def _parse_from_generations(self, generations: list) -> None:
|
|
105
|
+
"""从generations列表解析Token信息"""
|
|
106
|
+
if not isinstance(generations, list) or len(generations) == 0:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
# 遍历generation信息
|
|
110
|
+
for gen_group in generations:
|
|
111
|
+
for generation in gen_group:
|
|
112
|
+
if hasattr(generation, 'generation_info') and generation.generation_info:
|
|
113
|
+
gen_info = generation.generation_info
|
|
114
|
+
self.input_tokens = gen_info.get(
|
|
115
|
+
'input_tokens', gen_info.get('prompt_tokens', 0))
|
|
116
|
+
self.output_tokens = gen_info.get(
|
|
117
|
+
'output_tokens', gen_info.get('completion_tokens', 0))
|
|
118
|
+
self.total_tokens = gen_info.get('total_tokens', 0)
|
|
119
|
+
return
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from typing import Dict, List, Optional, Any
|
|
2
|
+
from langfuse import Langfuse, LangfuseSpan, propagate_attributes
|
|
3
|
+
from sycommon.llm.llm_logger import LLMLogger
|
|
4
|
+
from langchain_core.runnables import Runnable, RunnableConfig
|
|
5
|
+
from langchain_core.messages import BaseMessage, HumanMessage
|
|
6
|
+
from sycommon.llm.llm_tokens import TokensCallbackHandler
|
|
7
|
+
from sycommon.logging.kafka_log import SYLogger
|
|
8
|
+
from sycommon.tools.env import get_env_var
|
|
9
|
+
from sycommon.tools.merge_headers import get_header_value
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StructuredRunnableWithToken(Runnable):
|
|
13
|
+
"""带Token统计的Runnable类"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, retry_chain: Runnable, langfuse: Optional[Langfuse]):
|
|
16
|
+
super().__init__()
|
|
17
|
+
self.retry_chain = retry_chain
|
|
18
|
+
self.langfuse = langfuse
|
|
19
|
+
|
|
20
|
+
def _adapt_input(self, input: Any) -> List[BaseMessage]:
|
|
21
|
+
"""适配输入格式"""
|
|
22
|
+
if isinstance(input, list) and all(isinstance(x, BaseMessage) for x in input):
|
|
23
|
+
return input
|
|
24
|
+
elif isinstance(input, BaseMessage):
|
|
25
|
+
return [input]
|
|
26
|
+
elif isinstance(input, str):
|
|
27
|
+
return [HumanMessage(content=input)]
|
|
28
|
+
elif isinstance(input, dict) and "input" in input:
|
|
29
|
+
return [HumanMessage(content=str(input["input"]))]
|
|
30
|
+
else:
|
|
31
|
+
raise ValueError(f"不支持的输入格式:{type(input)}")
|
|
32
|
+
|
|
33
|
+
def _get_callback_config(
|
|
34
|
+
self,
|
|
35
|
+
config: Optional[RunnableConfig] = None,
|
|
36
|
+
trace_id: Optional[str] = None,
|
|
37
|
+
user_id: Optional[str] = None
|
|
38
|
+
) -> tuple[RunnableConfig, TokensCallbackHandler]:
|
|
39
|
+
"""构建包含Token统计和metadata的回调配置"""
|
|
40
|
+
token_handler = TokensCallbackHandler()
|
|
41
|
+
|
|
42
|
+
if config is None:
|
|
43
|
+
processed_config = {"callbacks": [], "metadata": {}}
|
|
44
|
+
else:
|
|
45
|
+
processed_config = config.copy()
|
|
46
|
+
if "callbacks" not in processed_config:
|
|
47
|
+
processed_config["callbacks"] = []
|
|
48
|
+
if "metadata" not in processed_config:
|
|
49
|
+
processed_config["metadata"] = {}
|
|
50
|
+
|
|
51
|
+
# 添加 Langfuse metadata
|
|
52
|
+
if trace_id:
|
|
53
|
+
processed_config["metadata"]["langfuse_session_id"] = trace_id
|
|
54
|
+
if user_id:
|
|
55
|
+
processed_config["metadata"]["langfuse_user_id"] = user_id
|
|
56
|
+
|
|
57
|
+
callbacks = processed_config["callbacks"]
|
|
58
|
+
if not any(isinstance(cb, LLMLogger) for cb in callbacks):
|
|
59
|
+
callbacks.append(LLMLogger())
|
|
60
|
+
callbacks.append(token_handler)
|
|
61
|
+
|
|
62
|
+
callback_types = {}
|
|
63
|
+
unique_callbacks = []
|
|
64
|
+
for cb in callbacks:
|
|
65
|
+
cb_type = type(cb)
|
|
66
|
+
if cb_type not in callback_types:
|
|
67
|
+
callback_types[cb_type] = cb
|
|
68
|
+
unique_callbacks.append(cb)
|
|
69
|
+
|
|
70
|
+
processed_config["callbacks"] = unique_callbacks
|
|
71
|
+
|
|
72
|
+
return processed_config, token_handler
|
|
73
|
+
|
|
74
|
+
def invoke(self, input: Any, config: Optional[RunnableConfig] = None, **kwargs) -> Dict[str, Any]:
|
|
75
|
+
# 获取 trace_id 和 user_id
|
|
76
|
+
trace_id = SYLogger.get_trace_id()
|
|
77
|
+
userid = get_header_value(SYLogger.get_headers(), "x-userid-header")
|
|
78
|
+
syVersion = get_header_value(SYLogger.get_headers(), "s-y-version")
|
|
79
|
+
user_id = userid or syVersion or get_env_var('VERSION')
|
|
80
|
+
|
|
81
|
+
# 判断是否启用 Langfuse
|
|
82
|
+
if self.langfuse:
|
|
83
|
+
try:
|
|
84
|
+
with self.langfuse.start_as_current_observation(as_type="span", name="invoke") as span:
|
|
85
|
+
with propagate_attributes(session_id=trace_id, user_id=user_id):
|
|
86
|
+
span.update_trace(user_id=user_id, session_id=trace_id)
|
|
87
|
+
return self._execute_chain(input, config, trace_id, user_id, span)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
# Langfuse 跟踪失败不应阻断业务,降级执行
|
|
90
|
+
SYLogger.error(f"Langfuse 同步跟踪失败: {str(e)}", exc_info=True)
|
|
91
|
+
return self._execute_chain(input, config, trace_id, user_id, None)
|
|
92
|
+
else:
|
|
93
|
+
# 未启用 Langfuse,直接执行业务逻辑
|
|
94
|
+
return self._execute_chain(input, config, trace_id, user_id, None)
|
|
95
|
+
|
|
96
|
+
async def ainvoke(self, input: Any, config: Optional[RunnableConfig] = None, **kwargs) -> Dict[str, Any]:
|
|
97
|
+
# 获取 trace_id 和 user_id
|
|
98
|
+
trace_id = SYLogger.get_trace_id()
|
|
99
|
+
userid = get_header_value(SYLogger.get_headers(), "x-userid-header")
|
|
100
|
+
syVersion = get_header_value(SYLogger.get_headers(), "s-y-version")
|
|
101
|
+
user_id = userid or syVersion or get_env_var('VERSION')
|
|
102
|
+
|
|
103
|
+
# 判断是否启用 Langfuse
|
|
104
|
+
if self.langfuse:
|
|
105
|
+
try:
|
|
106
|
+
with self.langfuse.start_as_current_observation(as_type="span", name="ainvoke") as span:
|
|
107
|
+
with propagate_attributes(session_id=trace_id, user_id=user_id):
|
|
108
|
+
span.update_trace(user_id=user_id, session_id=trace_id)
|
|
109
|
+
return await self._aexecute_chain(input, config, trace_id, user_id, span)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
# Langfuse 跟踪失败不应阻断业务,降级执行
|
|
112
|
+
SYLogger.error(f"Langfuse 异步跟踪失败: {str(e)}", exc_info=True)
|
|
113
|
+
return await self._aexecute_chain(input, config, trace_id, user_id, None)
|
|
114
|
+
else:
|
|
115
|
+
# 未启用 Langfuse,直接执行业务逻辑
|
|
116
|
+
return await self._aexecute_chain(input, config, trace_id, user_id, None)
|
|
117
|
+
|
|
118
|
+
def _execute_chain(
|
|
119
|
+
self,
|
|
120
|
+
input: Any,
|
|
121
|
+
config: Optional[RunnableConfig],
|
|
122
|
+
trace_id: str,
|
|
123
|
+
user_id: str,
|
|
124
|
+
span: LangfuseSpan
|
|
125
|
+
) -> Dict[str, Any]:
|
|
126
|
+
"""执行实际的调用逻辑 (同步)"""
|
|
127
|
+
try:
|
|
128
|
+
processed_config, token_handler = self._get_callback_config(
|
|
129
|
+
config,
|
|
130
|
+
trace_id=trace_id,
|
|
131
|
+
user_id=user_id
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
adapted_input = self._adapt_input(input)
|
|
135
|
+
input_data = {"messages": adapted_input}
|
|
136
|
+
|
|
137
|
+
if span:
|
|
138
|
+
span.update_trace(input=input_data)
|
|
139
|
+
|
|
140
|
+
structured_result = self.retry_chain.invoke(
|
|
141
|
+
input_data,
|
|
142
|
+
config=processed_config
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if span:
|
|
146
|
+
span.update_trace(output=structured_result)
|
|
147
|
+
|
|
148
|
+
token_usage = token_handler.usage_metadata
|
|
149
|
+
structured_result._token_usage_ = token_usage
|
|
150
|
+
|
|
151
|
+
return structured_result
|
|
152
|
+
except Exception as e:
|
|
153
|
+
SYLogger.error(f"同步LLM调用失败: {str(e)}", exc_info=True)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
async def _aexecute_chain(
|
|
157
|
+
self,
|
|
158
|
+
input: Any,
|
|
159
|
+
config: Optional[RunnableConfig],
|
|
160
|
+
trace_id: str,
|
|
161
|
+
user_id: str,
|
|
162
|
+
span: LangfuseSpan
|
|
163
|
+
) -> Dict[str, Any]:
|
|
164
|
+
"""执行实际的调用逻辑 (异步)"""
|
|
165
|
+
try:
|
|
166
|
+
processed_config, token_handler = self._get_callback_config(
|
|
167
|
+
config,
|
|
168
|
+
trace_id=trace_id,
|
|
169
|
+
user_id=user_id
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
adapted_input = self._adapt_input(input)
|
|
173
|
+
input_data = {"messages": adapted_input}
|
|
174
|
+
|
|
175
|
+
if span:
|
|
176
|
+
span.update_trace(input=input_data)
|
|
177
|
+
|
|
178
|
+
structured_result = await self.retry_chain.ainvoke(
|
|
179
|
+
input_data,
|
|
180
|
+
config=processed_config
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if span:
|
|
184
|
+
span.update_trace(output=structured_result)
|
|
185
|
+
|
|
186
|
+
token_usage = token_handler.usage_metadata
|
|
187
|
+
structured_result._token_usage_ = token_usage
|
|
188
|
+
|
|
189
|
+
return structured_result
|
|
190
|
+
except Exception as e:
|
|
191
|
+
SYLogger.error(f"异步LLM调用失败: {str(e)}", exc_info=True)
|
|
192
|
+
return None
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Tuple, List, Optional, Any, Dict
|
|
3
|
+
from langfuse import Langfuse, get_client
|
|
4
|
+
from sycommon.config.Config import Config, SingletonMeta
|
|
5
|
+
from sycommon.logging.kafka_log import SYLogger
|
|
6
|
+
from langfuse.langchain import CallbackHandler
|
|
7
|
+
from sycommon.tools.env import get_env_var
|
|
8
|
+
from sycommon.tools.merge_headers import get_header_value
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LangfuseInitializer(metaclass=SingletonMeta):
|
|
12
|
+
"""
|
|
13
|
+
Langfuse 初始化管理器
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self._langfuse_client: Optional[Langfuse] = None
|
|
18
|
+
self._base_callbacks: List[Any] = []
|
|
19
|
+
|
|
20
|
+
# 执行初始化
|
|
21
|
+
self._initialize()
|
|
22
|
+
|
|
23
|
+
def _initialize(self):
|
|
24
|
+
"""执行实际的配置读取和组件创建"""
|
|
25
|
+
try:
|
|
26
|
+
config_dict = Config().config
|
|
27
|
+
|
|
28
|
+
server_name = config_dict.get('Name', '')
|
|
29
|
+
langfuse_configs = config_dict.get('LangfuseConfig', [])
|
|
30
|
+
environment = config_dict.get('Nacos', {}).get('namespaceId', '')
|
|
31
|
+
|
|
32
|
+
# 3. 查找匹配的配置项
|
|
33
|
+
target_config = next(
|
|
34
|
+
(item for item in langfuse_configs if item.get(
|
|
35
|
+
'name') == server_name), None
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# 4. 如果启用且配置存在,初始化 Langfuse
|
|
39
|
+
if target_config and target_config.get('enable', False):
|
|
40
|
+
# 设置环境变量
|
|
41
|
+
os.environ["LANGFUSE_SECRET_KEY"] = target_config.get(
|
|
42
|
+
'secretKey', '')
|
|
43
|
+
os.environ["LANGFUSE_PUBLIC_KEY"] = target_config.get(
|
|
44
|
+
'publicKey', '')
|
|
45
|
+
os.environ["LANGFUSE_BASE_URL"] = target_config.get(
|
|
46
|
+
'baseUrl', '')
|
|
47
|
+
os.environ["LANGFUSE_TRACING_ENVIRONMENT"] = environment
|
|
48
|
+
os.environ["OTEL_SERVICE_NAME"] = server_name
|
|
49
|
+
|
|
50
|
+
self._langfuse_client = get_client()
|
|
51
|
+
|
|
52
|
+
langfuse_handler = CallbackHandler()
|
|
53
|
+
self._base_callbacks.append(langfuse_handler)
|
|
54
|
+
|
|
55
|
+
SYLogger.info(f"Langfuse 初始化成功 [Service: {server_name}]")
|
|
56
|
+
else:
|
|
57
|
+
SYLogger.info(f"Langfuse 未启用或未找到匹配配置 [Service: {server_name}]")
|
|
58
|
+
|
|
59
|
+
except Exception as e:
|
|
60
|
+
SYLogger.error(f"Langfuse 初始化异常: {str(e)}", exc_info=True)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def callbacks(self) -> List[Any]:
|
|
64
|
+
"""获取回调列表"""
|
|
65
|
+
return self._base_callbacks
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def metadata(self) -> Dict[str, Any]:
|
|
69
|
+
"""动态生成包含 langfuse_session_id 和 langfuse_user_id 的 metadata"""
|
|
70
|
+
trace_id = SYLogger.get_trace_id()
|
|
71
|
+
userid = get_header_value(
|
|
72
|
+
SYLogger.get_headers(), "x-userid-header")
|
|
73
|
+
syVersion = get_header_value(
|
|
74
|
+
SYLogger.get_headers(), "s-y-version")
|
|
75
|
+
user_id = userid or syVersion or get_env_var('VERSION')
|
|
76
|
+
metadata_config = {
|
|
77
|
+
"langfuse_session_id": trace_id,
|
|
78
|
+
"langfuse_user_id": user_id,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return metadata_config
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def client(self) -> Optional[Langfuse]:
|
|
85
|
+
"""获取 Langfuse 原生客户端实例"""
|
|
86
|
+
return self._langfuse_client
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def config(self) -> Dict[str, Any]:
|
|
90
|
+
return {
|
|
91
|
+
"callbacks": self.callbacks,
|
|
92
|
+
"metadata": self.metadata,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def get_components(self) -> Tuple[List[Any], Optional[Langfuse]]:
|
|
96
|
+
"""获取 Langfuse 组件"""
|
|
97
|
+
return list(self._base_callbacks), self._langfuse_client
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def get() -> Tuple[List[Any], Optional[Langfuse]]:
|
|
101
|
+
"""一句话获取组件"""
|
|
102
|
+
initializer = LangfuseInitializer()
|
|
103
|
+
return initializer.get_components()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from typing import Type, List, Optional, Callable
|
|
2
|
+
from langfuse import Langfuse
|
|
3
|
+
from langchain_core.language_models import BaseChatModel
|
|
4
|
+
from langchain_core.runnables import Runnable, RunnableLambda
|
|
5
|
+
from langchain_core.output_parsers import PydanticOutputParser
|
|
6
|
+
from langchain_core.messages import BaseMessage, HumanMessage
|
|
7
|
+
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
|
8
|
+
from pydantic import BaseModel, ValidationError, Field
|
|
9
|
+
from sycommon.llm.struct_token import StructuredRunnableWithToken
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LLMWithAutoTokenUsage(BaseChatModel):
|
|
13
|
+
"""自动为结构化调用返回token_usage的LLM包装类"""
|
|
14
|
+
llm: BaseChatModel = Field(default=None)
|
|
15
|
+
langfuse: Optional[Langfuse] = Field(default=None, exclude=True)
|
|
16
|
+
|
|
17
|
+
def __init__(self, llm: BaseChatModel, langfuse: Langfuse, **kwargs):
|
|
18
|
+
super().__init__(llm=llm, langfuse=langfuse, **kwargs)
|
|
19
|
+
|
|
20
|
+
def with_structured_output(
|
|
21
|
+
self,
|
|
22
|
+
output_model: Type[BaseModel],
|
|
23
|
+
max_retries: int = 3,
|
|
24
|
+
is_extract: bool = False,
|
|
25
|
+
override_prompt: ChatPromptTemplate = None,
|
|
26
|
+
custom_processors: Optional[List[Callable[[str], str]]] = None,
|
|
27
|
+
custom_parser: Optional[Callable[[str], BaseModel]] = None
|
|
28
|
+
) -> Runnable:
|
|
29
|
+
"""返回支持自动统计Token的结构化Runnable"""
|
|
30
|
+
parser = PydanticOutputParser(pydantic_object=output_model)
|
|
31
|
+
|
|
32
|
+
# 提示词模板
|
|
33
|
+
accuracy_instructions = """
|
|
34
|
+
字段值的抽取准确率(0~1之间),评分规则:
|
|
35
|
+
1.0(完全准确):直接从原文提取,无需任何加工,且格式与原文完全一致
|
|
36
|
+
0.9(轻微处理):数据来源明确,但需进行格式标准化或冗余信息剔除(不改变原始数值)
|
|
37
|
+
0.8(有限推断):数据需通过上下文关联或简单计算得出,仍有明确依据
|
|
38
|
+
0.8以下(不可靠):数据需大量推测、存在歧义或来源不明,处理方式:直接忽略该数据,设置为None
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
if is_extract:
|
|
42
|
+
prompt = ChatPromptTemplate.from_messages([
|
|
43
|
+
MessagesPlaceholder(variable_name="messages"),
|
|
44
|
+
HumanMessage(content=f"""
|
|
45
|
+
请提取信息并遵循以下规则:
|
|
46
|
+
1. 准确率要求:{accuracy_instructions.strip()}
|
|
47
|
+
2. 输出格式:{parser.get_format_instructions()}
|
|
48
|
+
""")
|
|
49
|
+
])
|
|
50
|
+
else:
|
|
51
|
+
prompt = override_prompt or ChatPromptTemplate.from_messages([
|
|
52
|
+
MessagesPlaceholder(variable_name="messages"),
|
|
53
|
+
HumanMessage(content=f"""
|
|
54
|
+
输出格式:{parser.get_format_instructions()}
|
|
55
|
+
""")
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
# 文本处理函数
|
|
59
|
+
def extract_response_content(response: BaseMessage) -> str:
|
|
60
|
+
try:
|
|
61
|
+
return response.content
|
|
62
|
+
except Exception as e:
|
|
63
|
+
raise ValueError(f"提取响应内容失败:{str(e)}") from e
|
|
64
|
+
|
|
65
|
+
def strip_code_block_markers(content: str) -> str:
|
|
66
|
+
try:
|
|
67
|
+
return content.strip("```json").strip("```").strip()
|
|
68
|
+
except Exception as e:
|
|
69
|
+
raise ValueError(f"移除代码块标记失败:{str(e)}") from e
|
|
70
|
+
|
|
71
|
+
def normalize_in_json(content: str) -> str:
|
|
72
|
+
try:
|
|
73
|
+
return content.replace("None", "null").replace("none", "null").replace("NONE", "null").replace("''", '""')
|
|
74
|
+
except Exception as e:
|
|
75
|
+
raise ValueError(f"JSON格式化失败:{str(e)}") from e
|
|
76
|
+
|
|
77
|
+
def default_parse_to_pydantic(content: str) -> BaseModel:
|
|
78
|
+
try:
|
|
79
|
+
return parser.parse(content)
|
|
80
|
+
except (ValidationError, ValueError) as e:
|
|
81
|
+
raise ValueError(f"解析结构化结果失败:{str(e)}") from e
|
|
82
|
+
|
|
83
|
+
# ========== 构建处理链 ==========
|
|
84
|
+
base_chain = prompt | self.llm | RunnableLambda(
|
|
85
|
+
extract_response_content)
|
|
86
|
+
|
|
87
|
+
# 文本处理链
|
|
88
|
+
process_runnables = custom_processors or [
|
|
89
|
+
RunnableLambda(strip_code_block_markers),
|
|
90
|
+
RunnableLambda(normalize_in_json)
|
|
91
|
+
]
|
|
92
|
+
process_chain = base_chain
|
|
93
|
+
for runnable in process_runnables:
|
|
94
|
+
process_chain = process_chain | runnable
|
|
95
|
+
|
|
96
|
+
# 解析链
|
|
97
|
+
parse_chain = process_chain | RunnableLambda(
|
|
98
|
+
custom_parser or default_parse_to_pydantic)
|
|
99
|
+
|
|
100
|
+
# 重试链
|
|
101
|
+
retry_chain = parse_chain.with_retry(
|
|
102
|
+
retry_if_exception_type=(ValidationError, ValueError),
|
|
103
|
+
stop_after_attempt=max_retries,
|
|
104
|
+
wait_exponential_jitter=True,
|
|
105
|
+
exponential_jitter_params={
|
|
106
|
+
"initial": 0.1, "max": 3.0, "exp_base": 2.0, "jitter": 1.0}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return StructuredRunnableWithToken(retry_chain, self.langfuse)
|
|
110
|
+
|
|
111
|
+
# ========== 实现BaseChatModel抽象方法 ==========
|
|
112
|
+
def _generate(self, messages, stop=None, run_manager=None, ** kwargs):
|
|
113
|
+
return self.llm._generate(messages, stop=stop, run_manager=run_manager, ** kwargs)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def _llm_type(self) -> str:
|
|
117
|
+
return self.llm._llm_type
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from sqlalchemy import event
|
|
2
|
+
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
3
|
+
from sycommon.logging.kafka_log import SYLogger
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AsyncSQLTraceLogger:
|
|
10
|
+
@staticmethod
|
|
11
|
+
def setup_sql_logging(engine):
|
|
12
|
+
"""
|
|
13
|
+
为 SQLAlchemy 异步引擎注册事件监听器
|
|
14
|
+
注意:必须监听 engine.sync_engine,而不能直接监听 AsyncEngine
|
|
15
|
+
"""
|
|
16
|
+
def serialize_params(params):
|
|
17
|
+
"""处理特殊类型参数的序列化"""
|
|
18
|
+
if isinstance(params, (list, tuple)):
|
|
19
|
+
return [serialize_params(p) for p in params]
|
|
20
|
+
elif isinstance(params, dict):
|
|
21
|
+
return {k: serialize_params(v) for k, v in params.items()}
|
|
22
|
+
elif isinstance(params, datetime):
|
|
23
|
+
return params.isoformat()
|
|
24
|
+
elif isinstance(params, Decimal):
|
|
25
|
+
return float(params)
|
|
26
|
+
else:
|
|
27
|
+
return params
|
|
28
|
+
|
|
29
|
+
# ========== 核心修改 ==========
|
|
30
|
+
# 必须通过 engine.sync_engine 来获取底层的同步引擎进行监听
|
|
31
|
+
target = engine.sync_engine
|
|
32
|
+
|
|
33
|
+
@event.listens_for(target, "after_cursor_execute")
|
|
34
|
+
def after_cursor_execute(
|
|
35
|
+
conn, cursor, statement, parameters, context, executemany
|
|
36
|
+
):
|
|
37
|
+
try:
|
|
38
|
+
# 从连接选项中获取开始时间
|
|
39
|
+
# conn 在这里是同步连接对象
|
|
40
|
+
start_time = conn.info.get('_start_time') or \
|
|
41
|
+
conn._execution_options.get("_start_time", time.time())
|
|
42
|
+
|
|
43
|
+
execution_time = (time.time() - start_time) * 1000
|
|
44
|
+
|
|
45
|
+
sql_log = {
|
|
46
|
+
"type": "SQL",
|
|
47
|
+
"statement": statement,
|
|
48
|
+
"parameters": serialize_params(parameters),
|
|
49
|
+
"execution_time_ms": round(execution_time, 2),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# 注意:SYLogger.info 必须是线程安全的或非阻塞的,否则可能影响异步性能
|
|
53
|
+
SYLogger.info(f"SQL执行: {sql_log}")
|
|
54
|
+
except Exception as e:
|
|
55
|
+
SYLogger.error(f"SQL日志处理失败: {str(e)}")
|
|
56
|
+
|
|
57
|
+
@event.listens_for(target, "before_cursor_execute")
|
|
58
|
+
def before_cursor_execute(
|
|
59
|
+
conn, cursor, statement, parameters, context, executemany
|
|
60
|
+
):
|
|
61
|
+
try:
|
|
62
|
+
# 记录开始时间到 execution_options
|
|
63
|
+
conn = conn.execution_options(_start_time=time.time())
|
|
64
|
+
except Exception as e:
|
|
65
|
+
SYLogger.error(f"SQL开始时间记录失败: {str(e)}")
|