sycommon-python-lib 0.1.56b2__py3-none-any.whl → 0.1.56b4__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/llm/get_llm.py +177 -108
- sycommon/llm/llm_tokens.py +119 -0
- sycommon/notice/__init__.py +0 -0
- sycommon/notice/uvicorn_monitor.py +195 -0
- sycommon/synacos/nacos_service.py +5 -1
- {sycommon_python_lib-0.1.56b2.dist-info → sycommon_python_lib-0.1.56b4.dist-info}/METADATA +1 -1
- {sycommon_python_lib-0.1.56b2.dist-info → sycommon_python_lib-0.1.56b4.dist-info}/RECORD +10 -7
- {sycommon_python_lib-0.1.56b2.dist-info → sycommon_python_lib-0.1.56b4.dist-info}/WHEEL +0 -0
- {sycommon_python_lib-0.1.56b2.dist-info → sycommon_python_lib-0.1.56b4.dist-info}/entry_points.txt +0 -0
- {sycommon_python_lib-0.1.56b2.dist-info → sycommon_python_lib-0.1.56b4.dist-info}/top_level.txt +0 -0
sycommon/llm/get_llm.py
CHANGED
|
@@ -1,48 +1,137 @@
|
|
|
1
|
-
from typing import Dict, Type, List,
|
|
2
|
-
|
|
1
|
+
from typing import Dict, Type, List, Optional, Callable, Any
|
|
3
2
|
from sycommon.llm.llm_logger import LLMLogger
|
|
4
3
|
from langchain_core.language_models import BaseChatModel
|
|
5
|
-
from langchain_core.runnables import Runnable, RunnableLambda
|
|
4
|
+
from langchain_core.runnables import Runnable, RunnableLambda, RunnableConfig
|
|
6
5
|
from langchain_core.output_parsers import PydanticOutputParser
|
|
7
|
-
from langchain_core.messages import BaseMessage,
|
|
6
|
+
from langchain_core.messages import BaseMessage, HumanMessage
|
|
8
7
|
from langchain.chat_models import init_chat_model
|
|
9
8
|
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
|
10
|
-
from pydantic import BaseModel, ValidationError
|
|
9
|
+
from pydantic import BaseModel, ValidationError, Field
|
|
11
10
|
from sycommon.config.LLMConfig import LLMConfig
|
|
11
|
+
from sycommon.llm.llm_tokens import TokensCallbackHandler
|
|
12
|
+
from sycommon.logging.kafka_log import SYLogger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StructuredRunnableWithToken(Runnable):
|
|
16
|
+
"""带Token统计的Runnable类"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, retry_chain: Runnable):
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.retry_chain = retry_chain
|
|
21
|
+
|
|
22
|
+
def _adapt_input(self, input: Any) -> List[BaseMessage]:
|
|
23
|
+
"""适配输入格式"""
|
|
24
|
+
if isinstance(input, list) and all(isinstance(x, BaseMessage) for x in input):
|
|
25
|
+
return input
|
|
26
|
+
elif isinstance(input, BaseMessage):
|
|
27
|
+
return [input]
|
|
28
|
+
elif isinstance(input, str):
|
|
29
|
+
return [HumanMessage(content=input)]
|
|
30
|
+
elif isinstance(input, dict) and "input" in input:
|
|
31
|
+
return [HumanMessage(content=str(input["input"]))]
|
|
32
|
+
else:
|
|
33
|
+
raise ValueError(f"不支持的输入格式:{type(input)}")
|
|
12
34
|
|
|
35
|
+
def _get_callback_config(self, config: Optional[RunnableConfig] = None) -> tuple[RunnableConfig, TokensCallbackHandler]:
|
|
36
|
+
"""构建包含Token统计的回调配置"""
|
|
37
|
+
# 每次调用创建新的Token处理器实例
|
|
38
|
+
token_handler = TokensCallbackHandler()
|
|
13
39
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
# 初始化配置
|
|
41
|
+
if config is None:
|
|
42
|
+
processed_config = {"callbacks": []}
|
|
43
|
+
else:
|
|
44
|
+
processed_config = config.copy()
|
|
45
|
+
if "callbacks" not in processed_config:
|
|
46
|
+
processed_config["callbacks"] = []
|
|
47
|
+
|
|
48
|
+
# 添加回调(去重)
|
|
49
|
+
callbacks = processed_config["callbacks"]
|
|
50
|
+
# 添加LLMLogger(如果不存在)
|
|
51
|
+
if not any(isinstance(cb, LLMLogger) for cb in callbacks):
|
|
52
|
+
callbacks.append(LLMLogger())
|
|
53
|
+
# 添加Token处理器
|
|
54
|
+
callbacks.append(token_handler)
|
|
55
|
+
|
|
56
|
+
# 按类型去重
|
|
57
|
+
callback_types = {}
|
|
58
|
+
unique_callbacks = []
|
|
59
|
+
for cb in callbacks:
|
|
60
|
+
cb_type = type(cb)
|
|
61
|
+
if cb_type not in callback_types:
|
|
62
|
+
callback_types[cb_type] = cb
|
|
63
|
+
unique_callbacks.append(cb)
|
|
64
|
+
|
|
65
|
+
processed_config["callbacks"] = unique_callbacks
|
|
66
|
+
|
|
67
|
+
return processed_config, token_handler
|
|
68
|
+
|
|
69
|
+
# 同步调用
|
|
70
|
+
def invoke(self, input: Any, config: Optional[RunnableConfig] = None, ** kwargs) -> Dict[str, Any]:
|
|
71
|
+
try:
|
|
72
|
+
processed_config, token_handler = self._get_callback_config(
|
|
73
|
+
config)
|
|
74
|
+
adapted_input = self._adapt_input(input)
|
|
75
|
+
|
|
76
|
+
structured_result = self.retry_chain.invoke(
|
|
77
|
+
{"messages": adapted_input},
|
|
78
|
+
config=processed_config,
|
|
79
|
+
**kwargs
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# 获取Token统计结果
|
|
83
|
+
token_usage = token_handler.usage_metadata
|
|
84
|
+
structured_result._token_usage_ = token_usage
|
|
85
|
+
|
|
86
|
+
return structured_result
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
SYLogger.error(f"同步LLM调用失败: {str(e)}", exc_info=True)
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
# 异步调用
|
|
93
|
+
async def ainvoke(self, input: Any, config: Optional[RunnableConfig] = None, ** kwargs) -> Dict[str, Any]:
|
|
94
|
+
try:
|
|
95
|
+
processed_config, token_handler = self._get_callback_config(
|
|
96
|
+
config)
|
|
97
|
+
adapted_input = self._adapt_input(input)
|
|
98
|
+
|
|
99
|
+
structured_result = await self.retry_chain.ainvoke(
|
|
100
|
+
{"messages": adapted_input},
|
|
101
|
+
config=processed_config,
|
|
102
|
+
**kwargs
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
token_usage = token_handler.usage_metadata
|
|
106
|
+
structured_result._token_usage_ = token_usage
|
|
107
|
+
|
|
108
|
+
return structured_result
|
|
109
|
+
|
|
110
|
+
except Exception as e:
|
|
111
|
+
SYLogger.error(f"异步LLM调用失败: {str(e)}", exc_info=True)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class LLMWithAutoTokenUsage(BaseChatModel):
|
|
116
|
+
"""自动为结构化调用返回token_usage的LLM包装类"""
|
|
117
|
+
llm: BaseChatModel = Field(default=None)
|
|
118
|
+
|
|
119
|
+
def __init__(self, llm: BaseChatModel, **kwargs):
|
|
120
|
+
super().__init__(llm=llm, ** kwargs)
|
|
31
121
|
|
|
32
|
-
# 为LLM动态添加with_structured_output方法,官方的with_structured_output方法有概率在qwen2.5中导致模型卡死不返回数据,2.5对functioncall支持不好
|
|
33
122
|
def with_structured_output(
|
|
34
|
-
self
|
|
123
|
+
self,
|
|
35
124
|
output_model: Type[BaseModel],
|
|
36
125
|
max_retries: int = 3,
|
|
37
126
|
is_extract: bool = False,
|
|
38
127
|
override_prompt: ChatPromptTemplate = None,
|
|
39
|
-
# 自定义处理函数列表(每个函数接收str,返回str)
|
|
40
128
|
custom_processors: Optional[List[Callable[[str], str]]] = None,
|
|
41
|
-
# 自定义解析函数(接收str,返回BaseModel)
|
|
42
129
|
custom_parser: Optional[Callable[[str], BaseModel]] = None
|
|
43
|
-
) -> Runnable
|
|
130
|
+
) -> Runnable:
|
|
131
|
+
"""返回支持自动统计Token的结构化Runnable"""
|
|
44
132
|
parser = PydanticOutputParser(pydantic_object=output_model)
|
|
45
133
|
|
|
134
|
+
# 提示词模板
|
|
46
135
|
accuracy_instructions = """
|
|
47
136
|
字段值的抽取准确率(0~1之间),评分规则:
|
|
48
137
|
1.0(完全准确):直接从原文提取,无需任何加工,且格式与原文完全一致
|
|
@@ -52,7 +141,6 @@ def get_llm(model: str = None, streaming: bool = False) -> BaseChatModel:
|
|
|
52
141
|
"""
|
|
53
142
|
|
|
54
143
|
if is_extract:
|
|
55
|
-
# 抽取模式下使用固定的抽取专用prompt
|
|
56
144
|
prompt = ChatPromptTemplate.from_messages([
|
|
57
145
|
MessagesPlaceholder(variable_name="messages"),
|
|
58
146
|
HumanMessage(content=f"""
|
|
@@ -62,116 +150,97 @@ def get_llm(model: str = None, streaming: bool = False) -> BaseChatModel:
|
|
|
62
150
|
""")
|
|
63
151
|
])
|
|
64
152
|
else:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
])
|
|
74
|
-
|
|
75
|
-
# ========== 基础处理函数 ==========
|
|
153
|
+
prompt = override_prompt or ChatPromptTemplate.from_messages([
|
|
154
|
+
MessagesPlaceholder(variable_name="messages"),
|
|
155
|
+
HumanMessage(content=f"""
|
|
156
|
+
输出格式:{parser.get_format_instructions()}
|
|
157
|
+
""")
|
|
158
|
+
])
|
|
159
|
+
|
|
160
|
+
# 文本处理函数
|
|
76
161
|
def extract_response_content(response: BaseMessage) -> str:
|
|
77
|
-
"""提取响应中的文本内容"""
|
|
78
162
|
try:
|
|
79
163
|
return response.content
|
|
80
164
|
except Exception as e:
|
|
81
165
|
raise ValueError(f"提取响应内容失败:{str(e)}") from e
|
|
82
166
|
|
|
83
167
|
def strip_code_block_markers(content: str) -> str:
|
|
84
|
-
"""移除JSON代码块标记(```json/```)"""
|
|
85
168
|
try:
|
|
86
169
|
return content.strip("```json").strip("```").strip()
|
|
87
170
|
except Exception as e:
|
|
88
|
-
raise ValueError(
|
|
89
|
-
f"移除代码块标记失败(内容:{str(content)[:100]}):{str(e)}") from e
|
|
171
|
+
raise ValueError(f"移除代码块标记失败:{str(e)}") from e
|
|
90
172
|
|
|
91
173
|
def normalize_in_json(content: str) -> str:
|
|
92
|
-
"""将None替换为null,确保JSON格式合法"""
|
|
93
174
|
try:
|
|
94
|
-
|
|
95
|
-
cleaned = cleaned.replace("none", "null")
|
|
96
|
-
cleaned = cleaned.replace("NONE", "null")
|
|
97
|
-
cleaned = cleaned.replace("''", '""')
|
|
98
|
-
return cleaned
|
|
175
|
+
return content.replace("None", "null").replace("none", "null").replace("NONE", "null").replace("''", '""')
|
|
99
176
|
except Exception as e:
|
|
100
|
-
raise ValueError(
|
|
101
|
-
f"替换None为null失败(内容:{str(content)[:100]}):{str(e)}") from e
|
|
177
|
+
raise ValueError(f"JSON格式化失败:{str(e)}") from e
|
|
102
178
|
|
|
103
179
|
def default_parse_to_pydantic(content: str) -> BaseModel:
|
|
104
|
-
"""默认解析函数:将处理后的文本解析为Pydantic模型"""
|
|
105
180
|
try:
|
|
106
181
|
return parser.parse(content)
|
|
107
182
|
except (ValidationError, ValueError) as e:
|
|
108
|
-
raise
|
|
109
|
-
|
|
110
|
-
# ========== 构建处理链条 ==========
|
|
111
|
-
# 基础链 prompt → LLM → 提取响应内容
|
|
112
|
-
base_chain = (
|
|
113
|
-
prompt
|
|
114
|
-
| self
|
|
115
|
-
| RunnableLambda(extract_response_content)
|
|
116
|
-
)
|
|
183
|
+
raise ValueError(f"解析结构化结果失败:{str(e)}") from e
|
|
117
184
|
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
process_runnables = [RunnableLambda(
|
|
122
|
-
func) for func in custom_processors]
|
|
123
|
-
else:
|
|
124
|
-
# 默认处理函数:移除代码块标记 → 标准化JSON空值
|
|
125
|
-
process_runnables = [
|
|
126
|
-
RunnableLambda(strip_code_block_markers),
|
|
127
|
-
RunnableLambda(normalize_in_json)
|
|
128
|
-
]
|
|
185
|
+
# ========== 构建处理链 ==========
|
|
186
|
+
base_chain = prompt | self.llm | RunnableLambda(
|
|
187
|
+
extract_response_content)
|
|
129
188
|
|
|
130
|
-
#
|
|
189
|
+
# 文本处理链
|
|
190
|
+
process_runnables = custom_processors or [
|
|
191
|
+
RunnableLambda(strip_code_block_markers),
|
|
192
|
+
RunnableLambda(normalize_in_json)
|
|
193
|
+
]
|
|
131
194
|
process_chain = base_chain
|
|
132
195
|
for runnable in process_runnables:
|
|
133
196
|
process_chain = process_chain | runnable
|
|
134
197
|
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
|
|
198
|
+
# 解析链
|
|
199
|
+
parse_chain = process_chain | RunnableLambda(
|
|
200
|
+
custom_parser or default_parse_to_pydantic)
|
|
138
201
|
|
|
202
|
+
# 重试链
|
|
139
203
|
retry_chain = parse_chain.with_retry(
|
|
140
204
|
retry_if_exception_type=(ValidationError, ValueError),
|
|
141
205
|
stop_after_attempt=max_retries,
|
|
142
206
|
wait_exponential_jitter=True,
|
|
143
207
|
exponential_jitter_params={
|
|
144
|
-
"initial": 0.1,
|
|
145
|
-
"max": 3.0, # 最大等待时间(秒)
|
|
146
|
-
"exp_base": 2.0, # 指数基数(默认2)
|
|
147
|
-
"jitter": 1.0 # 随机抖动值(默认1)
|
|
148
|
-
}
|
|
208
|
+
"initial": 0.1, "max": 3.0, "exp_base": 2.0, "jitter": 1.0}
|
|
149
209
|
)
|
|
150
210
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
211
|
+
return StructuredRunnableWithToken(retry_chain)
|
|
212
|
+
|
|
213
|
+
# ========== 实现BaseChatModel抽象方法 ==========
|
|
214
|
+
def _generate(self, messages, stop=None, run_manager=None, ** kwargs):
|
|
215
|
+
return self.llm._generate(messages, stop=stop, run_manager=run_manager, ** kwargs)
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def _llm_type(self) -> str:
|
|
219
|
+
return self.llm._llm_type
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_llm(
|
|
223
|
+
model: str = None,
|
|
224
|
+
streaming: bool = False
|
|
225
|
+
) -> LLMWithAutoTokenUsage:
|
|
226
|
+
if not model:
|
|
227
|
+
model = "Qwen2.5-72B"
|
|
228
|
+
|
|
229
|
+
llmConfig = LLMConfig.from_config(model)
|
|
230
|
+
if not llmConfig:
|
|
231
|
+
raise Exception(f"无效的模型配置:{model}")
|
|
232
|
+
|
|
233
|
+
llm = init_chat_model(
|
|
234
|
+
model_provider=llmConfig.provider,
|
|
235
|
+
model=llmConfig.model,
|
|
236
|
+
base_url=llmConfig.baseUrl,
|
|
237
|
+
api_key="-",
|
|
238
|
+
temperature=0.1,
|
|
239
|
+
streaming=streaming,
|
|
240
|
+
callbacks=[LLMLogger()]
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if llm is None:
|
|
244
|
+
raise Exception(f"初始化原始LLM实例失败:{model}")
|
|
245
|
+
|
|
246
|
+
return LLMWithAutoTokenUsage(llm)
|
|
@@ -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
|
|
File without changes
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import sys
|
|
3
|
+
import traceback
|
|
4
|
+
import aiohttp
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
|
9
|
+
from sycommon.config.Config import Config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def send_wechat_markdown_msg(
|
|
13
|
+
content: str,
|
|
14
|
+
webhook: str = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=b9992872-ea66-494f-8683-f43411b7713a"
|
|
15
|
+
) -> Optional[dict]:
|
|
16
|
+
"""
|
|
17
|
+
异步发送企业微信Markdown格式的WebHook消息
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
content: Markdown格式的消息内容(支持企业微信支持的markdown语法)
|
|
21
|
+
webhook: 完整的企业微信WebHook URL(默认值包含key)
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
接口返回的JSON数据(dict),失败返回None
|
|
25
|
+
"""
|
|
26
|
+
# 设置请求头
|
|
27
|
+
headers = {
|
|
28
|
+
"Content-Type": "application/json; charset=utf-8"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# 构造请求体(Markdown格式)
|
|
32
|
+
payload = {
|
|
33
|
+
"msgtype": "markdown",
|
|
34
|
+
"markdown": {
|
|
35
|
+
"content": content
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
async with aiohttp.ClientSession() as session:
|
|
41
|
+
async with session.post(
|
|
42
|
+
url=webhook,
|
|
43
|
+
data=json.dumps(payload, ensure_ascii=False),
|
|
44
|
+
headers=headers,
|
|
45
|
+
timeout=aiohttp.ClientTimeout(total=10)
|
|
46
|
+
) as response:
|
|
47
|
+
status = response.status
|
|
48
|
+
response_text = await response.text()
|
|
49
|
+
response_data = json.loads(
|
|
50
|
+
response_text) if response_text else {}
|
|
51
|
+
|
|
52
|
+
if status == 200 and response_data.get("errcode") == 0:
|
|
53
|
+
print(f"消息发送成功: {response_data}")
|
|
54
|
+
return response_data
|
|
55
|
+
else:
|
|
56
|
+
print(f"消息发送失败 - 状态码: {status}, 响应: {response_data}")
|
|
57
|
+
return None
|
|
58
|
+
except Exception as e:
|
|
59
|
+
print(f"错误:未知异常 - {str(e)}")
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def send_webhook(error_info: dict = None, webhook: str = None):
|
|
64
|
+
"""
|
|
65
|
+
发送服务启动结果的企业微信通知
|
|
66
|
+
Args:
|
|
67
|
+
error_info: 错误信息字典(启动失败时必填),包含:error_type, error_msg, stack_trace, elapsed_time
|
|
68
|
+
webhook: 完整的企业微信WebHook URL(覆盖默认值)
|
|
69
|
+
"""
|
|
70
|
+
# 获取服务名和环境(兼容配置读取失败)
|
|
71
|
+
try:
|
|
72
|
+
service_name = Config().config.get('Name', "未知服务")
|
|
73
|
+
env = Config().config.get('Nacos', {}).get('namespaceId', '未知环境')
|
|
74
|
+
except Exception as e:
|
|
75
|
+
service_name = "未知服务"
|
|
76
|
+
env = "未知环境"
|
|
77
|
+
print(f"读取配置失败: {str(e)}")
|
|
78
|
+
|
|
79
|
+
start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
80
|
+
|
|
81
|
+
# 启动失败的通知内容(包含详细错误信息)
|
|
82
|
+
error_type = error_info.get("error_type", "未知错误")
|
|
83
|
+
error_msg = error_info.get("error_msg", "无错误信息")
|
|
84
|
+
stack_trace = error_info.get("stack_trace", "无堆栈信息")[:500] # 限制长度避免超限
|
|
85
|
+
elapsed_time = error_info.get("elapsed_time", 0)
|
|
86
|
+
|
|
87
|
+
markdown_content = f"""### {service_name}服务启动失败告警 ⚠️
|
|
88
|
+
> 环境: <font color="warning">{env}</font>
|
|
89
|
+
> 启动时间: <font color="comment">{start_time}</font>
|
|
90
|
+
> 耗时: <font color="comment">{elapsed_time:.2f}秒</font>
|
|
91
|
+
> 错误类型: <font color="danger">{error_type}</font>
|
|
92
|
+
> 错误信息: <font color="danger">{error_msg}</font>
|
|
93
|
+
> 错误堆栈: {stack_trace}"""
|
|
94
|
+
|
|
95
|
+
# 发送消息(优先使用传入的webhook,否则用默认值)
|
|
96
|
+
default_webhook = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=b9992872-ea66-494f-8683-f43411b7713a"
|
|
97
|
+
result = await send_wechat_markdown_msg(
|
|
98
|
+
content=markdown_content,
|
|
99
|
+
webhook=webhook if webhook else default_webhook
|
|
100
|
+
)
|
|
101
|
+
print(f"通知发送结果: {result}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def run(*args, webhook: str = None, **kwargs):
|
|
105
|
+
"""
|
|
106
|
+
带企业微信告警的Uvicorn启动监控
|
|
107
|
+
调用方式1(默认配置):uvicorn_monitor.run("app:app", **app.state.config)
|
|
108
|
+
调用方式2(自定义webhook):uvicorn_monitor.run("app:app", webhook="完整URL", **app.state.config)
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
*args: 传递给uvicorn.run的位置参数(如"app:app")
|
|
112
|
+
webhook: 完整的企业微信WebHook URL(可选,覆盖默认值)
|
|
113
|
+
**kwargs: 传递给uvicorn.run的关键字参数(如app.state.config)
|
|
114
|
+
"""
|
|
115
|
+
# 判断环境
|
|
116
|
+
env = Config().config.get('Nacos', {}).get('namespaceId', '未知环境')
|
|
117
|
+
if env == "prod":
|
|
118
|
+
import uvicorn
|
|
119
|
+
uvicorn.run(*args, **kwargs)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
# 记录启动开始时间
|
|
123
|
+
start_time = datetime.now()
|
|
124
|
+
|
|
125
|
+
if webhook:
|
|
126
|
+
# 脱敏展示webhook(隐藏key的后半部分)
|
|
127
|
+
parsed = urlparse(webhook)
|
|
128
|
+
query = parse_qs(parsed.query)
|
|
129
|
+
if 'key' in query and query['key'][0]:
|
|
130
|
+
key = query['key'][0]
|
|
131
|
+
masked_key = key[:8] + "****" if len(key) > 8 else key + "****"
|
|
132
|
+
query['key'] = [masked_key]
|
|
133
|
+
masked_query = urlencode(query, doseq=True)
|
|
134
|
+
masked_webhook = urlunparse(
|
|
135
|
+
(parsed.scheme, parsed.netloc, parsed.path, parsed.params, masked_query, parsed.fragment))
|
|
136
|
+
print(f"自定义企业微信WebHook: {masked_webhook}")
|
|
137
|
+
|
|
138
|
+
# 初始化错误信息
|
|
139
|
+
error_info = None
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
import uvicorn
|
|
143
|
+
# 执行启动(如果启动成功,此方法会阻塞,不会执行后续except)
|
|
144
|
+
uvicorn.run(*args, **kwargs)
|
|
145
|
+
|
|
146
|
+
except KeyboardInterrupt:
|
|
147
|
+
# 处理用户手动中断(不算启动失败)
|
|
148
|
+
elapsed = (datetime.now() - start_time).total_seconds()
|
|
149
|
+
print(f"\n{'='*50}")
|
|
150
|
+
print(f"ℹ️ 应用被用户手动中断")
|
|
151
|
+
print(f"启动耗时: {elapsed:.2f} 秒")
|
|
152
|
+
print(f"{'='*50}\n")
|
|
153
|
+
sys.exit(0)
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
# 捕获启动失败异常,收集错误信息
|
|
157
|
+
elapsed = (datetime.now() - start_time).total_seconds()
|
|
158
|
+
# 捕获堆栈信息
|
|
159
|
+
stack_trace = traceback.format_exc()
|
|
160
|
+
|
|
161
|
+
# 构造错误信息字典
|
|
162
|
+
error_info = {
|
|
163
|
+
"error_type": type(e).__name__,
|
|
164
|
+
"error_msg": str(e),
|
|
165
|
+
"stack_trace": stack_trace,
|
|
166
|
+
"elapsed_time": elapsed
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# 打印错误信息
|
|
170
|
+
print(f"\n{'='*50}")
|
|
171
|
+
print(f"🚨 应用启动失败!")
|
|
172
|
+
print(f"失败时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
173
|
+
print(f"错误类型: {type(e).__name__}")
|
|
174
|
+
print(f"错误信息: {str(e)}")
|
|
175
|
+
print(f"\n📝 错误堆栈(关键):")
|
|
176
|
+
print(f"-"*50)
|
|
177
|
+
traceback.print_exc(file=sys.stdout)
|
|
178
|
+
print(f"\n⏱️ 启动耗时: {elapsed:.2f} 秒")
|
|
179
|
+
print(f"{'='*50}\n")
|
|
180
|
+
|
|
181
|
+
finally:
|
|
182
|
+
# 运行异步通知函数,传递自定义的webhook参数
|
|
183
|
+
try:
|
|
184
|
+
asyncio.run(send_webhook(
|
|
185
|
+
error_info=error_info,
|
|
186
|
+
webhook=webhook
|
|
187
|
+
))
|
|
188
|
+
except Exception as e:
|
|
189
|
+
print(f"错误:异步通知失败 - {str(e)}")
|
|
190
|
+
# 启动失败时退出程序
|
|
191
|
+
sys.exit(1)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# 兼容旧调用方式(可选)
|
|
195
|
+
run_uvicorn_with_monitor = run
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
import threading
|
|
3
2
|
import json
|
|
4
3
|
from typing import Callable, Dict, List, Optional
|
|
@@ -818,6 +817,11 @@ class NacosService(metaclass=SingletonMeta):
|
|
|
818
817
|
return []
|
|
819
818
|
|
|
820
819
|
all_instances = instances.get('hosts', [])
|
|
820
|
+
# 筛选已上线实例
|
|
821
|
+
all_instances = [
|
|
822
|
+
instance for instance in all_instances
|
|
823
|
+
if instance.get('enabled', True) # 默认True担心阿里变更sdk
|
|
824
|
+
]
|
|
821
825
|
SYLogger.info(
|
|
822
826
|
f"nacos:共发现 {len(all_instances)} 个 {service_name} 服务实例")
|
|
823
827
|
|
|
@@ -18,8 +18,9 @@ sycommon/health/metrics.py,sha256=fHqO73JuhoZkNPR-xIlxieXiTCvttq-kG-tvxag1s1s,26
|
|
|
18
18
|
sycommon/health/ping.py,sha256=FTlnIKk5y1mPfS1ZGOeT5IM_2udF5aqVLubEtuBp18M,250
|
|
19
19
|
sycommon/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
20
|
sycommon/llm/embedding.py,sha256=Wcm2W7JU3FyZXvOhMSdyhiZJhJS1MwW8bMqdrOzD2TY,5768
|
|
21
|
-
sycommon/llm/get_llm.py,sha256=
|
|
21
|
+
sycommon/llm/get_llm.py,sha256=wawJO_WSLSYPE8ImL421SYBPtAvWqwbRAcUN7d5i0W0,9434
|
|
22
22
|
sycommon/llm/llm_logger.py,sha256=n4UeNy_-g4oHQOsw-VUzF4uo3JVRLtxaMp1FcI8FiEo,5437
|
|
23
|
+
sycommon/llm/llm_tokens.py,sha256=-udDyFcmyzx6UAwIi6_d_wwI5kMd5w0-WcS2soVPQxg,4309
|
|
23
24
|
sycommon/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
25
|
sycommon/logging/async_sql_logger.py,sha256=_OY36XkUm__U3NhMgiecy-qd-nptZ_0gpE3J8lGAr58,2619
|
|
25
26
|
sycommon/logging/kafka_log.py,sha256=sVw-dFZKEgCosjSUqgTj7YrpK-ggXhleZFwMUVhl-K0,21416
|
|
@@ -43,6 +44,8 @@ sycommon/models/mqlistener_config.py,sha256=vXp2uMmd0XQ5B9noSRXWHewTy-juQ2y7IsWt
|
|
|
43
44
|
sycommon/models/mqmsg_model.py,sha256=cxn0M5b0utQK6crMYmL-1waeGYHvK3AlGaRy23clqTE,277
|
|
44
45
|
sycommon/models/mqsend_config.py,sha256=NQX9dc8PpuquMG36GCVhJe8omAW1KVXXqr6lSRU6D7I,268
|
|
45
46
|
sycommon/models/sso_user.py,sha256=i1WAN6k5sPcPApQEdtjpWDy7VrzWLpOrOQewGLGoGIw,2702
|
|
47
|
+
sycommon/notice/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
48
|
+
sycommon/notice/uvicorn_monitor.py,sha256=NRQVWs3jzZq9IY0M55s7LvOKtacsNpfCGGzH5071dGw,7181
|
|
46
49
|
sycommon/rabbitmq/rabbitmq_client.py,sha256=GkuYILMZJnvgZs4ID46I-w_UGzzI28YUydKgkTIDhIs,20226
|
|
47
50
|
sycommon/rabbitmq/rabbitmq_pool.py,sha256=QtUcK4HuepRqRmy5XkUQo8gDgj74fr77CX7T5rN0y4I,15640
|
|
48
51
|
sycommon/rabbitmq/rabbitmq_service.py,sha256=wpEJynP0gzbnmMyB02sfR9nTWB4hfTTP00xQxDJu644,38119
|
|
@@ -54,15 +57,15 @@ sycommon/synacos/example.py,sha256=61XL03tU8WTNOo3FUduf93F2fAwah1S0lbH1ufhRhRk,5
|
|
|
54
57
|
sycommon/synacos/example2.py,sha256=adUaru3Hy482KrOA17DfaC4nwvLj8etIDS_KrWLWmCU,4811
|
|
55
58
|
sycommon/synacos/feign.py,sha256=frB3D5LeFDtT3pJLFOwFzEOrNAJKeQNGk-BzUg9T3WM,8295
|
|
56
59
|
sycommon/synacos/feign_client.py,sha256=ExO7Pd5B3eFKDjXqBRc260K1jkI49IYguLwJJaD2R-o,16166
|
|
57
|
-
sycommon/synacos/nacos_service.py,sha256=
|
|
60
|
+
sycommon/synacos/nacos_service.py,sha256=9cW2tSjm0vuYFRMPR_cl_EOZPe4axQDCUBcVxoGdPKM,36180
|
|
58
61
|
sycommon/synacos/param.py,sha256=KcfSkxnXOa0TGmCjY8hdzU9pzUsA8-4PeyBKWI2-568,1765
|
|
59
62
|
sycommon/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
60
63
|
sycommon/tools/docs.py,sha256=OPj2ETheuWjXLyaXtaZPbwmJKfJaYXV5s4XMVAUNrms,1607
|
|
61
64
|
sycommon/tools/merge_headers.py,sha256=HV_i52Q-9se3SP8qh7ZGYl8bP7Fxtal4CGVkyMwEdM8,4373
|
|
62
65
|
sycommon/tools/snowflake.py,sha256=lVEe5mNCOgz5OqGQpf5_nXaGnRJlI2STX2s-ppTtanA,11947
|
|
63
66
|
sycommon/tools/timing.py,sha256=OiiE7P07lRoMzX9kzb8sZU9cDb0zNnqIlY5pWqHcnkY,2064
|
|
64
|
-
sycommon_python_lib-0.1.
|
|
65
|
-
sycommon_python_lib-0.1.
|
|
66
|
-
sycommon_python_lib-0.1.
|
|
67
|
-
sycommon_python_lib-0.1.
|
|
68
|
-
sycommon_python_lib-0.1.
|
|
67
|
+
sycommon_python_lib-0.1.56b4.dist-info/METADATA,sha256=c2XIy1w2EUZHGBPqJQvPTb6eYH2i5jTVNmnV-cPD-hs,7226
|
|
68
|
+
sycommon_python_lib-0.1.56b4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
69
|
+
sycommon_python_lib-0.1.56b4.dist-info/entry_points.txt,sha256=q_h2nbvhhmdnsOUZEIwpuoDjaNfBF9XqppDEmQn9d_A,46
|
|
70
|
+
sycommon_python_lib-0.1.56b4.dist-info/top_level.txt,sha256=98CJ-cyM2WIKxLz-Pf0AitWLhJyrfXvyY8slwjTXNuc,17
|
|
71
|
+
sycommon_python_lib-0.1.56b4.dist-info/RECORD,,
|
|
File without changes
|
{sycommon_python_lib-0.1.56b2.dist-info → sycommon_python_lib-0.1.56b4.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{sycommon_python_lib-0.1.56b2.dist-info → sycommon_python_lib-0.1.56b4.dist-info}/top_level.txt
RENAMED
|
File without changes
|