sycommon-python-lib 0.1.56b3__tar.gz → 0.1.56b5__tar.gz

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.
Files changed (78) hide show
  1. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/PKG-INFO +1 -1
  2. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/pyproject.toml +1 -1
  3. sycommon_python_lib-0.1.56b5/src/sycommon/llm/get_llm.py +246 -0
  4. sycommon_python_lib-0.1.56b5/src/sycommon/llm/llm_tokens.py +119 -0
  5. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/rabbitmq/rabbitmq_client.py +26 -19
  6. sycommon_python_lib-0.1.56b5/src/sycommon/rabbitmq/rabbitmq_pool.py +320 -0
  7. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/synacos/nacos_service.py +5 -0
  8. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon_python_lib.egg-info/PKG-INFO +1 -1
  9. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon_python_lib.egg-info/SOURCES.txt +1 -0
  10. sycommon_python_lib-0.1.56b3/src/sycommon/llm/get_llm.py +0 -177
  11. sycommon_python_lib-0.1.56b3/src/sycommon/rabbitmq/rabbitmq_pool.py +0 -338
  12. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/README.md +0 -0
  13. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/setup.cfg +0 -0
  14. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/command/cli.py +0 -0
  15. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/__init__.py +0 -0
  16. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/config/Config.py +0 -0
  17. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/config/DatabaseConfig.py +0 -0
  18. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/config/EmbeddingConfig.py +0 -0
  19. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/config/LLMConfig.py +0 -0
  20. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/config/MQConfig.py +0 -0
  21. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/config/RerankerConfig.py +0 -0
  22. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/config/__init__.py +0 -0
  23. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/database/async_base_db_service.py +0 -0
  24. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/database/async_database_service.py +0 -0
  25. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/database/base_db_service.py +0 -0
  26. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/database/database_service.py +0 -0
  27. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/health/__init__.py +0 -0
  28. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/health/health_check.py +0 -0
  29. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/health/metrics.py +0 -0
  30. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/health/ping.py +0 -0
  31. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/llm/__init__.py +0 -0
  32. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/llm/embedding.py +0 -0
  33. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/llm/llm_logger.py +0 -0
  34. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/logging/__init__.py +0 -0
  35. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/logging/async_sql_logger.py +0 -0
  36. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/logging/kafka_log.py +0 -0
  37. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/logging/logger_levels.py +0 -0
  38. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/logging/logger_wrapper.py +0 -0
  39. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/logging/sql_logger.py +0 -0
  40. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/middleware/__init__.py +0 -0
  41. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/middleware/context.py +0 -0
  42. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/middleware/cors.py +0 -0
  43. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/middleware/docs.py +0 -0
  44. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/middleware/exception.py +0 -0
  45. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/middleware/middleware.py +0 -0
  46. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/middleware/monitor_memory.py +0 -0
  47. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/middleware/mq.py +0 -0
  48. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/middleware/timeout.py +0 -0
  49. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/middleware/traceid.py +0 -0
  50. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/models/__init__.py +0 -0
  51. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/models/base_http.py +0 -0
  52. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/models/log.py +0 -0
  53. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/models/mqlistener_config.py +0 -0
  54. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/models/mqmsg_model.py +0 -0
  55. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/models/mqsend_config.py +0 -0
  56. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/models/sso_user.py +0 -0
  57. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/notice/__init__.py +0 -0
  58. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/notice/uvicorn_monitor.py +0 -0
  59. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/rabbitmq/rabbitmq_service.py +0 -0
  60. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/services.py +0 -0
  61. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/sse/__init__.py +0 -0
  62. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/sse/event.py +0 -0
  63. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/sse/sse.py +0 -0
  64. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/synacos/__init__.py +0 -0
  65. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/synacos/example.py +0 -0
  66. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/synacos/example2.py +0 -0
  67. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/synacos/feign.py +0 -0
  68. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/synacos/feign_client.py +0 -0
  69. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/synacos/param.py +0 -0
  70. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/tools/__init__.py +0 -0
  71. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/tools/docs.py +0 -0
  72. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/tools/merge_headers.py +0 -0
  73. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/tools/snowflake.py +0 -0
  74. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon/tools/timing.py +0 -0
  75. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon_python_lib.egg-info/dependency_links.txt +0 -0
  76. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon_python_lib.egg-info/entry_points.txt +0 -0
  77. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon_python_lib.egg-info/requires.txt +0 -0
  78. {sycommon_python_lib-0.1.56b3 → sycommon_python_lib-0.1.56b5}/src/sycommon_python_lib.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.1.56b3
3
+ Version: 0.1.56b5
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sycommon-python-lib"
3
- version = "0.1.56-beta3"
3
+ version = "0.1.56-beta5"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -0,0 +1,246 @@
1
+ from typing import Dict, Type, List, Optional, Callable, Any
2
+ from sycommon.llm.llm_logger import LLMLogger
3
+ from langchain_core.language_models import BaseChatModel
4
+ from langchain_core.runnables import Runnable, RunnableLambda, RunnableConfig
5
+ from langchain_core.output_parsers import PydanticOutputParser
6
+ from langchain_core.messages import BaseMessage, HumanMessage
7
+ from langchain.chat_models import init_chat_model
8
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
9
+ from pydantic import BaseModel, ValidationError, Field
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)}")
34
+
35
+ def _get_callback_config(self, config: Optional[RunnableConfig] = None) -> tuple[RunnableConfig, TokensCallbackHandler]:
36
+ """构建包含Token统计的回调配置"""
37
+ # 每次调用创建新的Token处理器实例
38
+ token_handler = TokensCallbackHandler()
39
+
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)
121
+
122
+ def with_structured_output(
123
+ self,
124
+ output_model: Type[BaseModel],
125
+ max_retries: int = 3,
126
+ is_extract: bool = False,
127
+ override_prompt: ChatPromptTemplate = None,
128
+ custom_processors: Optional[List[Callable[[str], str]]] = None,
129
+ custom_parser: Optional[Callable[[str], BaseModel]] = None
130
+ ) -> Runnable:
131
+ """返回支持自动统计Token的结构化Runnable"""
132
+ parser = PydanticOutputParser(pydantic_object=output_model)
133
+
134
+ # 提示词模板
135
+ accuracy_instructions = """
136
+ 字段值的抽取准确率(0~1之间),评分规则:
137
+ 1.0(完全准确):直接从原文提取,无需任何加工,且格式与原文完全一致
138
+ 0.9(轻微处理):数据来源明确,但需进行格式标准化或冗余信息剔除(不改变原始数值)
139
+ 0.8(有限推断):数据需通过上下文关联或简单计算得出,仍有明确依据
140
+ 0.8以下(不可靠):数据需大量推测、存在歧义或来源不明,处理方式:直接忽略该数据,设置为None
141
+ """
142
+
143
+ if is_extract:
144
+ prompt = ChatPromptTemplate.from_messages([
145
+ MessagesPlaceholder(variable_name="messages"),
146
+ HumanMessage(content=f"""
147
+ 请提取信息并遵循以下规则:
148
+ 1. 准确率要求:{accuracy_instructions.strip()}
149
+ 2. 输出格式:{parser.get_format_instructions()}
150
+ """)
151
+ ])
152
+ else:
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
+ # 文本处理函数
161
+ def extract_response_content(response: BaseMessage) -> str:
162
+ try:
163
+ return response.content
164
+ except Exception as e:
165
+ raise ValueError(f"提取响应内容失败:{str(e)}") from e
166
+
167
+ def strip_code_block_markers(content: str) -> str:
168
+ try:
169
+ return content.strip("```json").strip("```").strip()
170
+ except Exception as e:
171
+ raise ValueError(f"移除代码块标记失败:{str(e)}") from e
172
+
173
+ def normalize_in_json(content: str) -> str:
174
+ try:
175
+ return content.replace("None", "null").replace("none", "null").replace("NONE", "null").replace("''", '""')
176
+ except Exception as e:
177
+ raise ValueError(f"JSON格式化失败:{str(e)}") from e
178
+
179
+ def default_parse_to_pydantic(content: str) -> BaseModel:
180
+ try:
181
+ return parser.parse(content)
182
+ except (ValidationError, ValueError) as e:
183
+ raise ValueError(f"解析结构化结果失败:{str(e)}") from e
184
+
185
+ # ========== 构建处理链 ==========
186
+ base_chain = prompt | self.llm | RunnableLambda(
187
+ extract_response_content)
188
+
189
+ # 文本处理链
190
+ process_runnables = custom_processors or [
191
+ RunnableLambda(strip_code_block_markers),
192
+ RunnableLambda(normalize_in_json)
193
+ ]
194
+ process_chain = base_chain
195
+ for runnable in process_runnables:
196
+ process_chain = process_chain | runnable
197
+
198
+ # 解析链
199
+ parse_chain = process_chain | RunnableLambda(
200
+ custom_parser or default_parse_to_pydantic)
201
+
202
+ # 重试链
203
+ retry_chain = parse_chain.with_retry(
204
+ retry_if_exception_type=(ValidationError, ValueError),
205
+ stop_after_attempt=max_retries,
206
+ wait_exponential_jitter=True,
207
+ exponential_jitter_params={
208
+ "initial": 0.1, "max": 3.0, "exp_base": 2.0, "jitter": 1.0}
209
+ )
210
+
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
@@ -145,10 +145,15 @@ class RabbitMQClient:
145
145
  raise RuntimeError("客户端已关闭,无法重新连接")
146
146
 
147
147
  async with self._connect_lock:
148
- # 释放旧资源(回调+通道,单通道无需归还,仅清理状态)
149
- if self._conn_close_callback and self._channel_conn:
150
- self._channel_conn.close_callbacks.discard(
151
- self._conn_close_callback)
148
+ # 1. 清理旧连接回调(防止内存泄漏)
149
+ if self._channel_conn and self._conn_close_callback:
150
+ try:
151
+ self._channel_conn.close_callbacks.discard(
152
+ self._conn_close_callback)
153
+ except Exception:
154
+ pass
155
+
156
+ # 2. 清理状态
152
157
  self._channel = None
153
158
  self._channel_conn = None
154
159
  self._exchange = None
@@ -156,18 +161,14 @@ class RabbitMQClient:
156
161
  self._conn_close_callback = None
157
162
 
158
163
  try:
159
- # 从单通道池获取通道+连接(连接池自动确保通道有效)
164
+ # 3. 获取新通道
160
165
  self._channel, self._channel_conn = await self.connection_pool.acquire_channel()
161
166
 
167
+ # 4. 设置新连接回调(使用 weakref)
162
168
  def on_conn_closed(conn: AbstractRobustConnection, exc: Optional[BaseException]):
163
- """连接关闭回调:触发固定间隔重连"""
164
- logger.warning(
165
- f"客户端连接关闭: {conn!r},原因: {exc}", exc_info=exc)
166
- self._reconnect_fail_count += 1
167
- # 超过阈值告警
168
- if self._reconnect_fail_count >= self._reconnect_alert_threshold:
169
- logger.error(
170
- f"连接失败次数已达阈值({self._reconnect_alert_threshold}),请检查MQ服务状态")
169
+ # 注意:这里需要访问外部的 self,使用闭包或 weakref
170
+ # 简单起见,这里用闭包,但务必在 self.close 或 self.connect 时清理回调
171
+ logger.warning(f"检测到连接关闭: {exc}")
171
172
  if not self._closed:
172
173
  asyncio.create_task(self._safe_reconnect())
173
174
 
@@ -176,20 +177,26 @@ class RabbitMQClient:
176
177
  self._channel_conn.close_callbacks.add(
177
178
  self._conn_close_callback)
178
179
 
179
- # 重建交换机/队列资源
180
+ # 5. 重建资源
180
181
  await self._rebuild_resources()
181
182
 
182
- # 重连成功,重置失败计数器
183
+ # 重置计数
183
184
  self._reconnect_fail_count = 0
184
185
  logger.info("客户端连接初始化完成")
185
186
  except Exception as e:
186
187
  logger.error(f"客户端连接失败: {str(e)}", exc_info=True)
187
- # 清理异常状态
188
- if self._conn_close_callback and self._channel_conn:
189
- self._channel_conn.close_callbacks.discard(
190
- self._conn_close_callback)
188
+ # 失败时也要清理可能产生的残留引用
189
+ if self._channel_conn and self._conn_close_callback:
190
+ try:
191
+ self._channel_conn.close_callbacks.discard(
192
+ self._conn_close_callback)
193
+ except Exception:
194
+ pass
195
+ # 清空状态
191
196
  self._channel = None
192
197
  self._channel_conn = None
198
+ self._conn_close_callback = None
199
+
193
200
  # 触发重连
194
201
  if not self._closed:
195
202
  asyncio.create_task(self._safe_reconnect())