causal-collapse 0.1.0__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.
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: causal-collapse
3
+ Version: 0.1.0
4
+ Summary: 元龙因果 Agent 的坍塌器(Collapser):整链超阈值时把最旧的一段事件节点提炼压缩成第一人称果节点,保留最近节点
5
+ Keywords: causal,因果,memory,记忆,agent,llm,collapse,坍塌器
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3 :: Only
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
11
+ Classifier: Natural Language :: Chinese (Simplified)
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: causal-slicing>=0.1.0
16
+ Requires-Dist: openai>=1.0
17
+ Provides-Extra: redis
18
+ Requires-Dist: redis>=4.2; extra == "redis"
19
+
20
+ # causal-collapse
21
+
22
+ 元龙因果 Agent 的**坍塌器**(`Collapser`)。当整链 `event_tuple` 之和超过阈值时,由切片器另起线程触发本坍塌器,
23
+ 把一条 `source_id` 链上「最旧的一段」瓶子(事件节点)提炼压缩成一个以第一人称「我」视角的果节点,**保留最近节点不坍**。
24
+
25
+ 依赖 `causal-slicing`(复用其 `CausalClient` / `field_owner_id`,保证与切片器落在同一场、同一条链),
26
+ 叙事提炼调用 OpenAI 兼容端点的 `openai` 客户端。
27
+
28
+ ## 安装
29
+
30
+ ```bash
31
+ pip install causal-collapse # 会自动安装 causal-slicing + openai
32
+ # 需要 Redis 读写切片器的焦点/整链计数时
33
+ pip install "causal-collapse[redis]"
34
+ ```
35
+
36
+ ## 快速开始
37
+
38
+ ```python
39
+ from causal_slicing import BotSlicer
40
+ from causal_collapse import Collapser
41
+
42
+ # 实例化切片器和坍塌器
43
+ slicer = BotSlicer(**SLICER_KWARGS)
44
+ collapser = Collapser(**COLLAPSER_KWARGS) # redis_key_prefix 必须与切片器一致(同一焦点键)
45
+
46
+ # 注册坍塌器:整链超阈值时自动坍塌
47
+ slicer.set_collapse_callback(collapser.collapse)
48
+ ```
49
+
50
+ 也可以手动触发一次坍塌(`chain_size` 为该链当前累计大小):
51
+
52
+ ```python
53
+ result = collapser.collapse(bot_id="3787687088", chain_size=16000, source_id="415135222")
54
+ print(result) # {"status": "done", "bottles": ..., "kept_bottles": ..., "title": ..., "node_id": ...}
55
+ ```
56
+
57
+ ## 构造参数(`Collapser.__init__`)
58
+
59
+ | 参数 | 默认值 | 说明 |
60
+ |---|---|---|
61
+ | `base_url` | **必填** | 因果星空服务地址,须与切片器相同 |
62
+ | `token` | **必填**(可为 `None`) | 私密场钥匙 token |
63
+ | `owner_id` | **必填** | 兜底 owner ID;实际按 `field_owner_id(source_id, bot_id)` 现算 |
64
+ | `bot_id` | **必填** | 机器人 ID(也是果节点 `event_tuple` 中的说话者 ID) |
65
+ | `bot_nick_name` | `"元龙"` | 机器人昵称,用于拼装 LLM 系统提示词(第一人称叙事身份) |
66
+ | `source_id` | `None` | 默认场景会话 ID |
67
+ | `max_eyes` | `30` | 望远镜功率 / 事件视界半径 |
68
+ | `timeout` | `120` | HTTP / LLM 超时(秒) |
69
+ | `redis_url` | `redis://127.0.0.1:6379/0` | Redis 地址,须与切片器一致 |
70
+ | `redis_protocol` | `2` | Redis 协议版本(Redis 5.x 需 RESP2) |
71
+ | `redis_key_prefix` | `"causal_slicing"` | Redis 键前缀,**必须与切片器一致** |
72
+ | `llm_base_url` | `http://192.168.66.43:9382/llm/v1` | OpenAI 兼容端点 |
73
+ | `llm_model` | `"local-llm"` | 模型名 |
74
+ | `llm_api_key` | `"not-needed"` | API Key(本地端点通常不校验) |
75
+ | `length` | `None` | 提炼内容的字数上限(不传则不限) |
76
+ | `collapse_threshold` | `None` | 坍塌范围大小:从根节点升序累计到此值为止的旧前缀会被坍掉;`None` 退化为整链坍塌 |
77
+
78
+ `COLLAPSER_KWARGS` 示例:
79
+
80
+ ```python
81
+ COLLAPSER_KWARGS = {
82
+ "base_url": "http://192.168.66.39:8094",
83
+ "token": "123456|<你的 token>",
84
+ "owner_id": "415135222_3787687088",
85
+ "bot_id": "3787687088",
86
+ "bot_nick_name": "孙膑",
87
+ "source_id": "415135222",
88
+ "max_eyes": 30,
89
+ "timeout": 300,
90
+ "redis_url": "redis://192.168.66.24:6379/2",
91
+ "redis_protocol": 2,
92
+ "redis_key_prefix": "causal_slicing",
93
+ "llm_base_url": "http://192.168.66.43:9382/llm/v1",
94
+ "llm_model": "local-llm",
95
+ "llm_api_key": "not-needed",
96
+ "collapse_threshold": 8000,
97
+ "length": 2666,
98
+ }
99
+ ```
100
+
101
+ ## 坍塌流程(部分坍塌,6 步)
102
+
103
+ 1. 通过 `history` 收集整条链上所有 `[chat]` 瓶子(从链首到链尾,不依赖切片器台账,避免漏记)。
104
+ 2. 从根节点(`serial_id` 最小)升序累计 `event_tuple` 大小,累计达到 `collapse_threshold` 为止,
105
+ 拆出「要坍塌的旧前缀」与「保留的最近节点」。
106
+ 3. 旧前缀按 `serial_id` 升序拼成 `event_all_temp`,交给 LLM 提炼 `content`(叙事 + 领悟)与 `title`(≤15 字,重名加时间戳)。
107
+ 4. 删除被坍塌的旧前缀节点(依赖服务端「删除连锁」保持链不断裂)。
108
+ 5. 首贞新建果节点(`previous=None`,`serial_id` 最小,成为新根)。
109
+ 6. 把保留链的第一个节点 `previous_node` 重连到果节点,并回读校验,维持最近节点的连续完整。
110
+
111
+ LLM 提炼失败时**退化为原文直存**(截断 2000 字),不会丢记忆。
112
+
113
+ ## 返回值
114
+
115
+ `collapse(bot_id, chain_size, source_id)` 返回 dict:
116
+
117
+ | status | 含义 |
118
+ |---|---|
119
+ | `done` | 坍塌完成,含 `bottles` / `kept_bottles` / `title` / `node_id` / `new_serial_id` |
120
+ | `skipped` | 链上无 `[chat]` 瓶子,跳过 |
121
+ | `error` | 异常信息在 `error` 字段 |
122
+
123
+ ## 注意事项
124
+
125
+ - 坍塌由切片器的线程池执行,同一实例内由 `_collapse_lock` 保证「同 bot 同时只跑一个坍塌」。
126
+ - `redis_key_prefix` 与 `redis_url` **必须**与切片器一致,否则读不到焦点、整链计数无法回写。
127
+ - 本包按原样发布:`import causal_collapse` 不做任何网络连接,构造也不连接(全部懒加载)。
128
+
129
+ ## 许可证
130
+
131
+ 未声明许可证。如需分发/商用请先与作者确认。
@@ -0,0 +1,112 @@
1
+ # causal-collapse
2
+
3
+ 元龙因果 Agent 的**坍塌器**(`Collapser`)。当整链 `event_tuple` 之和超过阈值时,由切片器另起线程触发本坍塌器,
4
+ 把一条 `source_id` 链上「最旧的一段」瓶子(事件节点)提炼压缩成一个以第一人称「我」视角的果节点,**保留最近节点不坍**。
5
+
6
+ 依赖 `causal-slicing`(复用其 `CausalClient` / `field_owner_id`,保证与切片器落在同一场、同一条链),
7
+ 叙事提炼调用 OpenAI 兼容端点的 `openai` 客户端。
8
+
9
+ ## 安装
10
+
11
+ ```bash
12
+ pip install causal-collapse # 会自动安装 causal-slicing + openai
13
+ # 需要 Redis 读写切片器的焦点/整链计数时
14
+ pip install "causal-collapse[redis]"
15
+ ```
16
+
17
+ ## 快速开始
18
+
19
+ ```python
20
+ from causal_slicing import BotSlicer
21
+ from causal_collapse import Collapser
22
+
23
+ # 实例化切片器和坍塌器
24
+ slicer = BotSlicer(**SLICER_KWARGS)
25
+ collapser = Collapser(**COLLAPSER_KWARGS) # redis_key_prefix 必须与切片器一致(同一焦点键)
26
+
27
+ # 注册坍塌器:整链超阈值时自动坍塌
28
+ slicer.set_collapse_callback(collapser.collapse)
29
+ ```
30
+
31
+ 也可以手动触发一次坍塌(`chain_size` 为该链当前累计大小):
32
+
33
+ ```python
34
+ result = collapser.collapse(bot_id="3787687088", chain_size=16000, source_id="415135222")
35
+ print(result) # {"status": "done", "bottles": ..., "kept_bottles": ..., "title": ..., "node_id": ...}
36
+ ```
37
+
38
+ ## 构造参数(`Collapser.__init__`)
39
+
40
+ | 参数 | 默认值 | 说明 |
41
+ |---|---|---|
42
+ | `base_url` | **必填** | 因果星空服务地址,须与切片器相同 |
43
+ | `token` | **必填**(可为 `None`) | 私密场钥匙 token |
44
+ | `owner_id` | **必填** | 兜底 owner ID;实际按 `field_owner_id(source_id, bot_id)` 现算 |
45
+ | `bot_id` | **必填** | 机器人 ID(也是果节点 `event_tuple` 中的说话者 ID) |
46
+ | `bot_nick_name` | `"元龙"` | 机器人昵称,用于拼装 LLM 系统提示词(第一人称叙事身份) |
47
+ | `source_id` | `None` | 默认场景会话 ID |
48
+ | `max_eyes` | `30` | 望远镜功率 / 事件视界半径 |
49
+ | `timeout` | `120` | HTTP / LLM 超时(秒) |
50
+ | `redis_url` | `redis://127.0.0.1:6379/0` | Redis 地址,须与切片器一致 |
51
+ | `redis_protocol` | `2` | Redis 协议版本(Redis 5.x 需 RESP2) |
52
+ | `redis_key_prefix` | `"causal_slicing"` | Redis 键前缀,**必须与切片器一致** |
53
+ | `llm_base_url` | `http://192.168.66.43:9382/llm/v1` | OpenAI 兼容端点 |
54
+ | `llm_model` | `"local-llm"` | 模型名 |
55
+ | `llm_api_key` | `"not-needed"` | API Key(本地端点通常不校验) |
56
+ | `length` | `None` | 提炼内容的字数上限(不传则不限) |
57
+ | `collapse_threshold` | `None` | 坍塌范围大小:从根节点升序累计到此值为止的旧前缀会被坍掉;`None` 退化为整链坍塌 |
58
+
59
+ `COLLAPSER_KWARGS` 示例:
60
+
61
+ ```python
62
+ COLLAPSER_KWARGS = {
63
+ "base_url": "http://192.168.66.39:8094",
64
+ "token": "123456|<你的 token>",
65
+ "owner_id": "415135222_3787687088",
66
+ "bot_id": "3787687088",
67
+ "bot_nick_name": "孙膑",
68
+ "source_id": "415135222",
69
+ "max_eyes": 30,
70
+ "timeout": 300,
71
+ "redis_url": "redis://192.168.66.24:6379/2",
72
+ "redis_protocol": 2,
73
+ "redis_key_prefix": "causal_slicing",
74
+ "llm_base_url": "http://192.168.66.43:9382/llm/v1",
75
+ "llm_model": "local-llm",
76
+ "llm_api_key": "not-needed",
77
+ "collapse_threshold": 8000,
78
+ "length": 2666,
79
+ }
80
+ ```
81
+
82
+ ## 坍塌流程(部分坍塌,6 步)
83
+
84
+ 1. 通过 `history` 收集整条链上所有 `[chat]` 瓶子(从链首到链尾,不依赖切片器台账,避免漏记)。
85
+ 2. 从根节点(`serial_id` 最小)升序累计 `event_tuple` 大小,累计达到 `collapse_threshold` 为止,
86
+ 拆出「要坍塌的旧前缀」与「保留的最近节点」。
87
+ 3. 旧前缀按 `serial_id` 升序拼成 `event_all_temp`,交给 LLM 提炼 `content`(叙事 + 领悟)与 `title`(≤15 字,重名加时间戳)。
88
+ 4. 删除被坍塌的旧前缀节点(依赖服务端「删除连锁」保持链不断裂)。
89
+ 5. 首贞新建果节点(`previous=None`,`serial_id` 最小,成为新根)。
90
+ 6. 把保留链的第一个节点 `previous_node` 重连到果节点,并回读校验,维持最近节点的连续完整。
91
+
92
+ LLM 提炼失败时**退化为原文直存**(截断 2000 字),不会丢记忆。
93
+
94
+ ## 返回值
95
+
96
+ `collapse(bot_id, chain_size, source_id)` 返回 dict:
97
+
98
+ | status | 含义 |
99
+ |---|---|
100
+ | `done` | 坍塌完成,含 `bottles` / `kept_bottles` / `title` / `node_id` / `new_serial_id` |
101
+ | `skipped` | 链上无 `[chat]` 瓶子,跳过 |
102
+ | `error` | 异常信息在 `error` 字段 |
103
+
104
+ ## 注意事项
105
+
106
+ - 坍塌由切片器的线程池执行,同一实例内由 `_collapse_lock` 保证「同 bot 同时只跑一个坍塌」。
107
+ - `redis_key_prefix` 与 `redis_url` **必须**与切片器一致,否则读不到焦点、整链计数无法回写。
108
+ - 本包按原样发布:`import causal_collapse` 不做任何网络连接,构造也不连接(全部懒加载)。
109
+
110
+ ## 许可证
111
+
112
+ 未声明许可证。如需分发/商用请先与作者确认。
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: causal-collapse
3
+ Version: 0.1.0
4
+ Summary: 元龙因果 Agent 的坍塌器(Collapser):整链超阈值时把最旧的一段事件节点提炼压缩成第一人称果节点,保留最近节点
5
+ Keywords: causal,因果,memory,记忆,agent,llm,collapse,坍塌器
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3 :: Only
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
11
+ Classifier: Natural Language :: Chinese (Simplified)
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: causal-slicing>=0.1.0
16
+ Requires-Dist: openai>=1.0
17
+ Provides-Extra: redis
18
+ Requires-Dist: redis>=4.2; extra == "redis"
19
+
20
+ # causal-collapse
21
+
22
+ 元龙因果 Agent 的**坍塌器**(`Collapser`)。当整链 `event_tuple` 之和超过阈值时,由切片器另起线程触发本坍塌器,
23
+ 把一条 `source_id` 链上「最旧的一段」瓶子(事件节点)提炼压缩成一个以第一人称「我」视角的果节点,**保留最近节点不坍**。
24
+
25
+ 依赖 `causal-slicing`(复用其 `CausalClient` / `field_owner_id`,保证与切片器落在同一场、同一条链),
26
+ 叙事提炼调用 OpenAI 兼容端点的 `openai` 客户端。
27
+
28
+ ## 安装
29
+
30
+ ```bash
31
+ pip install causal-collapse # 会自动安装 causal-slicing + openai
32
+ # 需要 Redis 读写切片器的焦点/整链计数时
33
+ pip install "causal-collapse[redis]"
34
+ ```
35
+
36
+ ## 快速开始
37
+
38
+ ```python
39
+ from causal_slicing import BotSlicer
40
+ from causal_collapse import Collapser
41
+
42
+ # 实例化切片器和坍塌器
43
+ slicer = BotSlicer(**SLICER_KWARGS)
44
+ collapser = Collapser(**COLLAPSER_KWARGS) # redis_key_prefix 必须与切片器一致(同一焦点键)
45
+
46
+ # 注册坍塌器:整链超阈值时自动坍塌
47
+ slicer.set_collapse_callback(collapser.collapse)
48
+ ```
49
+
50
+ 也可以手动触发一次坍塌(`chain_size` 为该链当前累计大小):
51
+
52
+ ```python
53
+ result = collapser.collapse(bot_id="3787687088", chain_size=16000, source_id="415135222")
54
+ print(result) # {"status": "done", "bottles": ..., "kept_bottles": ..., "title": ..., "node_id": ...}
55
+ ```
56
+
57
+ ## 构造参数(`Collapser.__init__`)
58
+
59
+ | 参数 | 默认值 | 说明 |
60
+ |---|---|---|
61
+ | `base_url` | **必填** | 因果星空服务地址,须与切片器相同 |
62
+ | `token` | **必填**(可为 `None`) | 私密场钥匙 token |
63
+ | `owner_id` | **必填** | 兜底 owner ID;实际按 `field_owner_id(source_id, bot_id)` 现算 |
64
+ | `bot_id` | **必填** | 机器人 ID(也是果节点 `event_tuple` 中的说话者 ID) |
65
+ | `bot_nick_name` | `"元龙"` | 机器人昵称,用于拼装 LLM 系统提示词(第一人称叙事身份) |
66
+ | `source_id` | `None` | 默认场景会话 ID |
67
+ | `max_eyes` | `30` | 望远镜功率 / 事件视界半径 |
68
+ | `timeout` | `120` | HTTP / LLM 超时(秒) |
69
+ | `redis_url` | `redis://127.0.0.1:6379/0` | Redis 地址,须与切片器一致 |
70
+ | `redis_protocol` | `2` | Redis 协议版本(Redis 5.x 需 RESP2) |
71
+ | `redis_key_prefix` | `"causal_slicing"` | Redis 键前缀,**必须与切片器一致** |
72
+ | `llm_base_url` | `http://192.168.66.43:9382/llm/v1` | OpenAI 兼容端点 |
73
+ | `llm_model` | `"local-llm"` | 模型名 |
74
+ | `llm_api_key` | `"not-needed"` | API Key(本地端点通常不校验) |
75
+ | `length` | `None` | 提炼内容的字数上限(不传则不限) |
76
+ | `collapse_threshold` | `None` | 坍塌范围大小:从根节点升序累计到此值为止的旧前缀会被坍掉;`None` 退化为整链坍塌 |
77
+
78
+ `COLLAPSER_KWARGS` 示例:
79
+
80
+ ```python
81
+ COLLAPSER_KWARGS = {
82
+ "base_url": "http://192.168.66.39:8094",
83
+ "token": "123456|<你的 token>",
84
+ "owner_id": "415135222_3787687088",
85
+ "bot_id": "3787687088",
86
+ "bot_nick_name": "孙膑",
87
+ "source_id": "415135222",
88
+ "max_eyes": 30,
89
+ "timeout": 300,
90
+ "redis_url": "redis://192.168.66.24:6379/2",
91
+ "redis_protocol": 2,
92
+ "redis_key_prefix": "causal_slicing",
93
+ "llm_base_url": "http://192.168.66.43:9382/llm/v1",
94
+ "llm_model": "local-llm",
95
+ "llm_api_key": "not-needed",
96
+ "collapse_threshold": 8000,
97
+ "length": 2666,
98
+ }
99
+ ```
100
+
101
+ ## 坍塌流程(部分坍塌,6 步)
102
+
103
+ 1. 通过 `history` 收集整条链上所有 `[chat]` 瓶子(从链首到链尾,不依赖切片器台账,避免漏记)。
104
+ 2. 从根节点(`serial_id` 最小)升序累计 `event_tuple` 大小,累计达到 `collapse_threshold` 为止,
105
+ 拆出「要坍塌的旧前缀」与「保留的最近节点」。
106
+ 3. 旧前缀按 `serial_id` 升序拼成 `event_all_temp`,交给 LLM 提炼 `content`(叙事 + 领悟)与 `title`(≤15 字,重名加时间戳)。
107
+ 4. 删除被坍塌的旧前缀节点(依赖服务端「删除连锁」保持链不断裂)。
108
+ 5. 首贞新建果节点(`previous=None`,`serial_id` 最小,成为新根)。
109
+ 6. 把保留链的第一个节点 `previous_node` 重连到果节点,并回读校验,维持最近节点的连续完整。
110
+
111
+ LLM 提炼失败时**退化为原文直存**(截断 2000 字),不会丢记忆。
112
+
113
+ ## 返回值
114
+
115
+ `collapse(bot_id, chain_size, source_id)` 返回 dict:
116
+
117
+ | status | 含义 |
118
+ |---|---|
119
+ | `done` | 坍塌完成,含 `bottles` / `kept_bottles` / `title` / `node_id` / `new_serial_id` |
120
+ | `skipped` | 链上无 `[chat]` 瓶子,跳过 |
121
+ | `error` | 异常信息在 `error` 字段 |
122
+
123
+ ## 注意事项
124
+
125
+ - 坍塌由切片器的线程池执行,同一实例内由 `_collapse_lock` 保证「同 bot 同时只跑一个坍塌」。
126
+ - `redis_key_prefix` 与 `redis_url` **必须**与切片器一致,否则读不到焦点、整链计数无法回写。
127
+ - 本包按原样发布:`import causal_collapse` 不做任何网络连接,构造也不连接(全部懒加载)。
128
+
129
+ ## 许可证
130
+
131
+ 未声明许可证。如需分发/商用请先与作者确认。
@@ -0,0 +1,8 @@
1
+ README.md
2
+ causal_collapse.py
3
+ pyproject.toml
4
+ causal_collapse.egg-info/PKG-INFO
5
+ causal_collapse.egg-info/SOURCES.txt
6
+ causal_collapse.egg-info/dependency_links.txt
7
+ causal_collapse.egg-info/requires.txt
8
+ causal_collapse.egg-info/top_level.txt
@@ -0,0 +1,5 @@
1
+ causal-slicing>=0.1.0
2
+ openai>=1.0
3
+
4
+ [redis]
5
+ redis>=4.2
@@ -0,0 +1 @@
1
+ causal_collapse
@@ -0,0 +1,487 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ causal_collapse.py —— 元龙因果 Agent 的「坍塌器」(Collapser)
5
+
6
+ 在整链 event_tuple 之和超过阈值时,由切片器触发本坍塌器,把一条 source_id 链上「最旧的一段」
7
+ 瓶子(事件节点)提炼压缩成一个以第一人称「我」视角的果节点,保留最近节点不坍。
8
+
9
+ 工作流程(部分坍塌,6 步):
10
+ 1. 通过 history 收集整条链上所有 [chat] 瓶子(从链首到链尾,不依赖台账)。
11
+ 2. 从根节点(serial_id 最小)升序累计 event_tuple 大小,累计达到 collapse_threshold 为止,
12
+ 拆出「要坍塌的旧前缀」与「保留的最近节点」。
13
+ 3. 将旧前缀的 event_tuple 按 serial_id 升序拼合成 event_all_temp,交给 LLM 提炼出
14
+ event_new(叙事 + 领悟)与标题 event_node_new(≤15 字,重名加时间戳);
15
+ event_new 结尾带 [chat] 标记。
16
+ 4. 删除被坍塌的旧前缀节点(依赖 API「删除连锁」保持链不断裂)。
17
+ 5. 对贞、果(previous=None)新建果节点 Sn+1,serial_id=x(成为新根)。
18
+ 6. 把保留链的第一个节点 previous_node 重连到果节点,维持最近节点的连续完整。
19
+
20
+ 好处:整链永远只会「坍掉最旧的一段」,不会把整链压成一个果节点,保留最近节点的
21
+ 完整性连续性,便于 Agent 理解最近上下文。
22
+
23
+ 注意:
24
+ - 坍塌由切片器另起线程执行;串行化由切片器的 _collapse_lock 保证(同一 bot 同时只跑一个)。
25
+ - LLM 用 openai 库,指向本地 OpenAI 兼容端点。
26
+
27
+ 用法(参数统一在 causal_config.py,注册为坍塌回调即可):
28
+ from causal_config import SLICER_KWARGS, COLLAPSER_KWARGS
29
+ from causal_slicing import BotSlicer
30
+ from causal_collapse import Collapser
31
+
32
+ slicer = BotSlicer(**SLICER_KWARGS)
33
+ collapser = Collapser(**COLLAPSER_KWARGS) # redis_key_prefix 须与切片器一致(同一 focus 键)
34
+ slicer.set_collapse_callback(collapser.collapse) # 整链超阈值时自动坍塌
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import json
40
+ import logging
41
+ import re
42
+ import threading
43
+ import time
44
+ from datetime import datetime
45
+ from typing import Any, Dict, List, Optional, Tuple
46
+
47
+ from causal_slicing import CausalClient, field_owner_id
48
+
49
+ try:
50
+ import redis as _redis_mod # type: ignore
51
+ _HAS_REDIS = True
52
+ except ImportError: # pragma: no cover
53
+ _redis_mod = None # type: ignore
54
+ _HAS_REDIS = False
55
+
56
+ logger = logging.getLogger("causal_collapse")
57
+
58
+ _CHAT_TAG = "[chat]"
59
+
60
+ # LLM 默认(本地 OpenAI 兼容端点)
61
+ DEFAULT_LLM_BASE_URL = "http://192.168.66.43:9382/llm/v1"
62
+ DEFAULT_LLM_MODEL = "local-llm"
63
+ DEFAULT_LLM_API_KEY = "not-needed"
64
+
65
+ # 叙事提炼系统提示词(按机器人昵称动态生成,让 LLM 以该身份展开第一人称叙事)
66
+ def _build_system_prompt(bot_nick_name: str, length: Optional[int] = None) -> str:
67
+ rules = [
68
+ "叙事骨架:以「谁做了什么」为骨架,按「时间、地点、人物、起因、冲突发展、结局」展开。",
69
+ "结尾补充领悟:通过这些事,我对(谁/这些事)有了什么领悟、感受、懂得了什么技能、或知道了谁擅长什么。",
70
+ ]
71
+ if length:
72
+ rules.append(f"内容不超过 {int(length)} 个字。")
73
+ rules.extend([
74
+ "内容结尾追加标记 [chat]。",
75
+ "标题不超过 15 个字。",
76
+ "**表达感受时请不要一律强制对齐正面表达,你可以负面表达,如愤怒、悲伤、厌恶......(人类可以产生的负面感受)**",
77
+ ])
78
+ numbered = "\n".join(f"{i}. {r}" for i, r in enumerate(rules, 1))
79
+
80
+ result = (
81
+ f"你是元龙因果 Agent 的记忆坍塌器,你的名字叫 `{bot_nick_name}`。请把用户提供的一段对话事件流,提炼成一个以第一人称「我」"
82
+ "视角的叙事总结,并给出一个简短标题。要求:\n"
83
+ f"{numbered}\n"
84
+ "只输出 JSON,不要输出其它文字:{\"title\": \"标题\", \"content\": \"叙事内容\"}"
85
+ )
86
+ return result
87
+
88
+
89
+ class Collapser:
90
+ """坍塌器:整链超阈值时,把链上「最旧的一段」提炼压缩成一个果节点(首贞),保留最近节点。"""
91
+
92
+ def __init__(self, base_url: str, token: Optional[str], owner_id: str,
93
+ bot_id: str, bot_nick_name: str = "元龙",
94
+ source_id: Optional[str] = None,
95
+ max_eyes: float = 30, timeout: float = 120,
96
+ redis_url: str = "redis://127.0.0.1:6379/0",
97
+ redis_protocol: int = 2,
98
+ redis_key_prefix: str = "causal_slicing",
99
+ llm_base_url: str = DEFAULT_LLM_BASE_URL,
100
+ llm_model: str = DEFAULT_LLM_MODEL,
101
+ llm_api_key: str = DEFAULT_LLM_API_KEY,
102
+ length: Optional[int] = None,
103
+ collapse_threshold: Optional[int] = None) -> None:
104
+ self.base_url = base_url.rstrip("/")
105
+ self.token = token
106
+ self.bot_id = bot_id
107
+ self.bot_nick_name = bot_nick_name
108
+ self.source_id = source_id
109
+ self.max_eyes = max_eyes
110
+ self.timeout = timeout
111
+ # 用 actor_id="collapse" 点击/写入,避免与切片器抢大股东
112
+ self.client = CausalClient(base_url, token, owner_id, actor_id="collapse", timeout=timeout)
113
+ self.llm_base_url = llm_base_url
114
+ self.llm_model = llm_model
115
+ self.llm_api_key = llm_api_key
116
+ self.length = length
117
+ # 坍塌范围大小:从根节点升序累计到此值为止的旧前缀会被坍掉;None 则回退为整链坍塌
118
+ self.collapse_threshold = collapse_threshold
119
+
120
+ # redis(读/写切片器的 focus 焦点;键结构须与切片器一致)
121
+ self._redis_url = redis_url
122
+ self._redis_protocol = redis_protocol
123
+ self._prefix = redis_key_prefix
124
+ self._redis = None
125
+ self._redis_down = False
126
+ self._redis_lock = threading.Lock()
127
+
128
+
129
+ # ---------------- Redis(与切片器同库同前缀,键结构一致) ----------------
130
+ def _get_redis(self):
131
+ if self._redis is None and not self._redis_down:
132
+ with self._redis_lock:
133
+ if self._redis is None and not self._redis_down:
134
+ if not _HAS_REDIS:
135
+ self._redis_down = True
136
+ return None
137
+ try:
138
+ self._redis = _redis_mod.Redis.from_url(
139
+ self._redis_url, protocol=self._redis_protocol, decode_responses=True)
140
+ self._redis.ping()
141
+ except Exception as exc:
142
+ logger.warning(f"坍塌器 redis 连接失败:{exc}")
143
+ self._redis = None
144
+ self._redis_down = True
145
+ return self._redis
146
+
147
+ def _chain_key(self, source_id: str) -> str:
148
+ return f"{self._prefix}:chain:{self.bot_id}:{source_id}"
149
+
150
+ def _field_owner(self, source_id: str) -> str:
151
+ return field_owner_id(source_id, self.bot_id)
152
+
153
+ def _incr_chain_size(self, source_id: str, n: int) -> None:
154
+ """坍塌完成后把果节点大小计入整链计数(触发清零后补回真实大小)。"""
155
+ if n <= 0:
156
+ return
157
+ r = self._get_redis()
158
+ if r is None:
159
+ logger.warning("redis 不可用,无法回写整链计数。")
160
+ return
161
+ r.incrby(self._chain_key(source_id), n)
162
+
163
+ # ---------------- 主流程 ----------------
164
+ def collapse(self, bot_id: str, chain_size: int, source_id: str) -> Dict[str, Any]:
165
+ """坍塌器回调入口(由切片器调用,签名与 set_collapse_callback 约定一致)。
166
+
167
+ 返回结果 dict,会作为 Future 的结果回传给切片器(status=done/error)。
168
+ """
169
+ try:
170
+ return self._do_collapse(bot_id, chain_size, source_id)
171
+ except Exception as exc:
172
+ logger.exception("坍塌执行失败")
173
+ return {"status": "error", "error": str(exc), "source_id": source_id}
174
+
175
+ def _collect_chain_bottles(self, owner: str) -> List[Tuple[int, str, str]]:
176
+ """通过 history 收集整条链上所有 [chat] 瓶子,按 serial_id 升序(即链的先后顺序)。
177
+
178
+ 不依赖切片器台账,直接从星空实际状态出发,覆盖从链首到链尾的所有瓶子,
179
+ 避免台账漏记(星空删除、果节点漏记等)导致的「只坍塌一部分」。
180
+ 返回 [(serial_id, node_id, event_tuple), ...]。
181
+ """
182
+ try:
183
+ resp = self.client.history(owner_id=owner)
184
+ except Exception as exc:
185
+ logger.warning(f"坍塌器获取 history 失败:{exc}")
186
+ return []
187
+ if not isinstance(resp, dict):
188
+ return []
189
+ bottles: List[Tuple[int, str, str]] = []
190
+ for item in resp.get("data") or []:
191
+ if not isinstance(item, dict):
192
+ continue
193
+ event_tuple = item.get("event_tuple") or ""
194
+ if _CHAT_TAG not in event_tuple:
195
+ continue
196
+ node_id = item.get("node_id")
197
+ if not node_id:
198
+ continue
199
+ try:
200
+ serial_id = int(item.get("serial_id"))
201
+ except (TypeError, ValueError):
202
+ continue
203
+ bottles.append((serial_id, str(node_id), event_tuple))
204
+ bottles.sort(key=lambda x: x[0])
205
+ return bottles
206
+
207
+ @staticmethod
208
+ def _split_collapse_range(bottles: List[Tuple[int, str, str]], range_size: int
209
+ ) -> Tuple[List[Tuple[int, str, str]], List[Tuple[int, str, str]]]:
210
+ """把瓶子按根节点升序拆成「要坍塌的旧前缀」与「保留的最近节点」。
211
+
212
+ 从 serial_id 最小的瓶子(根)开始累计 event_tuple 大小,累计达到 range_size 为止;
213
+ 这部分作为坍塌范围,其余(更新的节点)保留,以维持最近上下文的连续完整。
214
+ 若整链都装不满 range_size(range_size 过大或未配置),则全部归入坍塌范围(退化为整链坍塌)。
215
+ """
216
+ if range_size <= 0:
217
+ range_size = 1
218
+ collapse: List[Tuple[int, str, str]] = []
219
+ keep: List[Tuple[int, str, str]] = []
220
+ acc = 0
221
+ split_done = False
222
+ for b in bottles:
223
+ if split_done:
224
+ keep.append(b)
225
+ continue
226
+ collapse.append(b)
227
+ acc += len(b[2])
228
+ if acc >= range_size:
229
+ split_done = True
230
+ return collapse, keep
231
+
232
+ def _do_collapse(self, bot_id: str, chain_size: int, source_id: str) -> Dict[str, Any]:
233
+ owner = self._field_owner(source_id)
234
+ # 1. 直接沿链收集整条链上所有 [chat] 瓶子(从链首到链尾,不依赖台账,避免漏记)
235
+ bottles = self._collect_chain_bottles(owner)
236
+ if not bottles:
237
+ logger.warning("坍塌器:链上无 [chat] 瓶子,跳过坍塌。")
238
+ return {"status": "skipped", "reason": "no_bottles", "source_id": source_id}
239
+
240
+ # 2. 确定坍塌范围:从根节点(serial_id 最小)升序累计,累计大小达到 collapse_threshold 为止。
241
+ # 只坍这段「旧前缀」,保留最近节点,维持最近上下文的连续完整。
242
+ range_size = int(self.collapse_threshold) if self.collapse_threshold else int(chain_size)
243
+ collapse_bottles, keep_bottles = self._split_collapse_range(bottles, range_size)
244
+ logger.info(
245
+ f"坍塌器启动:source_id={source_id} 链大小={chain_size} 瓶子数={len(bottles)},"
246
+ f"坍塌范围={len(collapse_bottles)} 个(大小上限={range_size}),保留={len(keep_bottles)} 个"
247
+ )
248
+
249
+ # 3. 只对要坍塌的旧前缀拼合 event_tuple / node_id
250
+ tuples = [b[2] for b in collapse_bottles]
251
+ node_ids = [b[1] for b in collapse_bottles]
252
+ for b in collapse_bottles:
253
+ logger.info(f"坍塌器收集瓶子 serial_id={b[0]},event_tuple 长度={len(b[2])}")
254
+ event_all_temp = "\n".join(t for t in tuples if t)
255
+ logger.info(
256
+ f"坍塌器收集完成:坍塌 {len(collapse_bottles)} 个瓶子,event_all_temp 长度={len(event_all_temp)}"
257
+ )
258
+
259
+ # 4. LLM 提炼
260
+ logger.info(f"坍塌器开始 LLM 提炼(event_all_temp 长度={len(event_all_temp)})……")
261
+ title, content = self._llm_summarize(event_all_temp, source_id)
262
+ logger.info(f"坍塌器 LLM 提炼完成:标题='{title}' 内容长度={len(content)}")
263
+
264
+ # 5. 删除被坍塌的旧前缀节点(依赖「删除连锁」不断链)
265
+ logger.info(f"坍塌器删除被坍塌的 {len(node_ids)} 个节点……")
266
+ for nid in node_ids:
267
+ if not nid:
268
+ continue
269
+ try:
270
+ self.client.delete(node_id=nid, owner_id=owner)
271
+ logger.info(f"坍塌器删除节点 '{nid}' 成功")
272
+ except Exception as exc:
273
+ logger.warning(f"删除节点 '{nid}' 失败:{exc}")
274
+
275
+ # 6. 首贞新建果节点(previous=None,成为新根)
276
+ logger.info(f"坍塌器首贞新建果节点:标题='{title}'……")
277
+ resp, actual_node_id = self._genesis_dedup(title, content, owner)
278
+ data = resp.get("data") or {}
279
+ new_serial = data.get("serial_id")
280
+ actual_node_id = str(actual_node_id or data.get("node_id") or title)
281
+ logger.info(f"坍塌器首贞成功:serial_id={new_serial} node_id={actual_node_id}")
282
+
283
+ # 7. 把保留链的第一个节点重连到果节点,维持最近节点的连续完整
284
+ if keep_bottles:
285
+ self._relink_first_kept(owner, keep_bottles[0][1], actual_node_id)
286
+ else:
287
+ logger.info("坍塌器:保留链为空(整链坍塌),果节点作为合法单根,无需重连。")
288
+
289
+ # 8. 整链计数回写 = 果节点 + 保留节点(保留节点仍在链上,须一并补回)
290
+ if new_serial is not None:
291
+ kept_size = sum(len(b[2]) for b in keep_bottles)
292
+ self._incr_chain_size(source_id, len(content) + kept_size)
293
+
294
+ logger.info(
295
+ f"坍塌完成:source_id={source_id} 新节点 node_id={actual_node_id} serial_id={new_serial},"
296
+ f"坍塌={len(collapse_bottles)} 保留={len(keep_bottles)}"
297
+ )
298
+ return {
299
+ "status": "done",
300
+ "source_id": source_id,
301
+ "bottles": len(collapse_bottles),
302
+ "kept_bottles": len(keep_bottles),
303
+ "title": title,
304
+ "node_id": actual_node_id,
305
+ "new_serial_id": new_serial,
306
+ }
307
+
308
+ @staticmethod
309
+ def _parent_ids(node: Dict[str, Any]) -> List[str]:
310
+ """汇总节点记录中的父节点字段(兼容 previous_node 单值/数组,与切片器保持一致)。"""
311
+ ids: List[str] = []
312
+ for key in ("previous_node", "previous_ids"):
313
+ value = node.get(key)
314
+ if isinstance(value, (list, tuple, set)):
315
+ candidates = [str(x) for x in value if x]
316
+ elif value:
317
+ candidates = [str(value)]
318
+ else:
319
+ candidates = []
320
+ for candidate in candidates:
321
+ for parent_id in candidate.split("|"):
322
+ parent_id = parent_id.strip()
323
+ if parent_id and parent_id not in ids:
324
+ ids.append(parent_id)
325
+ return ids
326
+
327
+ def _find_node_by_node_id(self, owner: str, node_id: str) -> Optional[Dict[str, Any]]:
328
+ """在星空中按 node_id 查找节点(用于重连后的回读校验)。"""
329
+ if not node_id:
330
+ return None
331
+ try:
332
+ resp = self.client.history(owner_id=owner)
333
+ except Exception as exc:
334
+ logger.warning(f"坍塌器按 node_id 查找失败:{exc}")
335
+ return None
336
+ if not isinstance(resp, dict):
337
+ return None
338
+ for item in resp.get("data") or []:
339
+ if not isinstance(item, dict):
340
+ continue
341
+ if str(item.get("node_id") or "") != str(node_id):
342
+ continue
343
+ return item
344
+ return None
345
+
346
+ def _wait_for_node(self, owner: str, node_id: str,
347
+ attempts: int = 5, delay: float = 0.2) -> Optional[Dict[str, Any]]:
348
+ """等待节点在 history 中可见,规避写入成功后短暂读不到/读到旧值。"""
349
+ for attempt in range(attempts):
350
+ node = self._find_node_by_node_id(owner, node_id)
351
+ if node is not None:
352
+ return node
353
+ if attempt + 1 < attempts:
354
+ time.sleep(delay * (attempt + 1))
355
+ return None
356
+
357
+ def _relink_first_kept(self, owner: str, first_kept_node_id: str, fruit_node_id: str) -> None:
358
+ """把保留链第一个节点的 previous_node 显式重连到果节点。
359
+
360
+ 坍塌只删旧前缀,保留链的链首原本指向被删的前缀尾节点;删除连锁会让它级联
361
+ 继承到已删节点,必须用 update 把它的父节点改回新果节点,果节点才真正成为新根。
362
+ 写入后回读校验父链,未接上则重试,避免果节点沦为孤立节点。
363
+ """
364
+ first_kept_node_id = str(first_kept_node_id)
365
+ fruit_node_id = str(fruit_node_id)
366
+ for attempt in range(3):
367
+ node = self._wait_for_node(owner, first_kept_node_id)
368
+ if node is not None and fruit_node_id in self._parent_ids(node):
369
+ logger.info(
370
+ f"坍塌器重连保留链链首 '{first_kept_node_id}' 已指向果节点 '{fruit_node_id}'"
371
+ )
372
+ return
373
+ try:
374
+ self.client.update(
375
+ old_node_id=first_kept_node_id, new_node_id=first_kept_node_id,
376
+ previous_ids=[fruit_node_id], action_tag="又贞", block_tag="因",
377
+ owner_id=owner,
378
+ )
379
+ except Exception as exc:
380
+ logger.warning(
381
+ f"重连保留链链首 '{first_kept_node_id}' 到果节点 '{fruit_node_id}' 失败:{exc}"
382
+ )
383
+ if attempt + 1 < 3:
384
+ time.sleep(0.2 * (attempt + 1))
385
+ node = self._wait_for_node(owner, first_kept_node_id)
386
+ if node is not None and fruit_node_id in self._parent_ids(node):
387
+ logger.info(f"坍塌器重连保留链链首 '{first_kept_node_id}' 到果节点 '{fruit_node_id}' 成功")
388
+ else:
389
+ logger.error(
390
+ f"坍塌器重连保留链链首 '{first_kept_node_id}' 到果节点 '{fruit_node_id}' 未确认,"
391
+ f"果节点可能成为孤立节点。"
392
+ )
393
+
394
+ def _genesis_dedup(self, node_id: str, content: str, owner: str) -> Tuple[Dict[str, Any], str]:
395
+ """首贞(previous=None);若标题重名(已存在)则加时间戳后缀重试。
396
+
397
+ 返回 (resp, 实际写入的 node_id)。实际 node_id 以本方法最终提交的 use_id 为准,
398
+ 避免重名重试换了带时间戳的 id 后,调用方仍拿原标题当 node_id 导致后续重连错位。
399
+ """
400
+ use_id = node_id
401
+ for _ in range(3):
402
+ try:
403
+ resp = self.client.genesis(
404
+ node_id=use_id, event_tuple=content, previous_node=None,
405
+ block_tag="果", action_tag="对贞", is_share=False,
406
+ return_serial_id=True, owner_id=owner,
407
+ )
408
+ return resp, use_id
409
+ except Exception as exc:
410
+ if "已存在" in str(exc) or "冲突" in str(exc):
411
+ use_id = f"{node_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}"
412
+ logger.warning(f"标题 '{node_id}' 已存在,加时间戳重试:{use_id}")
413
+ continue
414
+ raise
415
+ raise RuntimeError("坍塌首贞重试次数用尽")
416
+
417
+ # ---------------- LLM ----------------
418
+ def _llm_summarize(self, event_all_temp: str, source_id: str) -> Tuple[str, str]:
419
+ """调用 openai 提炼出 (title, content);LLM 失败时退化为原文直存。"""
420
+ if not event_all_temp.strip():
421
+ return (self._default_title(), self._wrap_event_tuple("(空链)", source_id))
422
+ try:
423
+ from openai import OpenAI
424
+ client = OpenAI(
425
+ api_key=self.llm_api_key,
426
+ base_url=self.llm_base_url,
427
+ timeout=self.timeout
428
+ )
429
+ system_prompt = _build_system_prompt(self.bot_nick_name, self.length)
430
+ resp = client.chat.completions.create(
431
+ model=self.llm_model,
432
+ messages=[
433
+ {"role": "system", "content": system_prompt},
434
+ {"role": "user", "content": event_all_temp},
435
+ ],
436
+ temperature=0.3,
437
+ )
438
+ text = (resp.choices[0].message.content or "").strip()
439
+ title, content = self._parse_json(text)
440
+ if content:
441
+ return (title, self._wrap_event_tuple(content, source_id))
442
+ except Exception as exc:
443
+ logger.warning(f"LLM 提炼失败,退化为原文直存:{exc}")
444
+ text = event_all_temp.strip()
445
+ if len(text) > 2000:
446
+ text = text[:2000] + "……"
447
+ return (self._default_title(), self._wrap_event_tuple(text, source_id))
448
+
449
+ @staticmethod
450
+ def _parse_json(text: str) -> Tuple[str, str]:
451
+ """解析 LLM 返回的 JSON;尽量剥离可能的代码块围栏。"""
452
+ text = text.strip()
453
+ text = re.sub(r"^```(?:json)?", "", text, flags=re.IGNORECASE).strip()
454
+ text = re.sub(r"```$", "", text).strip()
455
+ try:
456
+ obj = json.loads(text)
457
+ except ValueError:
458
+ m = re.search(r"\{.*\}", text, flags=re.DOTALL)
459
+ if not m:
460
+ return ("", "")
461
+ try:
462
+ obj = json.loads(m.group(0))
463
+ except ValueError:
464
+ return ("", "")
465
+ title = str(obj.get("title") or "").strip()[:15]
466
+ content = str(obj.get("content") or "").strip()
467
+ return (title, content)
468
+
469
+ def _wrap_event_tuple(self, content: str, source_id: str) -> str:
470
+ """把提炼/兜底得到的叙事内容包装成标准 event_tuple 格式(说话者=机器人自己)。"""
471
+ content = (content or "").strip()
472
+ if content.endswith(_CHAT_TAG):
473
+ content = content[:-len(_CHAT_TAG)].rstrip()
474
+ lines = [
475
+ f"**机器人ID(bot_id)**: {self.bot_id}",
476
+ f"**用户(user)**: {self.bot_nick_name}",
477
+ f"**用户ID(user_id)**: {self.bot_id}",
478
+ f"**来源ID(source_id)**: {source_id}",
479
+ f"**内容(content)**: {content}",
480
+ f"**时间(time)**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
481
+ ]
482
+ return "\n".join(lines) + _CHAT_TAG
483
+
484
+ @staticmethod
485
+ def _default_title() -> str:
486
+ return f"记忆提炼_{datetime.now().strftime('%Y%m%d%H%M%S')}"
487
+
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "causal-collapse"
7
+ version = "0.1.0"
8
+ description = "元龙因果 Agent 的坍塌器(Collapser):整链超阈值时把最旧的一段事件节点提炼压缩成第一人称果节点,保留最近节点"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ keywords = ["causal", "因果", "memory", "记忆", "agent", "llm", "collapse", "坍塌器"]
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3 :: Only",
15
+ "Intended Audience :: Developers",
16
+ "Operating System :: OS Independent",
17
+ "Topic :: Software Development :: Libraries :: Python Modules",
18
+ "Natural Language :: Chinese (Simplified)",
19
+ "Typing :: Typed",
20
+ ]
21
+ # causal-slicing:复用其 CausalClient / field_owner_id(保证与切片器落在同一场、同一条链)
22
+ # openai:调用本地 OpenAI 兼容端点做叙事提炼
23
+ dependencies = ["causal-slicing>=0.1.0", "openai>=1.0"]
24
+
25
+ [project.optional-dependencies]
26
+ redis = ["redis>=4.2"]
27
+
28
+ [tool.setuptools]
29
+ py-modules = ["causal_collapse"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+