sycommon-python-lib 0.1.55b1__tar.gz → 0.1.56b2__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 (74) hide show
  1. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/PKG-INFO +6 -2
  2. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/pyproject.toml +6 -2
  3. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/config/Config.py +6 -2
  4. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/config/RerankerConfig.py +1 -0
  5. sycommon_python_lib-0.1.56b2/src/sycommon/llm/embedding.py +149 -0
  6. sycommon_python_lib-0.1.56b2/src/sycommon/llm/get_llm.py +177 -0
  7. sycommon_python_lib-0.1.56b2/src/sycommon/llm/llm_logger.py +126 -0
  8. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/logging/kafka_log.py +13 -1
  9. sycommon_python_lib-0.1.56b2/src/sycommon/logging/logger_levels.py +23 -0
  10. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/middleware/context.py +2 -0
  11. sycommon_python_lib-0.1.56b2/src/sycommon/middleware/traceid.py +289 -0
  12. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/services.py +2 -0
  13. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/synacos/feign.py +8 -3
  14. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/synacos/feign_client.py +22 -8
  15. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/synacos/nacos_service.py +13 -4
  16. sycommon_python_lib-0.1.56b2/src/sycommon/tools/__init__.py +0 -0
  17. sycommon_python_lib-0.1.56b2/src/sycommon/tools/merge_headers.py +97 -0
  18. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon_python_lib.egg-info/PKG-INFO +6 -2
  19. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon_python_lib.egg-info/SOURCES.txt +6 -0
  20. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon_python_lib.egg-info/requires.txt +4 -0
  21. sycommon_python_lib-0.1.55b1/src/sycommon/middleware/traceid.py +0 -168
  22. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/README.md +0 -0
  23. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/setup.cfg +0 -0
  24. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/command/cli.py +0 -0
  25. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/__init__.py +0 -0
  26. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/config/DatabaseConfig.py +0 -0
  27. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/config/EmbeddingConfig.py +0 -0
  28. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/config/LLMConfig.py +0 -0
  29. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/config/MQConfig.py +0 -0
  30. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/config/__init__.py +0 -0
  31. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/database/async_base_db_service.py +0 -0
  32. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/database/async_database_service.py +0 -0
  33. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/database/base_db_service.py +0 -0
  34. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/database/database_service.py +0 -0
  35. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/health/__init__.py +0 -0
  36. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/health/health_check.py +0 -0
  37. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/health/metrics.py +0 -0
  38. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/health/ping.py +0 -0
  39. {sycommon_python_lib-0.1.55b1/src/sycommon/logging → sycommon_python_lib-0.1.56b2/src/sycommon/llm}/__init__.py +0 -0
  40. {sycommon_python_lib-0.1.55b1/src/sycommon/middleware → sycommon_python_lib-0.1.56b2/src/sycommon/logging}/__init__.py +0 -0
  41. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/logging/async_sql_logger.py +0 -0
  42. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/logging/logger_wrapper.py +0 -0
  43. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/logging/sql_logger.py +0 -0
  44. {sycommon_python_lib-0.1.55b1/src/sycommon/models → sycommon_python_lib-0.1.56b2/src/sycommon/middleware}/__init__.py +0 -0
  45. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/middleware/cors.py +0 -0
  46. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/middleware/docs.py +0 -0
  47. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/middleware/exception.py +0 -0
  48. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/middleware/middleware.py +0 -0
  49. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/middleware/monitor_memory.py +0 -0
  50. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/middleware/mq.py +0 -0
  51. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/middleware/timeout.py +0 -0
  52. {sycommon_python_lib-0.1.55b1/src/sycommon/sse → sycommon_python_lib-0.1.56b2/src/sycommon/models}/__init__.py +0 -0
  53. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/models/base_http.py +0 -0
  54. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/models/log.py +0 -0
  55. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/models/mqlistener_config.py +0 -0
  56. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/models/mqmsg_model.py +0 -0
  57. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/models/mqsend_config.py +0 -0
  58. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/models/sso_user.py +0 -0
  59. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/rabbitmq/rabbitmq_client.py +0 -0
  60. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/rabbitmq/rabbitmq_pool.py +0 -0
  61. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/rabbitmq/rabbitmq_service.py +0 -0
  62. {sycommon_python_lib-0.1.55b1/src/sycommon/synacos → sycommon_python_lib-0.1.56b2/src/sycommon/sse}/__init__.py +0 -0
  63. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/sse/event.py +0 -0
  64. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/sse/sse.py +0 -0
  65. {sycommon_python_lib-0.1.55b1/src/sycommon/tools → sycommon_python_lib-0.1.56b2/src/sycommon/synacos}/__init__.py +0 -0
  66. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/synacos/example.py +0 -0
  67. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/synacos/example2.py +0 -0
  68. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/synacos/param.py +0 -0
  69. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/tools/docs.py +0 -0
  70. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/tools/snowflake.py +0 -0
  71. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon/tools/timing.py +0 -0
  72. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon_python_lib.egg-info/dependency_links.txt +0 -0
  73. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon_python_lib.egg-info/entry_points.txt +0 -0
  74. {sycommon_python_lib-0.1.55b1 → sycommon_python_lib-0.1.56b2}/src/sycommon_python_lib.egg-info/top_level.txt +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.1.55b1
3
+ Version: 0.1.56b2
4
4
  Summary: Add your description here
5
- Requires-Python: >=3.10
5
+ Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
7
7
  Requires-Dist: aio-pika>=9.5.8
8
8
  Requires-Dist: aiohttp>=3.13.2
@@ -10,6 +10,10 @@ Requires-Dist: aiomysql>=0.3.2
10
10
  Requires-Dist: decorator>=5.2.1
11
11
  Requires-Dist: fastapi>=0.127.0
12
12
  Requires-Dist: kafka-python>=2.3.0
13
+ Requires-Dist: langchain>=1.2.0
14
+ Requires-Dist: langchain-core>=1.2.6
15
+ Requires-Dist: langchain-openai>=1.1.6
16
+ Requires-Dist: langgraph>=1.0.5
13
17
  Requires-Dist: loguru>=0.7.3
14
18
  Requires-Dist: mysql-connector-python>=9.5.0
15
19
  Requires-Dist: nacos-sdk-python<3.0,>=2.0.9
@@ -1,9 +1,9 @@
1
1
  [project]
2
2
  name = "sycommon-python-lib"
3
- version = "0.1.55-beta1"
3
+ version = "0.1.56-beta2"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
- requires-python = ">=3.10"
6
+ requires-python = ">=3.11"
7
7
  dependencies = [
8
8
  "aio-pika>=9.5.8",
9
9
  "aiohttp>=3.13.2",
@@ -11,6 +11,10 @@ dependencies = [
11
11
  "decorator>=5.2.1",
12
12
  "fastapi>=0.127.0",
13
13
  "kafka-python>=2.3.0",
14
+ "langchain>=1.2.0",
15
+ "langchain-core>=1.2.6",
16
+ "langchain-openai>=1.1.6",
17
+ "langgraph>=1.0.5",
14
18
  "loguru>=0.7.3",
15
19
  "mysql-connector-python>=9.5.0",
16
20
  "nacos-sdk-python>=2.0.9,<3.0",
@@ -16,10 +16,9 @@ class Config(metaclass=SingletonMeta):
16
16
  self.config = yaml.safe_load(f)
17
17
  self.MaxBytes = self.config.get('MaxBytes', 209715200)
18
18
  self.Timeout = self.config.get('Timeout', 300000)
19
+ self.MaxRetries = self.config.get('MaxRetries', 3)
19
20
  self.OCR = self.config.get('OCR', None)
20
21
  self.INVOICE_OCR = self.config.get('INVOICE_OCR', None)
21
- self.UnstructuredAPI = self.config.get('UnstructuredAPI', None)
22
- self.MaxRetries = self.config.get('MaxRetries', 3)
23
22
  self.llm_configs = []
24
23
  self.embedding_configs = []
25
24
  self.reranker_configs = []
@@ -71,3 +70,8 @@ class Config(metaclass=SingletonMeta):
71
70
  self.reranker_configs.append(validated_config.model_dump())
72
71
  except ValueError as e:
73
72
  print(f"Invalid LLM configuration: {e}")
73
+
74
+ def set_attr(self, share_configs: dict):
75
+ self.config = {**self.config, **
76
+ share_configs.get('llm', {}), **share_configs}
77
+ self._process_config()
@@ -5,6 +5,7 @@ class RerankerConfig(BaseModel):
5
5
  model: str
6
6
  provider: str
7
7
  baseUrl: str
8
+ maxTokens: int
8
9
 
9
10
  @classmethod
10
11
  def from_config(cls, model_name: str):
@@ -0,0 +1,149 @@
1
+ import asyncio
2
+ import json
3
+ import aiohttp
4
+ from typing import Union, List, Optional
5
+
6
+ from sycommon.config.Config import SingletonMeta
7
+ from sycommon.config.EmbeddingConfig import EmbeddingConfig
8
+ from sycommon.config.RerankerConfig import RerankerConfig
9
+ from sycommon.logging.kafka_log import SYLogger
10
+
11
+
12
+ class Embedding(metaclass=SingletonMeta):
13
+ def __init__(self):
14
+ # 1. 并发限制
15
+ self.max_concurrency = 20
16
+ # 保留默认模型名称
17
+ self.default_embedding_model = "bge-large-zh-v1.5"
18
+ self.default_reranker_model = "bge-reranker-large"
19
+
20
+ # 初始化默认模型的基础URL
21
+ self.embeddings_base_url = EmbeddingConfig.from_config(
22
+ self.default_embedding_model).baseUrl
23
+ self.reranker_base_url = RerankerConfig.from_config(
24
+ self.default_reranker_model).baseUrl
25
+
26
+ # 并发信号量
27
+ self.semaphore = asyncio.Semaphore(self.max_concurrency)
28
+
29
+ async def _get_embeddings_http_async(
30
+ self,
31
+ input: Union[str, List[str]],
32
+ encoding_format: str = None,
33
+ model: str = None,
34
+ **kwargs
35
+ ):
36
+ async with self.semaphore:
37
+ # 优先使用传入的模型名,无则用默认值
38
+ target_model = model or self.default_embedding_model
39
+ target_base_url = EmbeddingConfig.from_config(target_model).baseUrl
40
+ url = f"{target_base_url}/v1/embeddings"
41
+
42
+ request_body = {
43
+ "model": target_model,
44
+ "input": input,
45
+ "encoding_format": encoding_format or "float"
46
+ }
47
+ request_body.update(kwargs)
48
+
49
+ async with aiohttp.ClientSession() as session:
50
+ async with session.post(url, json=request_body) as response:
51
+ if response.status != 200:
52
+ error_detail = await response.text()
53
+ SYLogger.error(
54
+ f"Embedding request failed (model: {target_model}): {error_detail}")
55
+ return None
56
+ return await response.json()
57
+
58
+ async def _get_reranker_http_async(
59
+ self,
60
+ documents: List[str],
61
+ query: str,
62
+ top_n: Optional[int] = None,
63
+ model: str = None,
64
+ max_chunks_per_doc: Optional[int] = None,
65
+ return_documents: Optional[bool] = True,
66
+ return_len: Optional[bool] = True,
67
+ **kwargs
68
+ ):
69
+ async with self.semaphore:
70
+ # 优先使用传入的模型名,无则用默认值
71
+ target_model = model or self.default_reranker_model
72
+ target_base_url = RerankerConfig.from_config(target_model).baseUrl
73
+ url = f"{target_base_url}/v1/rerank"
74
+
75
+ request_body = {
76
+ "model": target_model,
77
+ "documents": documents,
78
+ "query": query,
79
+ "top_n": top_n or len(documents),
80
+ "max_chunks_per_doc": max_chunks_per_doc,
81
+ "return_documents": return_documents,
82
+ "return_len": return_len,
83
+ "kwargs": json.dumps(kwargs),
84
+ }
85
+ request_body.update(kwargs)
86
+
87
+ async with aiohttp.ClientSession() as session:
88
+ async with session.post(url, json=request_body) as response:
89
+ if response.status != 200:
90
+ error_detail = await response.text()
91
+ SYLogger.error(
92
+ f"Rerank request failed (model: {target_model}): {error_detail}")
93
+ return None
94
+ return await response.json()
95
+
96
+ async def get_embeddings(
97
+ self,
98
+ corpus: List[str],
99
+ model: str = None
100
+ ):
101
+ """
102
+ 获取语料库的嵌入向量,结果顺序与输入语料库顺序一致
103
+
104
+ Args:
105
+ corpus: 待生成嵌入向量的文本列表
106
+ model: 可选,指定使用的embedding模型名称,默认使用bge-large-zh-v1.5
107
+ """
108
+ SYLogger.info(
109
+ f"Requesting embeddings for corpus: {corpus} (model: {model or self.default_embedding_model}, max_concurrency: {self.max_concurrency})")
110
+ # 给每个异步任务传入模型名称
111
+ tasks = [self._get_embeddings_http_async(
112
+ text, model=model) for text in corpus]
113
+ results = await asyncio.gather(*tasks)
114
+
115
+ vectors = []
116
+ for result in results:
117
+ if result is None:
118
+ zero_vector = [0.0] * 1024
119
+ vectors.append(zero_vector)
120
+ SYLogger.warning(
121
+ f"Embedding request failed, append zero vector (1024D)")
122
+ continue
123
+ for item in result["data"]:
124
+ vectors.append(item["embedding"])
125
+
126
+ SYLogger.info(
127
+ f"Embeddings for corpus: {corpus} created (model: {model or self.default_embedding_model})")
128
+ return vectors
129
+
130
+ async def get_reranker(
131
+ self,
132
+ top_results: List[str],
133
+ query: str,
134
+ model: str = None
135
+ ):
136
+ """
137
+ 对搜索结果进行重排序
138
+
139
+ Args:
140
+ top_results: 待重排序的文本列表
141
+ query: 排序参考的查询语句
142
+ model: 可选,指定使用的reranker模型名称,默认使用bge-reranker-large
143
+ """
144
+ SYLogger.info(
145
+ f"Requesting reranker for top_results: {top_results} (model: {model or self.default_reranker_model}, max_concurrency: {self.max_concurrency})")
146
+ data = await self._get_reranker_http_async(top_results, query, model=model)
147
+ SYLogger.info(
148
+ f"Reranker for top_results: {top_results} completed (model: {model or self.default_reranker_model})")
149
+ return data
@@ -0,0 +1,177 @@
1
+ from typing import Dict, Type, List, Union, Optional, Callable
2
+
3
+ from sycommon.llm.llm_logger import LLMLogger
4
+ from langchain_core.language_models import BaseChatModel
5
+ from langchain_core.runnables import Runnable, RunnableLambda
6
+ from langchain_core.output_parsers import PydanticOutputParser
7
+ from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage
8
+ from langchain.chat_models import init_chat_model
9
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
10
+ from pydantic import BaseModel, ValidationError
11
+ from sycommon.config.LLMConfig import LLMConfig
12
+
13
+
14
+ def get_llm(model: str = None, streaming: bool = False) -> BaseChatModel:
15
+ if not model:
16
+ model = "Qwen2.5-72B"
17
+ # model = "SyMid"
18
+ llmConfig = LLMConfig.from_config(model)
19
+ llm = None
20
+ if llmConfig:
21
+ llm = init_chat_model(
22
+ model_provider=llmConfig.provider,
23
+ model=llmConfig.model,
24
+ base_url=llmConfig.baseUrl,
25
+ api_key="-",
26
+ temperature=0.1,
27
+ streaming=streaming,
28
+ )
29
+ else:
30
+ raise Exception("Invalid model")
31
+
32
+ # 为LLM动态添加with_structured_output方法,官方的with_structured_output方法有概率在qwen2.5中导致模型卡死不返回数据,2.5对functioncall支持不好
33
+ def with_structured_output(
34
+ self: BaseChatModel,
35
+ output_model: Type[BaseModel],
36
+ max_retries: int = 3,
37
+ is_extract: bool = False,
38
+ override_prompt: ChatPromptTemplate = None,
39
+ # 自定义处理函数列表(每个函数接收str,返回str)
40
+ custom_processors: Optional[List[Callable[[str], str]]] = None,
41
+ # 自定义解析函数(接收str,返回BaseModel)
42
+ custom_parser: Optional[Callable[[str], BaseModel]] = None
43
+ ) -> Runnable[List[BaseMessage], BaseModel]:
44
+ parser = PydanticOutputParser(pydantic_object=output_model)
45
+
46
+ accuracy_instructions = """
47
+ 字段值的抽取准确率(0~1之间),评分规则:
48
+ 1.0(完全准确):直接从原文提取,无需任何加工,且格式与原文完全一致
49
+ 0.9(轻微处理):数据来源明确,但需进行格式标准化或冗余信息剔除(不改变原始数值)
50
+ 0.8(有限推断):数据需通过上下文关联或简单计算得出,仍有明确依据
51
+ 0.8以下(不可靠):数据需大量推测、存在歧义或来源不明,处理方式:直接忽略该数据,设置为None
52
+ """
53
+
54
+ if is_extract:
55
+ # 抽取模式下使用固定的抽取专用prompt
56
+ prompt = ChatPromptTemplate.from_messages([
57
+ MessagesPlaceholder(variable_name="messages"),
58
+ HumanMessage(content=f"""
59
+ 请提取信息并遵循以下规则:
60
+ 1. 准确率要求:{accuracy_instructions.strip()}
61
+ 2. 输出格式:{parser.get_format_instructions()}
62
+ """)
63
+ ])
64
+ else:
65
+ if override_prompt:
66
+ prompt = override_prompt
67
+ else:
68
+ prompt = ChatPromptTemplate.from_messages([
69
+ MessagesPlaceholder(variable_name="messages"),
70
+ HumanMessage(content=f"""
71
+ 输出格式:{parser.get_format_instructions()}
72
+ """)
73
+ ])
74
+
75
+ # ========== 基础处理函数 ==========
76
+ def extract_response_content(response: BaseMessage) -> str:
77
+ """提取响应中的文本内容"""
78
+ try:
79
+ return response.content
80
+ except Exception as e:
81
+ raise ValueError(f"提取响应内容失败:{str(e)}") from e
82
+
83
+ def strip_code_block_markers(content: str) -> str:
84
+ """移除JSON代码块标记(```json/```)"""
85
+ try:
86
+ return content.strip("```json").strip("```").strip()
87
+ except Exception as e:
88
+ raise ValueError(
89
+ f"移除代码块标记失败(内容:{str(content)[:100]}):{str(e)}") from e
90
+
91
+ def normalize_in_json(content: str) -> str:
92
+ """将None替换为null,确保JSON格式合法"""
93
+ try:
94
+ cleaned = content.replace("None", "null")
95
+ cleaned = cleaned.replace("none", "null")
96
+ cleaned = cleaned.replace("NONE", "null")
97
+ cleaned = cleaned.replace("''", '""')
98
+ return cleaned
99
+ except Exception as e:
100
+ raise ValueError(
101
+ f"替换None为null失败(内容:{str(content)[:100]}):{str(e)}") from e
102
+
103
+ def default_parse_to_pydantic(content: str) -> BaseModel:
104
+ """默认解析函数:将处理后的文本解析为Pydantic模型"""
105
+ try:
106
+ return parser.parse(content)
107
+ except (ValidationError, ValueError) as e:
108
+ raise type(e)(f"解析失败(原始内容:{content[:200]}):{str(e)}") from e
109
+
110
+ # ========== 构建处理链条 ==========
111
+ # 基础链 prompt → LLM → 提取响应内容
112
+ base_chain = (
113
+ prompt
114
+ | self
115
+ | RunnableLambda(extract_response_content)
116
+ )
117
+
118
+ # 处理函数链 优先使用自定义,否则用默认
119
+ if custom_processors:
120
+ # 自定义处理函数 → 转为RunnableLambda列表
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
+ ]
129
+
130
+ # 拼接处理链
131
+ process_chain = base_chain
132
+ for runnable in process_runnables:
133
+ process_chain = process_chain | runnable
134
+
135
+ # 解析函数 优先使用自定义,否则用默认
136
+ parse_func = custom_parser if custom_parser else default_parse_to_pydantic
137
+ parse_chain = process_chain | RunnableLambda(parse_func)
138
+
139
+ retry_chain = parse_chain.with_retry(
140
+ retry_if_exception_type=(ValidationError, ValueError),
141
+ stop_after_attempt=max_retries,
142
+ wait_exponential_jitter=True,
143
+ exponential_jitter_params={
144
+ "initial": 0.1, # 初始等待时间(秒)
145
+ "max": 3.0, # 最大等待时间(秒)
146
+ "exp_base": 2.0, # 指数基数(默认2)
147
+ "jitter": 1.0 # 随机抖动值(默认1)
148
+ }
149
+ )
150
+
151
+ class StructuredRunnable(Runnable[Union[List[BaseMessage], BaseMessage, str, Dict[str, str]], BaseModel]):
152
+ def _adapt_input(self, input: Union[List[BaseMessage], BaseMessage, str, Dict[str, str]]) -> List[BaseMessage]:
153
+ """将多种输入格式统一转换为 List[BaseMessage]"""
154
+ if isinstance(input, list) and all(isinstance(x, BaseMessage) for x in input):
155
+ return input
156
+ elif isinstance(input, BaseMessage):
157
+ return [input]
158
+ elif isinstance(input, str):
159
+ return [HumanMessage(content=input)]
160
+ elif isinstance(input, dict) and "input" in input:
161
+ return [HumanMessage(content=str(input["input"]))]
162
+ else:
163
+ raise ValueError(
164
+ "不支持的输入格式,请使用消息列表、单条消息、文本或 {'input': '文本'}")
165
+
166
+ def invoke(self, input: Union[List[BaseMessage], BaseMessage, str, Dict[str, str]], config={"callbacks": [LLMLogger()]}):
167
+ adapted_input = self._adapt_input(input)
168
+ return retry_chain.invoke({"messages": adapted_input}, config=config)
169
+
170
+ async def ainvoke(self, input: Union[List[BaseMessage], BaseMessage, str, Dict[str, str]], config={"callbacks": [LLMLogger()]}):
171
+ adapted_input = self._adapt_input(input)
172
+ return await retry_chain.ainvoke({"messages": adapted_input}, config=config)
173
+
174
+ return StructuredRunnable()
175
+
176
+ llm.__class__.with_structured_output = with_structured_output
177
+ return llm
@@ -0,0 +1,126 @@
1
+ from langchain_core.callbacks import AsyncCallbackHandler
2
+ from typing import Any, Dict, List
3
+ from langchain_core.outputs import GenerationChunk, ChatGeneration
4
+ from langchain_core.messages import BaseMessage
5
+
6
+ from sycommon.logging.kafka_log import SYLogger
7
+
8
+
9
+ class LLMLogger(AsyncCallbackHandler):
10
+ """
11
+ 通用LLM日志回调处理器,同时支持:
12
+ - 同步调用(如 chain.invoke())
13
+ - 异步调用(如 chain.astream())
14
+ - 聊天模型调用
15
+ """
16
+
17
+ # ------------------------------
18
+ # 同步回调方法(处理 invoke 等同步调用)
19
+ # ------------------------------
20
+ def on_llm_start(self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any) -> None:
21
+ model_name = serialized.get('name', 'unknown')
22
+ SYLogger.info(
23
+ f"[同步] LLM调用开始 | 模型: {model_name} | 提示词数: {len(prompts)}")
24
+ self._log_prompts(prompts)
25
+
26
+ def on_chat_model_start(
27
+ self,
28
+ serialized: Dict[str, Any],
29
+ messages: List[List[BaseMessage]],
30
+ **kwargs: Any
31
+ ) -> None:
32
+ model_name = serialized.get('name', 'unknown')
33
+ SYLogger.info(
34
+ f"[同步] 聊天模型调用开始 | 模型: {model_name} | 消息组数: {len(messages)}")
35
+ self._log_chat_messages(messages)
36
+
37
+ def on_llm_end(self, response: Any, **kwargs: Any) -> None:
38
+ # 处理普通LLM结果
39
+ if hasattr(response, 'generations') and all(
40
+ isinstance(gen[0], GenerationChunk) for gen in response.generations
41
+ ):
42
+ for i, generation in enumerate(response.generations):
43
+ result = generation[0].text
44
+ SYLogger.info(
45
+ f"[同步] LLM调用结束 | 结果 #{i+1} 长度: {len(result)}")
46
+ self._log_result(result, i+1)
47
+ # 处理聊天模型结果
48
+ elif hasattr(response, 'generations') and all(
49
+ isinstance(gen[0], ChatGeneration) for gen in response.generations
50
+ ):
51
+ for i, generation in enumerate(response.generations):
52
+ result = generation[0].message.content
53
+ SYLogger.info(
54
+ f"[同步] 聊天模型调用结束 | 结果 #{i+1} 长度: {len(result)}")
55
+ self._log_result(result, i+1)
56
+
57
+ def on_llm_error(self, error: Exception, **kwargs: Any) -> None:
58
+ if isinstance(error, GeneratorExit):
59
+ SYLogger.info("[同步] LLM生成器正常关闭")
60
+ return
61
+ SYLogger.error(f"[同步] LLM调用出错: {str(error)}")
62
+
63
+ # ------------------------------
64
+ # 异步回调方法(处理 astream 等异步调用)
65
+ # ------------------------------
66
+ async def on_llm_start(self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any) -> None:
67
+ model_name = serialized.get('name', 'unknown')
68
+ SYLogger.info(
69
+ f"[异步] LLM调用开始 | 模型: {model_name} | 提示词数: {len(prompts)}")
70
+ self._log_prompts(prompts)
71
+
72
+ async def on_chat_model_start(
73
+ self,
74
+ serialized: Dict[str, Any],
75
+ messages: List[List[BaseMessage]],
76
+ **kwargs: Any
77
+ ) -> None:
78
+ model_name = serialized.get('name', 'unknown')
79
+ SYLogger.info(
80
+ f"[异步] 聊天模型调用开始 | 模型: {model_name} | 消息组数: {len(messages)}")
81
+ self._log_chat_messages(messages)
82
+
83
+ async def on_llm_end(self, response: Any, **kwargs: Any) -> None:
84
+ # 处理普通LLM结果
85
+ if hasattr(response, 'generations') and all(
86
+ isinstance(gen[0], GenerationChunk) for gen in response.generations
87
+ ):
88
+ for i, generation in enumerate(response.generations):
89
+ result = generation[0].text
90
+ SYLogger.info(
91
+ f"[异步] LLM调用结束 | 结果 #{i+1} 长度: {len(result)}")
92
+ self._log_result(result, i+1)
93
+ # 处理聊天模型结果
94
+ elif hasattr(response, 'generations') and all(
95
+ isinstance(gen[0], ChatGeneration) for gen in response.generations
96
+ ):
97
+ for i, generation in enumerate(response.generations):
98
+ result = generation[0].message.content
99
+ SYLogger.info(
100
+ f"[异步] 聊天模型调用结束 | 结果 #{i+1} 长度: {len(result)}")
101
+ self._log_result(result, i+1)
102
+
103
+ async def on_llm_error(self, error: Exception, **kwargs: Any) -> None:
104
+ if isinstance(error, GeneratorExit):
105
+ SYLogger.info("[异步] LLM生成器正常关闭")
106
+ return
107
+ SYLogger.error(f"[异步] LLM调用出错: {str(error)}")
108
+
109
+ # ------------------------------
110
+ # 共享工具方法(避免代码重复)
111
+ # ------------------------------
112
+ def _log_prompts(self, prompts: List[str]) -> None:
113
+ """记录提示词"""
114
+ for i, prompt in enumerate(prompts):
115
+ SYLogger.info(f"提示词 #{i+1}:\n{prompt}")
116
+
117
+ def _log_chat_messages(self, messages: List[List[BaseMessage]]) -> None:
118
+ """记录聊天模型的消息"""
119
+ for i, message_group in enumerate(messages):
120
+ SYLogger.info(f"消息组 #{i+1}:")
121
+ for msg in message_group:
122
+ SYLogger.info(f" {msg.type}: {msg.content}")
123
+
124
+ def _log_result(self, result: str, index: int) -> None:
125
+ """记录结果"""
126
+ SYLogger.info(f"结果 #{index}:\n{result}")
@@ -15,7 +15,7 @@ from kafka import KafkaProducer
15
15
  from loguru import logger
16
16
  import loguru
17
17
  from sycommon.config.Config import Config, SingletonMeta
18
- from sycommon.middleware.context import current_trace_id
18
+ from sycommon.middleware.context import current_trace_id, current_headers
19
19
  from sycommon.tools.snowflake import Snowflake
20
20
 
21
21
  # 配置Loguru的颜色方案
@@ -441,6 +441,18 @@ class SYLogger:
441
441
  """重置当前的 trace_id"""
442
442
  current_trace_id.reset(token)
443
443
 
444
+ @staticmethod
445
+ def get_headers():
446
+ return current_headers.get()
447
+
448
+ @staticmethod
449
+ def set_headers(headers: list[tuple[str, str]]):
450
+ return current_headers.set(headers)
451
+
452
+ @staticmethod
453
+ def reset_headers(token):
454
+ current_headers.reset(token)
455
+
444
456
  @staticmethod
445
457
  def _get_execution_context() -> str:
446
458
  """获取当前执行上下文的线程或协程信息,返回格式化字符串"""
@@ -0,0 +1,23 @@
1
+ import logging
2
+
3
+
4
+ def setup_logger_levels():
5
+ """配置各模块的日志级别,抑制无关INFO/DEBUG日志"""
6
+ # Nacos 客户端:仅输出WARNING及以上(屏蔽INFO级的心跳/注册日志)
7
+ logging.getLogger("nacos.client").setLevel(logging.WARNING)
8
+
9
+ # Kafka Python客户端:屏蔽INFO级的连接/版本检测日志
10
+ logging.getLogger("kafka.conn").setLevel(logging.WARNING)
11
+ logging.getLogger("kafka.producer").setLevel(logging.WARNING)
12
+
13
+ # Uvicorn/FastAPI:屏蔽启动/应用初始化的INFO日志(保留ERROR/WARNING)
14
+ # logging.getLogger("uvicorn").setLevel(logging.WARNING)
15
+ # logging.getLogger("uvicorn.access").setLevel(logging.WARNING) # 屏蔽访问日志
16
+ # logging.getLogger("uvicorn.error").setLevel(logging.ERROR) # 仅保留错误
17
+
18
+ # 自定义的root日志(如同步数据库/监听器初始化):屏蔽INFO
19
+ logging.getLogger("root").setLevel(logging.WARNING)
20
+
21
+ # RabbitMQ相关日志(如果有专属日志器)
22
+ logging.getLogger("pika").setLevel(logging.WARNING) # 若使用pika客户端
23
+ logging.getLogger("rabbitmq").setLevel(logging.WARNING)
@@ -1,3 +1,5 @@
1
1
  import contextvars
2
2
 
3
3
  current_trace_id = contextvars.ContextVar("trace_id", default=None)
4
+
5
+ current_headers = contextvars.ContextVar("headers", default=None)