sycommon-python-lib 0.1.57b6__tar.gz → 0.1.57b8__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 (94) hide show
  1. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/PKG-INFO +2 -1
  2. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/pyproject.toml +2 -1
  3. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/llm/embedding.py +8 -3
  4. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/llm/get_llm.py +23 -13
  5. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/llm/struct_token.py +115 -4
  6. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/llm/usage_token.py +7 -3
  7. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/rabbitmq/rabbitmq_client.py +68 -98
  8. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/rabbitmq/rabbitmq_pool.py +27 -20
  9. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon_python_lib.egg-info/PKG-INFO +2 -1
  10. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon_python_lib.egg-info/requires.txt +1 -0
  11. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/README.md +0 -0
  12. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/setup.cfg +0 -0
  13. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/command/cli.py +0 -0
  14. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/__init__.py +0 -0
  15. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/config/Config.py +0 -0
  16. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/config/DatabaseConfig.py +0 -0
  17. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/config/EmbeddingConfig.py +0 -0
  18. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/config/LLMConfig.py +0 -0
  19. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/config/LangfuseConfig.py +0 -0
  20. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/config/MQConfig.py +0 -0
  21. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/config/RerankerConfig.py +0 -0
  22. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/config/SentryConfig.py +0 -0
  23. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/config/__init__.py +0 -0
  24. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/database/async_base_db_service.py +0 -0
  25. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/database/async_database_service.py +0 -0
  26. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/database/base_db_service.py +0 -0
  27. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/database/database_service.py +0 -0
  28. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/health/__init__.py +0 -0
  29. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/health/health_check.py +0 -0
  30. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/health/metrics.py +0 -0
  31. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/health/ping.py +0 -0
  32. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/llm/__init__.py +0 -0
  33. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/llm/llm_logger.py +0 -0
  34. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/llm/llm_tokens.py +0 -0
  35. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/llm/sy_langfuse.py +0 -0
  36. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/logging/__init__.py +0 -0
  37. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/logging/async_sql_logger.py +0 -0
  38. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/logging/kafka_log.py +0 -0
  39. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/logging/logger_levels.py +0 -0
  40. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/logging/logger_wrapper.py +0 -0
  41. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/logging/sql_logger.py +0 -0
  42. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/middleware/__init__.py +0 -0
  43. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/middleware/context.py +0 -0
  44. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/middleware/cors.py +0 -0
  45. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/middleware/docs.py +0 -0
  46. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/middleware/exception.py +0 -0
  47. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/middleware/middleware.py +0 -0
  48. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/middleware/monitor_memory.py +0 -0
  49. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/middleware/mq.py +0 -0
  50. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/middleware/timeout.py +0 -0
  51. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/middleware/traceid.py +0 -0
  52. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/models/__init__.py +0 -0
  53. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/models/base_http.py +0 -0
  54. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/models/log.py +0 -0
  55. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/models/mqlistener_config.py +0 -0
  56. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/models/mqmsg_model.py +0 -0
  57. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/models/mqsend_config.py +0 -0
  58. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/models/sso_user.py +0 -0
  59. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/notice/__init__.py +0 -0
  60. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/notice/uvicorn_monitor.py +0 -0
  61. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/rabbitmq/rabbitmq_service.py +0 -0
  62. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/rabbitmq/rabbitmq_service_client_manager.py +0 -0
  63. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/rabbitmq/rabbitmq_service_connection_monitor.py +0 -0
  64. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/rabbitmq/rabbitmq_service_consumer_manager.py +0 -0
  65. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/rabbitmq/rabbitmq_service_core.py +0 -0
  66. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/rabbitmq/rabbitmq_service_producer_manager.py +0 -0
  67. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/sentry/__init__.py +0 -0
  68. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/sentry/sy_sentry.py +0 -0
  69. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/services.py +0 -0
  70. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/sse/__init__.py +0 -0
  71. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/sse/event.py +0 -0
  72. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/sse/sse.py +0 -0
  73. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/synacos/__init__.py +0 -0
  74. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/synacos/example.py +0 -0
  75. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/synacos/example2.py +0 -0
  76. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/synacos/feign.py +0 -0
  77. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/synacos/feign_client.py +0 -0
  78. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/synacos/nacos_client_base.py +0 -0
  79. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/synacos/nacos_config_manager.py +0 -0
  80. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/synacos/nacos_heartbeat_manager.py +0 -0
  81. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/synacos/nacos_service.py +0 -0
  82. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/synacos/nacos_service_discovery.py +0 -0
  83. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/synacos/nacos_service_registration.py +0 -0
  84. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/synacos/param.py +0 -0
  85. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/tools/__init__.py +0 -0
  86. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/tools/docs.py +0 -0
  87. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/tools/env.py +0 -0
  88. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/tools/merge_headers.py +0 -0
  89. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/tools/snowflake.py +0 -0
  90. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon/tools/timing.py +0 -0
  91. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon_python_lib.egg-info/SOURCES.txt +0 -0
  92. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon_python_lib.egg-info/dependency_links.txt +0 -0
  93. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/src/sycommon_python_lib.egg-info/entry_points.txt +0 -0
  94. {sycommon_python_lib-0.1.57b6 → sycommon_python_lib-0.1.57b8}/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.57b6
3
+ Version: 0.1.57b8
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -25,6 +25,7 @@ Requires-Dist: pyyaml>=6.0.3
25
25
  Requires-Dist: sentry-sdk[fastapi]>=2.49.0
26
26
  Requires-Dist: sqlalchemy[asyncio]>=2.0.45
27
27
  Requires-Dist: starlette>=0.50.0
28
+ Requires-Dist: tiktoken>=0.12.0
28
29
  Requires-Dist: uvicorn>=0.40.0
29
30
 
30
31
  # sycommon-python-lib
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sycommon-python-lib"
3
- version = "0.1.57b6"
3
+ version = "0.1.57b8"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -26,6 +26,7 @@ dependencies = [
26
26
  "sentry-sdk[fastapi]>=2.49.0",
27
27
  "sqlalchemy[asyncio]>=2.0.45",
28
28
  "starlette>=0.50.0",
29
+ "tiktoken>=0.12.0",
29
30
  "uvicorn>=0.40.0",
30
31
  ]
31
32
 
@@ -286,9 +286,12 @@ class Embedding(metaclass=SingletonMeta):
286
286
  for i in range(0, len(corpus), batch_size):
287
287
  batch_texts = corpus[i: i + batch_size]
288
288
 
289
+ SYLogger.info(
290
+ f"Requesting embeddings for text: {len(batch_texts)} items (model: {actual_model}, timeout: {timeout or 'None'})")
291
+
289
292
  # 给每个异步任务传入模型名称和超时配置
290
293
  tasks = [self._get_embeddings_http_async(
291
- text, model=model, timeout=request_timeout) for text in batch_texts]
294
+ text, model=actual_model, timeout=request_timeout) for text in batch_texts]
292
295
  results = await asyncio.gather(*tasks)
293
296
 
294
297
  for result in results:
@@ -345,9 +348,11 @@ class Embedding(metaclass=SingletonMeta):
345
348
  actual_model = model or self.default_reranker_model
346
349
  SYLogger.info(
347
350
  f"Requesting reranker for top_results: {top_results} (model: {actual_model}, max_concurrency: {self.max_concurrency}, timeout: {timeout or 'None'})")
348
-
351
+ # 打印请求参数
352
+ SYLogger.info(
353
+ f"Requesting reranker for top_results: {top_results} (model: {actual_model}) (query: {query}) (timeout: {timeout or 'None'})")
349
354
  data = await self._get_reranker_http_async(
350
- top_results, query, model=model, timeout=request_timeout)
355
+ top_results, query, model=actual_model, timeout=request_timeout)
351
356
  SYLogger.info(
352
357
  f"Reranker for top_results completed (model: {actual_model})")
353
358
  return data
@@ -3,11 +3,15 @@ from langchain.chat_models import init_chat_model
3
3
  from sycommon.config.LLMConfig import LLMConfig
4
4
  from sycommon.llm.sy_langfuse import LangfuseInitializer
5
5
  from sycommon.llm.usage_token import LLMWithAutoTokenUsage
6
+ from typing import Any
6
7
 
7
8
 
8
9
  def get_llm(
9
10
  model: str = None,
10
- streaming: bool = False
11
+ *,
12
+ streaming: bool = False,
13
+ temperature: float = 0.1,
14
+ **kwargs: Any
11
15
  ) -> LLMWithAutoTokenUsage:
12
16
  if not model:
13
17
  model = "Qwen2.5-72B"
@@ -16,22 +20,28 @@ def get_llm(
16
20
  if not llmConfig:
17
21
  raise Exception(f"无效的模型配置:{model}")
18
22
 
19
- # 初始化Langfuse
23
+ # 初始化 Langfuse
20
24
  langfuse_callbacks, langfuse = LangfuseInitializer.get()
21
-
22
25
  callbacks = [LLMLogger()] + langfuse_callbacks
23
26
 
24
- llm = init_chat_model(
25
- model_provider=llmConfig.provider,
26
- model=llmConfig.model,
27
- base_url=llmConfig.baseUrl,
28
- api_key="-",
29
- temperature=0.1,
30
- streaming=streaming,
31
- callbacks=callbacks
32
- )
27
+ init_params = {
28
+ "model_provider": llmConfig.provider,
29
+ "model": llmConfig.model,
30
+ "base_url": llmConfig.baseUrl,
31
+ "api_key": "-",
32
+ "callbacks": callbacks,
33
+ "temperature": temperature,
34
+ "streaming": streaming,
35
+ }
36
+
37
+ init_params.update(kwargs)
38
+
39
+ llm = init_chat_model(**init_params)
33
40
 
34
41
  if llm is None:
35
42
  raise Exception(f"初始化原始LLM实例失败:{model}")
36
43
 
37
- return LLMWithAutoTokenUsage(llm, langfuse)
44
+ # 获取kwargs中summary_prompt参数
45
+ summary_prompt = kwargs.get("summary_prompt")
46
+
47
+ return LLMWithAutoTokenUsage(llm, langfuse, llmConfig, summary_prompt)
@@ -1,21 +1,111 @@
1
+ import tiktoken
1
2
  from typing import Dict, List, Optional, Any
2
3
  from langfuse import Langfuse, LangfuseSpan, propagate_attributes
3
4
  from sycommon.llm.llm_logger import LLMLogger
4
5
  from langchain_core.runnables import Runnable, RunnableConfig
5
- from langchain_core.messages import BaseMessage, HumanMessage
6
+ from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage
6
7
  from sycommon.llm.llm_tokens import TokensCallbackHandler
7
8
  from sycommon.logging.kafka_log import SYLogger
9
+ from sycommon.config.LLMConfig import LLMConfig
8
10
  from sycommon.tools.env import get_env_var
9
11
  from sycommon.tools.merge_headers import get_header_value
10
12
 
11
13
 
12
14
  class StructuredRunnableWithToken(Runnable):
13
- """带Token统计的Runnable类"""
15
+ """
16
+ 统一功能 Runnable:Trace追踪 + Token统计 + 自动上下文压缩
17
+ """
14
18
 
15
- def __init__(self, retry_chain: Runnable, langfuse: Optional[Langfuse]):
19
+ def __init__(
20
+ self,
21
+ retry_chain: Runnable,
22
+ langfuse: Optional[Langfuse] = None,
23
+ llmConfig: Optional[LLMConfig] = None,
24
+ summary_prompt: Optional[str] = None,
25
+ model_name: str = "Qwen2.5-72B",
26
+ enable_compression: bool = True,
27
+ threshold_ratio: float = 0.8
28
+ ):
16
29
  super().__init__()
17
30
  self.retry_chain = retry_chain
18
31
  self.langfuse = langfuse
32
+ self.llmConfig = llmConfig
33
+ self.summary_prompt = summary_prompt
34
+ self.model_name = model_name
35
+ self.enable_compression = enable_compression
36
+ self.threshold_ratio = threshold_ratio
37
+
38
+ # 初始化 Tokenizer
39
+ try:
40
+ self.encoding = tiktoken.encoding_for_model(model_name)
41
+ except KeyError:
42
+ self.encoding = tiktoken.get_encoding("cl100k_base")
43
+
44
+ def _count_tokens(self, messages: List[BaseMessage]) -> int:
45
+ """快速估算 Token 数量"""
46
+ num_tokens = 0
47
+ for message in messages:
48
+ num_tokens += 4 # 每条消息的固定开销
49
+ # 兼容 content 是字符串或者 dict 的情况
50
+ content = message.content
51
+ if isinstance(content, str):
52
+ num_tokens += len(self.encoding.encode(content))
53
+ elif isinstance(content, list): # 多模态或复杂结构
54
+ for item in content:
55
+ if isinstance(item, dict) and "text" in item:
56
+ num_tokens += len(self.encoding.encode(item["text"]))
57
+ elif isinstance(content, dict):
58
+ num_tokens += len(self.encoding.encode(str(content)))
59
+ return num_tokens
60
+
61
+ async def _acompress_context(self, messages: List[BaseMessage]) -> List[BaseMessage]:
62
+ """执行异步上下文压缩"""
63
+ # 策略:保留 System Prompt + 最近 N 条,中间的摘要
64
+ keep_last_n = 1
65
+
66
+ # 分离系统消息和对话消息
67
+ system_msgs = [m for m in messages if isinstance(m, SystemMessage)]
68
+ conversation = [
69
+ m for m in messages if not isinstance(m, SystemMessage)]
70
+
71
+ if len(conversation) <= keep_last_n:
72
+ return messages
73
+
74
+ to_summarize = conversation[:-keep_last_n]
75
+ keep_recent = conversation[-keep_last_n:]
76
+
77
+ # 构造摘要 Prompt
78
+ # 注意:这里直接使用 retry_chain 进行摘要,防止死循环
79
+ summary_content = self.summary_prompt or "请将上下文内容进行摘要,保留关键信息,将内容压缩到原来长度的50%左右,保留关键信息。"
80
+ summary_prompt = [
81
+ SystemMessage(content=summary_content),
82
+ HumanMessage(content=f"历史记录:\n{to_summarize}\n\n摘要:")
83
+ ]
84
+
85
+ try:
86
+ SYLogger.info(
87
+ f"🚀 Triggering compression: {len(to_summarize)} messages -> summary")
88
+
89
+ # 调用子链生成摘要
90
+ # 【关键】必须清空 callbacks,否则 Langfuse 会递归追踪,导致死循环或噪音
91
+ summary_result = await self.retry_chain.ainvoke(
92
+ {"messages": summary_prompt},
93
+ config=RunnableConfig(callbacks=[])
94
+ )
95
+
96
+ summary_text = summary_result.content if hasattr(
97
+ summary_result, 'content') else str(summary_result)
98
+
99
+ # 重组消息:System + Summary + Recent
100
+ new_messages = system_msgs + \
101
+ [SystemMessage(
102
+ content=f"[History Summary]: {summary_text}")] + keep_recent
103
+ return new_messages
104
+
105
+ except Exception as e:
106
+ SYLogger.error(
107
+ f"❌ Compression failed: {e}, using original context.")
108
+ return messages
19
109
 
20
110
  def _adapt_input(self, input: Any) -> List[BaseMessage]:
21
111
  """适配输入格式"""
@@ -25,6 +115,10 @@ class StructuredRunnableWithToken(Runnable):
25
115
  return [input]
26
116
  elif isinstance(input, str):
27
117
  return [HumanMessage(content=input)]
118
+ elif isinstance(input, dict) and "messages" in input:
119
+ # 如果已经是标准格式字典,直接提取
120
+ msgs = input["messages"]
121
+ return msgs if isinstance(msgs, list) else [msgs]
28
122
  elif isinstance(input, dict) and "input" in input:
29
123
  return [HumanMessage(content=str(input["input"]))]
30
124
  else:
@@ -40,7 +134,7 @@ class StructuredRunnableWithToken(Runnable):
40
134
  token_handler = TokensCallbackHandler()
41
135
 
42
136
  if config is None:
43
- processed_config = {"callbacks": [], "metadata": {}}
137
+ processed_config = RunnableConfig(callbacks=[], metadata={})
44
138
  else:
45
139
  processed_config = config.copy()
46
140
  if "callbacks" not in processed_config:
@@ -59,6 +153,7 @@ class StructuredRunnableWithToken(Runnable):
59
153
  callbacks.append(LLMLogger())
60
154
  callbacks.append(token_handler)
61
155
 
156
+ # 去重
62
157
  callback_types = {}
63
158
  unique_callbacks = []
64
159
  for cb in callbacks:
@@ -131,6 +226,8 @@ class StructuredRunnableWithToken(Runnable):
131
226
  user_id=user_id
132
227
  )
133
228
 
229
+ # 【同步模式下不建议触发压缩,因为压缩本身是异步调用 LLM】
230
+ # 如果同步也要压缩,需要用 asyncio.run(...),这里暂时保持原逻辑直接透传
134
231
  adapted_input = self._adapt_input(input)
135
232
  input_data = {"messages": adapted_input}
136
233
 
@@ -169,12 +266,26 @@ class StructuredRunnableWithToken(Runnable):
169
266
  user_id=user_id
170
267
  )
171
268
 
269
+ # 1. 适配输入
172
270
  adapted_input = self._adapt_input(input)
271
+
272
+ # 2. 检查并执行上下文压缩 (仅在异步模式且开启时)
273
+ if self.enable_compression:
274
+ max_tokens = self.llmConfig.maxTokens
275
+ current_tokens = self._count_tokens(adapted_input)
276
+
277
+ if current_tokens > max_tokens * self.threshold_ratio:
278
+ SYLogger.warning(
279
+ f"⚠️ Context limit reached: {current_tokens}/{max_tokens}")
280
+ # 执行压缩,替换 adapted_input
281
+ adapted_input = await self._acompress_context(adapted_input)
282
+
173
283
  input_data = {"messages": adapted_input}
174
284
 
175
285
  if span:
176
286
  span.update_trace(input=input_data)
177
287
 
288
+ # 3. 调用子链
178
289
  structured_result = await self.retry_chain.ainvoke(
179
290
  input_data,
180
291
  config=processed_config
@@ -6,6 +6,7 @@ from langchain_core.output_parsers import PydanticOutputParser
6
6
  from langchain_core.messages import BaseMessage, HumanMessage
7
7
  from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
8
8
  from pydantic import BaseModel, ValidationError, Field
9
+ from sycommon.config.LLMConfig import LLMConfig
9
10
  from sycommon.llm.struct_token import StructuredRunnableWithToken
10
11
 
11
12
 
@@ -13,9 +14,12 @@ class LLMWithAutoTokenUsage(BaseChatModel):
13
14
  """自动为结构化调用返回token_usage的LLM包装类"""
14
15
  llm: BaseChatModel = Field(default=None)
15
16
  langfuse: Optional[Langfuse] = Field(default=None, exclude=True)
17
+ llmConfig: Optional[LLMConfig] = Field(default=None, exclude=True)
18
+ summary_prompt: Optional[str] = Field(default=None, exclude=True)
16
19
 
17
- def __init__(self, llm: BaseChatModel, langfuse: Langfuse, **kwargs):
18
- super().__init__(llm=llm, langfuse=langfuse, **kwargs)
20
+ def __init__(self, llm: BaseChatModel, langfuse: Langfuse, llmConfig: LLMConfig, summary_prompt: str, **kwargs):
21
+ super().__init__(llm=llm, langfuse=langfuse, llmConfig=llmConfig,
22
+ summary_prompt=summary_prompt, **kwargs)
19
23
 
20
24
  def with_structured_output(
21
25
  self,
@@ -106,7 +110,7 @@ class LLMWithAutoTokenUsage(BaseChatModel):
106
110
  "initial": 0.1, "max": 3.0, "exp_base": 2.0, "jitter": 1.0}
107
111
  )
108
112
 
109
- return StructuredRunnableWithToken(retry_chain, self.langfuse)
113
+ return StructuredRunnableWithToken(retry_chain, self.langfuse, self.llmConfig, self.summary_prompt)
110
114
 
111
115
  # ========== 实现BaseChatModel抽象方法 ==========
112
116
  def _generate(self, messages, stop=None, run_manager=None, ** kwargs):
@@ -117,112 +117,112 @@ class RabbitMQClient:
117
117
  logger.info(f"队列重建成功: {self.queue_name}")
118
118
 
119
119
  async def connect(self) -> None:
120
+ """连接方法(修复恢复消费失效问题)"""
120
121
  if self._closed:
121
122
  raise RuntimeError("客户端已关闭,无法重新连接")
122
123
 
123
- # 1. 并发控制:使用 _connect_lock 保证只有一个协程在执行连接流程
124
- async with self._connect_lock:
125
- # 如果已经在连了,等待其完成
124
+ # 1. 获取 Condition
125
+ await self._connect_condition.acquire()
126
+
127
+ try:
128
+ # ===== 阶段 A: 快速检查与等待 =====
129
+ if await self.is_connected:
130
+ self._connect_condition.release()
131
+ return
132
+
126
133
  if self._connecting:
127
- logger.debug("连接正在进行中,等待现有连接完成...")
128
134
  try:
129
- # 等待条件变量,超时设为 60 秒防止死等
130
- await asyncio.wait_for(
131
- self._connect_condition.wait_for(
132
- lambda: not self._connecting),
133
- timeout=60.0
134
- )
135
+ logger.debug("连接正在进行中,等待现有连接完成...")
136
+ await asyncio.wait_for(self._connect_condition.wait(), timeout=60.0)
135
137
  except asyncio.TimeoutError:
138
+ self._connect_condition.release()
136
139
  raise RuntimeError("等待连接超时")
137
140
 
138
- # 等待结束后,再次检查状态
139
- if not await self.is_connected:
141
+ if await self.is_connected:
142
+ self._connect_condition.release()
143
+ return
144
+ else:
145
+ self._connect_condition.release()
140
146
  raise RuntimeError("等待重连后,连接状态依然无效")
141
- return
142
147
 
143
- # 标记开始连接
148
+ # ===== 阶段 B: 标记开始连接 =====
144
149
  self._connecting = True
150
+ # 【关键】释放锁,允许其他协程进入等待逻辑
151
+ self._connect_condition.release()
145
152
 
146
- # 释放 _connect_lock,允许其他协程读取状态,但在连接完成前阻止新的连接请求
147
- # 注意:这里释放了 _connect_lock,但 self._connecting = True 阻止了新的连接流程
153
+ except Exception as e:
154
+ if self._connect_condition.locked():
155
+ self._connect_condition.release()
156
+ raise
148
157
 
158
+ # === 阶段 C: 执行耗时的连接逻辑 (此时已释放锁,不阻塞其他协程) ===
149
159
  try:
150
- # --- 阶段1: 清理旧资源 ---
151
- # 重新获取锁进行资源清理
152
- async with self._connect_lock:
153
- was_consuming = self._consumer_tag is not None
154
-
155
- if self._channel_conn and self._conn_close_callback:
156
- try:
157
- self._channel_conn.close_callbacks.discard(
158
- self._conn_close_callback)
159
- except Exception:
160
- pass
161
-
162
- self._channel = None
163
- self._channel_conn = None
164
- self._exchange = None
165
- self._queue = None
166
- self._conn_close_callback = None
167
-
168
- # --- 阶段2: 获取新连接 (耗时IO) ---
160
+ # --- 步骤 1: 记录旧状态并清理资源 ---
161
+ # 必须在清理前记录状态
162
+ was_consuming = self._consumer_tag is not None
163
+
164
+ # 清理连接回调,防止旧的连接关闭触发新的重连
165
+ if self._channel_conn:
166
+ try:
167
+ if self._channel_conn.close_callbacks:
168
+ self._channel_conn.close_callbacks.clear()
169
+ except Exception:
170
+ pass
171
+
172
+ # 统一重置资源状态
173
+ self._channel = None
174
+ self._channel_conn = None
175
+ self._exchange = None
176
+ self._queue = None
177
+ self._consumer_tag = None
178
+
179
+ # --- 步骤 2: 获取新连接 ---
169
180
  self._channel, self._channel_conn = await self.connection_pool.acquire_channel()
170
181
 
171
- # 设置回调
182
+ # 设置连接关闭回调
172
183
  def on_conn_closed(conn, exc):
173
- logger.warning(f"检测到连接关闭: {exc}")
184
+ logger.warning(f"检测到底层连接关闭: {exc}")
174
185
  if not self._closed and not self._connecting:
175
186
  asyncio.create_task(self._safe_reconnect())
176
187
 
177
- self._conn_close_callback = on_conn_closed
178
188
  if self._channel_conn:
179
- self._channel_conn.close_callbacks.add(
180
- self._conn_close_callback)
189
+ self._channel_conn.close_callbacks.add(on_conn_closed)
181
190
 
182
- # 重建资源
191
+ # --- 步骤 3: 重建基础资源 (交换机和队列) ---
183
192
  await self._rebuild_resources()
184
193
 
185
- # --- 阶段3: 恢复消费 ---
186
- if was_consuming and self._message_handler and self.queue_name and self.queue_name.endswith(f".{self.app_name}"):
187
- logger.info("🔄 检测到重连前处于消费状态,尝试自动恢复...")
194
+ # --- 步骤 4: 恢复消费 ---
195
+ if was_consuming and self._message_handler:
196
+ logger.info("🔄 检测到重连前处于消费状态,尝试自动恢复消费...")
188
197
  try:
189
- self._queue = await self._channel.declare_queue(
190
- name=self.queue_name,
191
- durable=self.durable,
192
- auto_delete=self.auto_delete,
193
- passive=False,
194
- )
195
- await self._queue.bind(exchange=self._exchange, routing_key=self.routing_key)
196
- self._consumer_tag = await self._queue.consume(self._process_message_callback)
198
+ # 直接调用 start_consuming 来恢复,它内部包含了完整的队列检查和绑定逻辑
199
+ self._consumer_tag = await self.start_consuming()
197
200
  logger.info(f"✅ 消费已自动恢复: {self._consumer_tag}")
198
201
  except Exception as e:
199
202
  logger.error(f"❌ 自动恢复消费失败: {e}")
200
203
  self._consumer_tag = None
201
- else:
202
- self._consumer_tag = None
203
204
 
204
205
  logger.info("客户端连接初始化完成")
205
206
 
206
207
  except Exception as e:
207
208
  logger.error(f"客户端连接失败: {str(e)}", exc_info=True)
208
-
209
- # 异常时清理资源
210
- async with self._connect_lock:
211
- if self._channel_conn and self._conn_close_callback:
212
- self._channel_conn.close_callbacks.discard(
213
- self._conn_close_callback)
214
- self._channel = None
215
- self._channel_conn = None
216
- self._consumer_tag = None
217
-
209
+ # 异常时彻底清理
210
+ if self._channel_conn and self._channel_conn.close_callbacks:
211
+ self._channel_conn.close_callbacks.clear()
212
+ self._channel = None
213
+ self._channel_conn = None
214
+ self._queue = None
215
+ self._consumer_tag = None
218
216
  raise
219
217
 
220
218
  finally:
221
- # 【关键修复】必须在持有 Condition 内部锁的情况下调用 notify_all
222
- # 这里使用 async with self._connect_condition: 自动完成 acquire() ... notify_all() ... release()
223
- async with self._connect_condition:
219
+ # === 阶段 D: 恢复状态并通知 ===
220
+ await self._connect_condition.acquire()
221
+ try:
224
222
  self._connecting = False
225
223
  self._connect_condition.notify_all()
224
+ finally:
225
+ self._connect_condition.release()
226
226
 
227
227
  async def _safe_reconnect(self):
228
228
  """安全重连任务(仅用于被动监听连接关闭)"""
@@ -256,11 +256,6 @@ class RabbitMQClient:
256
256
  self._message_handler = handler
257
257
 
258
258
  async def _process_message_callback(self, message: AbstractIncomingMessage):
259
- # 记录消息的原始追踪ID
260
- # original_trace_id = message.headers.get(
261
- # "trace-id") if message.headers else None
262
- # current_retry = 0
263
-
264
259
  try:
265
260
  msg_obj: MQMsgModel
266
261
 
@@ -282,41 +277,16 @@ class RabbitMQClient:
282
277
  "trace-id") if message.headers else SYLogger.get_trace_id(),
283
278
  )
284
279
 
285
- # 2. 设置日志上下文
286
- # 注意:如果 header 中有 x-last-retry-ts,说明之前重试过
287
- # current_retry = int(message.headers.get("x-retry-count", 0))
288
280
  SYLogger.set_trace_id(msg_obj.traceId)
289
281
 
290
282
  # 3. 执行业务逻辑
291
283
  if self._message_handler:
292
284
  await self._message_handler(msg_obj, message)
293
285
 
294
- # 4. 业务成功,Ack (移除 finally 中的 ack,成功即确认)
295
286
  await message.ack()
296
287
 
297
288
  except Exception as e:
298
- # logger.error(f"消息处理异常 (第 {current_retry} 次尝试): {e}", exc_info=True)
299
-
300
- # # 【核心修复】使用原生 Nack + Requeue
301
- # if current_retry >= 3:
302
- # # 超过重试次数,丢弃消息(或进入死信队列)
303
- # logger.warning(f"重试次数超限 (3次),丢弃消息: {message.delivery_tag}")
304
- # await message.reject(requeue=False)
305
- # else:
306
- # # 还没到重试上限,重新入队
307
- # # 为了防止立即重试导致的死循环,我们需要人为增加一点延迟
308
- # # 但 Nack 本身不支持延迟,所以这里只能快速 Nack 让它尽快回来,
309
- # # 并在业务层(或外层)做好限流保护。
310
-
311
- # # 如果你有延迟队列插件,可以 publish 到延迟交换机。
312
- # # 如果没有,直接 requeue 是最安全的不丢包方案。
313
- # logger.info(f"消息处理失败,重新入队等待重试... (当前重试: {current_retry})")
314
-
315
- # # 技巧:如果你不想立即重试,可以 Nack(False) 然后手动 Publish 延迟消息
316
- # # 但为了解决你当前的“死循环”问题,直接 Nack(True) 是最有效的
317
- # # 延迟5秒
318
- # await asyncio.sleep(5)
319
- # await message.nack(requeue=True)
289
+ logger.error(f"消息处理异常: {e}", exc_info=True)
320
290
  await message.ack()
321
291
 
322
292
  async def start_consuming(self) -> Optional[ConsumerTag]:
@@ -142,19 +142,18 @@ class RabbitMQConnectionPool:
142
142
  async def _ensure_main_channel(self) -> RobustChannel:
143
143
  """
144
144
  确保主通道有效
145
- 逻辑:
146
- 1. 检查连接状态
147
- 2. 如果断开 -> 清理 -> 轮询重试
148
- 3. 如果连接在但通道断开 -> 仅重建通道
145
+ 修复逻辑:
146
+ 1. 如果连接对象不存在或已关闭 -> 重建连接
147
+ 2. 如果连接对象存在但处于重连中 -> 等待其 ready
148
+ 3. 如果通道不存在或已关闭 -> 重建通道
149
149
  """
150
150
  async with self._lock:
151
151
  if self._is_shutdown:
152
152
  raise RuntimeError("客户端已关闭")
153
153
 
154
- # --- 阶段A:连接恢复逻辑 (如果连接断了) ---
154
+ # --- 阶段A:连接恢复逻辑 ---
155
155
  if self._connection is None or self._connection.is_closed:
156
-
157
- # 1. 【强制】先彻底清理所有旧资源
156
+ # 情况1:连接对象不存在,或已显式关闭 -> 走清理重连流程
158
157
  await self._cleanup_resources()
159
158
 
160
159
  retry_hosts = self.hosts.copy()
@@ -162,7 +161,6 @@ class RabbitMQConnectionPool:
162
161
  last_error = None
163
162
  max_attempts = min(len(retry_hosts), 3)
164
163
 
165
- # 2. 轮询尝试新连接
166
164
  for _ in range(max_attempts):
167
165
  if not retry_hosts:
168
166
  break
@@ -173,19 +171,15 @@ class RabbitMQConnectionPool:
173
171
 
174
172
  try:
175
173
  temp_conn = await self._create_connection_impl(host)
176
-
177
- # 3. 只有在连接成功后,才更新 self._connection
178
174
  self._connection = temp_conn
179
- temp_conn = None # 转移所有权
175
+ temp_conn = None
180
176
  self._initialized = True
181
177
  last_error = None
182
178
  logger.info(f"🔗 [RECONNECT_OK] 切换到节点: {host}")
183
179
  break
184
-
185
180
  except Exception as e:
186
181
  logger.warning(f"⚠️ [RECONNECT_RETRY] 节点 {host} 不可用")
187
182
  if temp_conn is not None:
188
- # 尝试连接失败了,必须把这个“半成品”连接关掉
189
183
  try:
190
184
  await temp_conn.close()
191
185
  except Exception:
@@ -193,25 +187,38 @@ class RabbitMQConnectionPool:
193
187
  last_error = e
194
188
  await asyncio.sleep(self.reconnect_interval)
195
189
 
196
- # 4. 如果所有尝试都失败
197
190
  if last_error:
198
- # 确保状态是干净的
199
191
  self._connection = None
200
192
  self._initialized = False
201
193
  logger.error("💥 [RECONNECT_FATAL] 所有节点重试失败")
202
194
  raise ConnectionError("所有 RabbitMQ 节点连接失败") from last_error
195
+ else:
196
+ # 情况2:连接对象存在,但可能处于“连接丢失,正在重连”的状态
197
+ # 【关键修复】显式调用 connect() 确保连接真正就绪
198
+ # 如果连接已经好,这会立即返回;如果正在重连,这会等待完成
199
+ try:
200
+ logger.debug(f"⏳ [WAIT_CONN] 等待连接就绪...")
201
+ await self._connection.connect(timeout=5) # 设置一个短暂超时避免卡死太久
202
+ logger.debug(f"✅ [WAIT_CONN_OK] 连接已就绪")
203
+ except asyncio.TimeoutError:
204
+ logger.warning("⚠️ [WAIT_CONN_TIMEOUT] 等待连接就绪超时,强制重连")
205
+ await self._cleanup_resources()
206
+ raise
207
+ except Exception as e:
208
+ logger.error(f"❌ [WAIT_CONN_FAIL] 连接不可用: {e}")
209
+ await self._cleanup_resources()
210
+ raise
203
211
 
204
- # --- 阶段B:通道恢复逻辑 (如果连接在但通道断了) ---
205
- # 注意:这里不需要清理连接,只重置通道
212
+ # --- 阶段B:通道恢复逻辑 ---
206
213
  if self._channel is None or self._channel.is_closed:
207
214
  try:
215
+ # 只有在确保连接有效后,才创建通道
208
216
  self._channel = await self._connection.channel()
209
217
  await self._channel.set_qos(prefetch_count=self.prefetch_count)
210
218
  logger.info(f"✅ [CHANNEL_OK] 主通道已恢复")
211
219
  except Exception as e:
212
- # 如果连通道都创建不了,说明这个连接也是坏的,回滚到阶段A
213
- logger.error(f"❌ [CHANNEL_FAIL] 通道创建失败,标记连接无效: {e}")
214
- # 强制清理连接,触发下一次进入阶段A
220
+ logger.error(f"❌ [CHANNEL_FAIL] 通道创建失败: {e}")
221
+ # 如果连通道都创建不了,说明连接状态有问题,回滚清理
215
222
  await self._cleanup_resources()
216
223
  raise
217
224
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.1.57b6
3
+ Version: 0.1.57b8
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -25,6 +25,7 @@ Requires-Dist: pyyaml>=6.0.3
25
25
  Requires-Dist: sentry-sdk[fastapi]>=2.49.0
26
26
  Requires-Dist: sqlalchemy[asyncio]>=2.0.45
27
27
  Requires-Dist: starlette>=0.50.0
28
+ Requires-Dist: tiktoken>=0.12.0
28
29
  Requires-Dist: uvicorn>=0.40.0
29
30
 
30
31
  # sycommon-python-lib
@@ -19,4 +19,5 @@ pyyaml>=6.0.3
19
19
  sentry-sdk[fastapi]>=2.49.0
20
20
  sqlalchemy[asyncio]>=2.0.45
21
21
  starlette>=0.50.0
22
+ tiktoken>=0.12.0
22
23
  uvicorn>=0.40.0