sycommon-python-lib 0.1.56b5__py3-none-any.whl → 0.1.57b4__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.
Files changed (38) hide show
  1. sycommon/config/Config.py +24 -3
  2. sycommon/config/LangfuseConfig.py +15 -0
  3. sycommon/config/SentryConfig.py +13 -0
  4. sycommon/llm/embedding.py +269 -50
  5. sycommon/llm/get_llm.py +9 -218
  6. sycommon/llm/struct_token.py +192 -0
  7. sycommon/llm/sy_langfuse.py +103 -0
  8. sycommon/llm/usage_token.py +117 -0
  9. sycommon/logging/kafka_log.py +187 -433
  10. sycommon/middleware/exception.py +10 -16
  11. sycommon/middleware/timeout.py +2 -1
  12. sycommon/middleware/traceid.py +81 -76
  13. sycommon/notice/uvicorn_monitor.py +32 -27
  14. sycommon/rabbitmq/rabbitmq_client.py +247 -242
  15. sycommon/rabbitmq/rabbitmq_pool.py +201 -123
  16. sycommon/rabbitmq/rabbitmq_service.py +25 -843
  17. sycommon/rabbitmq/rabbitmq_service_client_manager.py +211 -0
  18. sycommon/rabbitmq/rabbitmq_service_connection_monitor.py +73 -0
  19. sycommon/rabbitmq/rabbitmq_service_consumer_manager.py +285 -0
  20. sycommon/rabbitmq/rabbitmq_service_core.py +117 -0
  21. sycommon/rabbitmq/rabbitmq_service_producer_manager.py +238 -0
  22. sycommon/sentry/__init__.py +0 -0
  23. sycommon/sentry/sy_sentry.py +35 -0
  24. sycommon/services.py +122 -96
  25. sycommon/synacos/nacos_client_base.py +121 -0
  26. sycommon/synacos/nacos_config_manager.py +107 -0
  27. sycommon/synacos/nacos_heartbeat_manager.py +144 -0
  28. sycommon/synacos/nacos_service.py +63 -783
  29. sycommon/synacos/nacos_service_discovery.py +157 -0
  30. sycommon/synacos/nacos_service_registration.py +270 -0
  31. sycommon/tools/env.py +62 -0
  32. sycommon/tools/merge_headers.py +20 -0
  33. sycommon/tools/snowflake.py +101 -153
  34. {sycommon_python_lib-0.1.56b5.dist-info → sycommon_python_lib-0.1.57b4.dist-info}/METADATA +10 -8
  35. {sycommon_python_lib-0.1.56b5.dist-info → sycommon_python_lib-0.1.57b4.dist-info}/RECORD +38 -20
  36. {sycommon_python_lib-0.1.56b5.dist-info → sycommon_python_lib-0.1.57b4.dist-info}/WHEEL +0 -0
  37. {sycommon_python_lib-0.1.56b5.dist-info → sycommon_python_lib-0.1.57b4.dist-info}/entry_points.txt +0 -0
  38. {sycommon_python_lib-0.1.56b5.dist-info → sycommon_python_lib-0.1.57b4.dist-info}/top_level.txt +0 -0
sycommon/llm/get_llm.py CHANGED
@@ -1,222 +1,8 @@
1
- from typing import Dict, Type, List, Optional, Callable, Any
2
1
  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
2
  from langchain.chat_models import init_chat_model
8
- from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
9
- from pydantic import BaseModel, ValidationError, Field
10
3
  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
4
+ from sycommon.llm.sy_langfuse import LangfuseInitializer
5
+ from sycommon.llm.usage_token import LLMWithAutoTokenUsage
220
6
 
221
7
 
222
8
  def get_llm(
@@ -230,6 +16,11 @@ def get_llm(
230
16
  if not llmConfig:
231
17
  raise Exception(f"无效的模型配置:{model}")
232
18
 
19
+ # 初始化Langfuse
20
+ langfuse_callbacks, langfuse = LangfuseInitializer.get()
21
+
22
+ callbacks = [LLMLogger()] + langfuse_callbacks
23
+
233
24
  llm = init_chat_model(
234
25
  model_provider=llmConfig.provider,
235
26
  model=llmConfig.model,
@@ -237,10 +28,10 @@ def get_llm(
237
28
  api_key="-",
238
29
  temperature=0.1,
239
30
  streaming=streaming,
240
- callbacks=[LLMLogger()]
31
+ callbacks=callbacks
241
32
  )
242
33
 
243
34
  if llm is None:
244
35
  raise Exception(f"初始化原始LLM实例失败:{model}")
245
36
 
246
- return LLMWithAutoTokenUsage(llm)
37
+ return LLMWithAutoTokenUsage(llm, langfuse)
@@ -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