Autarkia-Cadence 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.
- autarkia_cadence-0.1.0/.gitignore +11 -0
- autarkia_cadence-0.1.0/LICENSE +21 -0
- autarkia_cadence-0.1.0/PKG-INFO +329 -0
- autarkia_cadence-0.1.0/README.md +301 -0
- autarkia_cadence-0.1.0/pyproject.toml +56 -0
- autarkia_cadence-0.1.0/src/autarkia_cadence/__init__.py +109 -0
- autarkia_cadence-0.1.0/src/autarkia_cadence/config.py +113 -0
- autarkia_cadence-0.1.0/src/autarkia_cadence/guard.py +325 -0
- autarkia_cadence-0.1.0/src/autarkia_cadence/py.typed +0 -0
- autarkia_cadence-0.1.0/src/autarkia_cadence/schedule_config.py +172 -0
- autarkia_cadence-0.1.0/src/autarkia_cadence/scheduling.py +181 -0
- autarkia_cadence-0.1.0/src/autarkia_cadence/silence.py +174 -0
- autarkia_cadence-0.1.0/src/autarkia_cadence/sleep.py +236 -0
- autarkia_cadence-0.1.0/src/autarkia_cadence/topic_groups.py +69 -0
- autarkia_cadence-0.1.0/src/autarkia_cadence/topics.py +158 -0
- autarkia_cadence-0.1.0/src/autarkia_cadence/tuning.py +77 -0
- autarkia_cadence-0.1.0/src/autarkia_cadence/windows.py +86 -0
- autarkia_cadence-0.1.0/tests/test_guard.py +486 -0
- autarkia_cadence-0.1.0/tests/test_schedule_config.py +152 -0
- autarkia_cadence-0.1.0/tests/test_scheduling.py +166 -0
- autarkia_cadence-0.1.0/tests/test_silence.py +153 -0
- autarkia_cadence-0.1.0/tests/test_sleep.py +177 -0
- autarkia_cadence-0.1.0/tests/test_topics.py +115 -0
- autarkia_cadence-0.1.0/tests/test_windows.py +86 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shihao Qi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: Autarkia-Cadence
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Proactive message cadence governance: cooldowns, quiet hours, daily caps, adaptive intervals, backoff jitter, and sleep-window probes. Extracted from the Autarkia agent runtime.
|
|
5
|
+
Project-URL: Homepage, https://qishihao.icu/projects/autarkia
|
|
6
|
+
Project-URL: Repository, https://github.com/hakituo/Autarkia-Cadence
|
|
7
|
+
Project-URL: Roadmap, https://github.com/hakituo/Autarkia-tool/blob/main/ROADMAP.md
|
|
8
|
+
Project-URL: Autarkia Runtime, https://github.com/hakituo/Autarkia
|
|
9
|
+
Project-URL: Author, https://qishihao.icu
|
|
10
|
+
Author-email: Shihao Qi <qishihao512@gmail.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: agent,autarkia,cooldown,notification,proactive,quiet-hours,rate-limit,scheduling,self-hosted
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<picture>
|
|
31
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/hakituo/Autarkia-Cadence/main/autarkia-dark.svg">
|
|
32
|
+
<img src="https://raw.githubusercontent.com/hakituo/Autarkia-Cadence/main/autarkia.svg" alt="Autarkia" width="300">
|
|
33
|
+
</picture>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<b>Autarkia-Cadence</b><br>
|
|
38
|
+
<sub>主动消息节奏治理 · Proactive message cadence governance for AI companions</sub>
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
<p align="center">
|
|
42
|
+
<a href="https://pypi.org/project/Autarkia-Cadence/"><img alt="PyPI" src="https://img.shields.io/pypi/v/autarkia-cadence.svg"></a>
|
|
43
|
+
<a href="https://pypi.org/project/Autarkia-Cadence/"><img alt="Python" src="https://img.shields.io/pypi/pyversions/autarkia-cadence.svg"></a>
|
|
44
|
+
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
<p align="center">
|
|
48
|
+
<a href="#中文">简体中文</a> · <a href="#english">English</a>
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 中文
|
|
54
|
+
|
|
55
|
+
分层冷却、话题组抑制、每日提问上限、自适应检查间隔(等抖动退避)、静默时段、睡眠探测策略——**在 LLM 被调用之前**就决定「现在到底该不该说话」。
|
|
56
|
+
|
|
57
|
+
来自长期无人值守运行的 [Autarkia](https://qishihao.icu/projects/autarkia) agent 运行时:它让一个自托管陪伴体没有变成通知轰炸机。整个系列见[组件库路线图](https://github.com/hakituo/Autarkia-tool/blob/main/ROADMAP.md)。
|
|
58
|
+
|
|
59
|
+
### 为什么
|
|
60
|
+
|
|
61
|
+
当角色能主动找你说话之后,难的问题不是「它该说什么」,而是**「此刻它到底该不该开口」**。提示词约束是守不住的。这个库在 LLM 调用之前划一条确定性的线:
|
|
62
|
+
|
|
63
|
+
- **全局冷却**:任意两条主动消息之间的最小间隔。
|
|
64
|
+
- **提问冷却**:刚问过问题,短时间内不再问。
|
|
65
|
+
- **同话题组**:「吃了吗」「外卖到了没」「饭好了吗」归入同一个 `meal` 组——换个说法也绕不过窗口。
|
|
66
|
+
- **未回复抑制**:上一条主动消息没有回应时,普通提问被抑制。
|
|
67
|
+
- **每日软上限**:好奇提问每天有上限。
|
|
68
|
+
- **专注闸门**:正式专注会话期间普通消息硬拦截;真提醒与定时任务豁免。
|
|
69
|
+
- **生成后复检**:一个以问号结尾的 `share_thought` 会被当作提问处理——意图名掩盖不了追问。
|
|
70
|
+
|
|
71
|
+
同包还带上闸门周围的调度算术:由精力/饥饿/情绪驱动的自适应间隔、等抖动的未回复退避、可注入数据源的下发时间表与静默时段加载、时钟窗口helper、晚安探测策略、长静默覆盖闸门。
|
|
72
|
+
|
|
73
|
+
零运行时依赖。纯函数 + 普通数据结构:不做 I/O、不发网络、不调 LLM、不读全局配置。
|
|
74
|
+
|
|
75
|
+
### 安装
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install autarkia-cadence
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 快速开始
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import time
|
|
85
|
+
from autarkia_cadence import CadenceGuard
|
|
86
|
+
|
|
87
|
+
guard = CadenceGuard(
|
|
88
|
+
focus_session_provider=lambda: my_focus_service.has_active_session(),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
verdict = guard.evaluate(
|
|
92
|
+
now=time.time(),
|
|
93
|
+
chosen_action="curious_question",
|
|
94
|
+
proactive_state={
|
|
95
|
+
"last_proactive_at": time.time() - 45 * 60,
|
|
96
|
+
"last_proactive_type": "share_thought",
|
|
97
|
+
"last_proactive_replied": True,
|
|
98
|
+
"last_sent_topic": "curious_question:food",
|
|
99
|
+
},
|
|
100
|
+
candidate_topic_label="curious_question:food",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if not verdict.allowed:
|
|
104
|
+
print(verdict.reason) # "cadence_same_topic_group"
|
|
105
|
+
print(verdict.retry_after_minutes) # 140
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
闸门不写状态:`last_proactive_*`、`last_sent_topic`、`today_sent_events` 由你持久化,它只读。
|
|
109
|
+
|
|
110
|
+
生成后复检:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
verdict = guard.evaluate_final_text(
|
|
114
|
+
now=time.time(),
|
|
115
|
+
intent="share_thought",
|
|
116
|
+
final_text="在炒菜。你做饭还是点外卖?",
|
|
117
|
+
proactive_state=state,
|
|
118
|
+
)
|
|
119
|
+
# 上一条主动消息未回复时,这条会被当作提问拦下
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
自适应间隔:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from autarkia_cadence import SchedulerConfig, SchedulerLogic
|
|
126
|
+
|
|
127
|
+
scheduler = SchedulerLogic(SchedulerConfig(active_interval_seconds=300))
|
|
128
|
+
delay = scheduler.calculate_dynamic_interval(
|
|
129
|
+
bio_state={"life": {"energy": 20, "hunger": 90}},
|
|
130
|
+
emotion_state={"primary_emotion": "sad", "intensity": 0.8},
|
|
131
|
+
consecutive_non_responses=2,
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
睡眠探测:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from autarkia_cadence import SleepPolicy
|
|
139
|
+
|
|
140
|
+
policy = SleepPolicy().resolve_sleep_probe_policy(
|
|
141
|
+
now=time.time(),
|
|
142
|
+
sleep_session_active=True,
|
|
143
|
+
allow_goodnight_probe=True,
|
|
144
|
+
reduced_mode_active=False,
|
|
145
|
+
reduced_mode_reason="none",
|
|
146
|
+
last_goodnight_ts=goodnight_ts,
|
|
147
|
+
last_goodnight_probe_ts=0,
|
|
148
|
+
default_next_check=300,
|
|
149
|
+
min_gap_seconds=600,
|
|
150
|
+
goodnight_probe_gap_seconds=7200,
|
|
151
|
+
goodnight_low_disturb_gap_seconds=7200,
|
|
152
|
+
)
|
|
153
|
+
policy["allow_send"] # 晚安后 10–30 分钟的第一次探测
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 模块分工
|
|
157
|
+
|
|
158
|
+
| 组件 | 模块 | 决定什么 |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| `CadenceGuard` / `CadenceVerdict` | `autarkia_cadence.guard` | 放行 / 推迟、原因、重试延迟 |
|
|
161
|
+
| `CadenceConfig` | `autarkia_cadence.config` | 全部阈值,注入式 |
|
|
162
|
+
| `classify_topic`、`group_for_topic_label` | `autarkia_cadence.topics`、`.topic_groups` | 话题标签与冷却分组 |
|
|
163
|
+
| `SchedulerLogic` | `autarkia_cadence.scheduling` | 下一次检查间隔 |
|
|
164
|
+
| `SilenceGate` / `SilenceContext` | `autarkia_cadence.silence` | 强制发送的覆盖条件 |
|
|
165
|
+
| `SleepPolicy` | `autarkia_cadence.sleep` | 晚安探测、专注低打扰 |
|
|
166
|
+
| `ScheduleConfigLoader` | `autarkia_cadence.schedule_config` | 下发时间表 + 静默时段 |
|
|
167
|
+
| `parse_hhmm`、`is_in_range_minutes` | `autarkia_cadence.windows` | 支持跨零点的时钟窗口 |
|
|
168
|
+
|
|
169
|
+
推迟原因(`reason`):`cadence_no_intent`、`formal_focus_session_active`、`last_proactive_unanswered`、`cadence_global_cooldown`、`cadence_question_cooldown`、`cadence_same_topic_group`、`cadence_curious_question_daily_cap`。
|
|
170
|
+
|
|
171
|
+
### 设计说明
|
|
172
|
+
|
|
173
|
+
- **豁免要显式声明,不靠推断**:硬事件意图(`reminder`、`good_morning_proactive` 等)写在 `CadenceConfig.exempt_intents` 里;未登记的意图一律走正常节奏检查。
|
|
174
|
+
- **用话题组,不做文本相似度**:确定性分桶更便宜,也更难被绕开。
|
|
175
|
+
- **专注状态是注入的**:库里没有学习子系统,你传一个 callable;provider 抛异常按「未在专注」处理。
|
|
176
|
+
- **时间全部由外部传入**:闸门内部不读墙钟,测试是纯的。
|
|
177
|
+
|
|
178
|
+
### 系列与链接
|
|
179
|
+
|
|
180
|
+
- 项目页:<https://qishihao.icu/projects/autarkia>
|
|
181
|
+
- 运行时仓库:<https://github.com/hakituo/Autarkia>
|
|
182
|
+
- 本包:<https://github.com/hakituo/Autarkia-Cadence>
|
|
183
|
+
- 组件库路线图:<https://github.com/hakituo/Autarkia-tool/blob/main/ROADMAP.md>
|
|
184
|
+
- 作者:Shihao Qi — <https://qishihao.icu>
|
|
185
|
+
|
|
186
|
+
### 许可
|
|
187
|
+
|
|
188
|
+
MIT
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## English
|
|
193
|
+
|
|
194
|
+
Layered cooldowns, topic-group suppression, daily question caps, adaptive check intervals with equal-jitter backoff, quiet hours, and sleep-window probes — a deterministic "should it speak at all, right now" line drawn **before** any LLM call.
|
|
195
|
+
|
|
196
|
+
Extracted from the [Autarkia](https://qishihao.icu/projects/autarkia) agent runtime, where it keeps a self-hosted companion from turning into a notification machine over months of unattended operation. See the [component roadmap](https://github.com/hakituo/Autarkia-tool/blob/main/ROADMAP.md) for the rest of the series.
|
|
197
|
+
|
|
198
|
+
### Why
|
|
199
|
+
|
|
200
|
+
Once a companion can message you first, the hard question is not "what should it say" but **"should it speak at all, right now"**. Prompt instructions do not hold. This library holds a deterministic line before any LLM call:
|
|
201
|
+
|
|
202
|
+
- **Global cooldown** — minimum gap between any two proactive messages.
|
|
203
|
+
- **Question cooldown** — after asking something, don't ask again soon.
|
|
204
|
+
- **Same topic group** — "did you eat?" / "did the delivery arrive?" / "food ready?" collapse into one `meal` group, so rephrasing cannot bypass the window.
|
|
205
|
+
- **Unanswered suppression** — if the last proactive message got no reply, ordinary questions are suppressed.
|
|
206
|
+
- **Daily soft cap** — a bounded number of curious questions per day.
|
|
207
|
+
- **Focus gate** — a formal focus session hard-blocks ordinary messages; real reminders and scheduled tasks stay exempt.
|
|
208
|
+
- **Post-generation recheck** — a `share_thought` that ends with a question is treated as a question. The intent name cannot hide a follow-up.
|
|
209
|
+
|
|
210
|
+
It also ships the scheduling arithmetic around the gate: adaptive intervals driven by energy/hunger/emotion, equal-jitter non-response backoff, push-schedule/quiet-hours loading with an injectable source, clock-window helpers, goodnight probe policy, and the long-silence override gate.
|
|
211
|
+
|
|
212
|
+
Zero runtime dependencies. Pure functions and plain data; no I/O, no network, no LLM, no global config.
|
|
213
|
+
|
|
214
|
+
### Install
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
pip install autarkia-cadence
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Quick start
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
import time
|
|
224
|
+
from autarkia_cadence import CadenceGuard
|
|
225
|
+
|
|
226
|
+
guard = CadenceGuard(
|
|
227
|
+
focus_session_provider=lambda: my_focus_service.has_active_session(),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
verdict = guard.evaluate(
|
|
231
|
+
now=time.time(),
|
|
232
|
+
chosen_action="curious_question",
|
|
233
|
+
proactive_state={
|
|
234
|
+
"last_proactive_at": time.time() - 45 * 60,
|
|
235
|
+
"last_proactive_type": "share_thought",
|
|
236
|
+
"last_proactive_replied": True,
|
|
237
|
+
"last_sent_topic": "curious_question:food",
|
|
238
|
+
},
|
|
239
|
+
candidate_topic_label="curious_question:food",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if not verdict.allowed:
|
|
243
|
+
print(verdict.reason) # "cadence_same_topic_group"
|
|
244
|
+
print(verdict.retry_after_minutes) # 140
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The guard never writes state. You persist `last_proactive_*`, `last_sent_topic` and `today_sent_events`; the guard only reads them.
|
|
248
|
+
|
|
249
|
+
#### Recheck after generation
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
verdict = guard.evaluate_final_text(
|
|
253
|
+
now=time.time(),
|
|
254
|
+
intent="share_thought",
|
|
255
|
+
final_text="在炒菜。你做饭还是点外卖?",
|
|
256
|
+
proactive_state=state,
|
|
257
|
+
)
|
|
258
|
+
# blocked as a question if the previous proactive message is unanswered
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
#### Adaptive intervals
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
from autarkia_cadence import SchedulerConfig, SchedulerLogic
|
|
265
|
+
|
|
266
|
+
scheduler = SchedulerLogic(SchedulerConfig(active_interval_seconds=300))
|
|
267
|
+
delay = scheduler.calculate_dynamic_interval(
|
|
268
|
+
bio_state={"life": {"energy": 20, "hunger": 90}},
|
|
269
|
+
emotion_state={"primary_emotion": "sad", "intensity": 0.8},
|
|
270
|
+
consecutive_non_responses=2,
|
|
271
|
+
)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
#### Sleep probes
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
from autarkia_cadence import SleepPolicy
|
|
278
|
+
|
|
279
|
+
policy = SleepPolicy().resolve_sleep_probe_policy(
|
|
280
|
+
now=time.time(),
|
|
281
|
+
sleep_session_active=True,
|
|
282
|
+
allow_goodnight_probe=True,
|
|
283
|
+
reduced_mode_active=False,
|
|
284
|
+
reduced_mode_reason="none",
|
|
285
|
+
last_goodnight_ts=goodnight_ts,
|
|
286
|
+
last_goodnight_probe_ts=0,
|
|
287
|
+
default_next_check=300,
|
|
288
|
+
min_gap_seconds=600,
|
|
289
|
+
goodnight_probe_gap_seconds=7200,
|
|
290
|
+
goodnight_low_disturb_gap_seconds=7200,
|
|
291
|
+
)
|
|
292
|
+
policy["allow_send"] # first probe 10-30 minutes after goodnight
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Concepts
|
|
296
|
+
|
|
297
|
+
| Piece | Module | What it decides |
|
|
298
|
+
| --- | --- | --- |
|
|
299
|
+
| `CadenceGuard` / `CadenceVerdict` | `autarkia_cadence.guard` | allow / defer, reason, retry delay |
|
|
300
|
+
| `CadenceConfig` | `autarkia_cadence.config` | all thresholds, injected |
|
|
301
|
+
| `classify_topic`, `group_for_topic_label` | `autarkia_cadence.topics`, `.topic_groups` | topic labels and cooldown groups |
|
|
302
|
+
| `SchedulerLogic` | `autarkia_cadence.scheduling` | next check interval |
|
|
303
|
+
| `SilenceGate` / `SilenceContext` | `autarkia_cadence.silence` | force-send overrides |
|
|
304
|
+
| `SleepPolicy` | `autarkia_cadence.sleep` | goodnight probes, focus low-disturb |
|
|
305
|
+
| `ScheduleConfigLoader` | `autarkia_cadence.schedule_config` | push schedule + quiet hours |
|
|
306
|
+
| `parse_hhmm`, `is_in_range_minutes` | `autarkia_cadence.windows` | clock windows with midnight wrap |
|
|
307
|
+
|
|
308
|
+
#### Defer reasons
|
|
309
|
+
|
|
310
|
+
`cadence_no_intent`, `formal_focus_session_active`, `last_proactive_unanswered`, `cadence_global_cooldown`, `cadence_question_cooldown`, `cadence_same_topic_group`, `cadence_curious_question_daily_cap`.
|
|
311
|
+
|
|
312
|
+
### Design notes
|
|
313
|
+
|
|
314
|
+
- **Explicit exemption, not inference.** Hard-event intents (`reminder`, `good_morning_proactive`, ...) are declared in `CadenceConfig.exempt_intents`. Unknown intents go through normal cadence.
|
|
315
|
+
- **Topic groups, not text similarity.** Deterministic buckets are cheaper and harder to game than fuzzy matching.
|
|
316
|
+
- **Injected focus state.** The library has no study subsystem; pass a callable. Provider errors mean "not active".
|
|
317
|
+
- **All time is passed in.** No wall-clock reads inside the guard; tests are pure.
|
|
318
|
+
|
|
319
|
+
### The bigger picture
|
|
320
|
+
|
|
321
|
+
- Project page: <https://qishihao.icu/projects/autarkia>
|
|
322
|
+
- Runtime repository: <https://github.com/hakituo/Autarkia>
|
|
323
|
+
- This package: <https://github.com/hakituo/Autarkia-Cadence>
|
|
324
|
+
- Roadmap: <https://github.com/hakituo/Autarkia-tool/blob/main/ROADMAP.md>
|
|
325
|
+
- Author: Shihao Qi — <https://qishihao.icu>
|
|
326
|
+
|
|
327
|
+
### License
|
|
328
|
+
|
|
329
|
+
MIT
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/hakituo/Autarkia-Cadence/main/autarkia-dark.svg">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/hakituo/Autarkia-Cadence/main/autarkia.svg" alt="Autarkia" width="300">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<b>Autarkia-Cadence</b><br>
|
|
10
|
+
<sub>主动消息节奏治理 · Proactive message cadence governance for AI companions</sub>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<a href="https://pypi.org/project/Autarkia-Cadence/"><img alt="PyPI" src="https://img.shields.io/pypi/v/autarkia-cadence.svg"></a>
|
|
15
|
+
<a href="https://pypi.org/project/Autarkia-Cadence/"><img alt="Python" src="https://img.shields.io/pypi/pyversions/autarkia-cadence.svg"></a>
|
|
16
|
+
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="#中文">简体中文</a> · <a href="#english">English</a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 中文
|
|
26
|
+
|
|
27
|
+
分层冷却、话题组抑制、每日提问上限、自适应检查间隔(等抖动退避)、静默时段、睡眠探测策略——**在 LLM 被调用之前**就决定「现在到底该不该说话」。
|
|
28
|
+
|
|
29
|
+
来自长期无人值守运行的 [Autarkia](https://qishihao.icu/projects/autarkia) agent 运行时:它让一个自托管陪伴体没有变成通知轰炸机。整个系列见[组件库路线图](https://github.com/hakituo/Autarkia-tool/blob/main/ROADMAP.md)。
|
|
30
|
+
|
|
31
|
+
### 为什么
|
|
32
|
+
|
|
33
|
+
当角色能主动找你说话之后,难的问题不是「它该说什么」,而是**「此刻它到底该不该开口」**。提示词约束是守不住的。这个库在 LLM 调用之前划一条确定性的线:
|
|
34
|
+
|
|
35
|
+
- **全局冷却**:任意两条主动消息之间的最小间隔。
|
|
36
|
+
- **提问冷却**:刚问过问题,短时间内不再问。
|
|
37
|
+
- **同话题组**:「吃了吗」「外卖到了没」「饭好了吗」归入同一个 `meal` 组——换个说法也绕不过窗口。
|
|
38
|
+
- **未回复抑制**:上一条主动消息没有回应时,普通提问被抑制。
|
|
39
|
+
- **每日软上限**:好奇提问每天有上限。
|
|
40
|
+
- **专注闸门**:正式专注会话期间普通消息硬拦截;真提醒与定时任务豁免。
|
|
41
|
+
- **生成后复检**:一个以问号结尾的 `share_thought` 会被当作提问处理——意图名掩盖不了追问。
|
|
42
|
+
|
|
43
|
+
同包还带上闸门周围的调度算术:由精力/饥饿/情绪驱动的自适应间隔、等抖动的未回复退避、可注入数据源的下发时间表与静默时段加载、时钟窗口helper、晚安探测策略、长静默覆盖闸门。
|
|
44
|
+
|
|
45
|
+
零运行时依赖。纯函数 + 普通数据结构:不做 I/O、不发网络、不调 LLM、不读全局配置。
|
|
46
|
+
|
|
47
|
+
### 安装
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install autarkia-cadence
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 快速开始
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import time
|
|
57
|
+
from autarkia_cadence import CadenceGuard
|
|
58
|
+
|
|
59
|
+
guard = CadenceGuard(
|
|
60
|
+
focus_session_provider=lambda: my_focus_service.has_active_session(),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
verdict = guard.evaluate(
|
|
64
|
+
now=time.time(),
|
|
65
|
+
chosen_action="curious_question",
|
|
66
|
+
proactive_state={
|
|
67
|
+
"last_proactive_at": time.time() - 45 * 60,
|
|
68
|
+
"last_proactive_type": "share_thought",
|
|
69
|
+
"last_proactive_replied": True,
|
|
70
|
+
"last_sent_topic": "curious_question:food",
|
|
71
|
+
},
|
|
72
|
+
candidate_topic_label="curious_question:food",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if not verdict.allowed:
|
|
76
|
+
print(verdict.reason) # "cadence_same_topic_group"
|
|
77
|
+
print(verdict.retry_after_minutes) # 140
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
闸门不写状态:`last_proactive_*`、`last_sent_topic`、`today_sent_events` 由你持久化,它只读。
|
|
81
|
+
|
|
82
|
+
生成后复检:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
verdict = guard.evaluate_final_text(
|
|
86
|
+
now=time.time(),
|
|
87
|
+
intent="share_thought",
|
|
88
|
+
final_text="在炒菜。你做饭还是点外卖?",
|
|
89
|
+
proactive_state=state,
|
|
90
|
+
)
|
|
91
|
+
# 上一条主动消息未回复时,这条会被当作提问拦下
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
自适应间隔:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from autarkia_cadence import SchedulerConfig, SchedulerLogic
|
|
98
|
+
|
|
99
|
+
scheduler = SchedulerLogic(SchedulerConfig(active_interval_seconds=300))
|
|
100
|
+
delay = scheduler.calculate_dynamic_interval(
|
|
101
|
+
bio_state={"life": {"energy": 20, "hunger": 90}},
|
|
102
|
+
emotion_state={"primary_emotion": "sad", "intensity": 0.8},
|
|
103
|
+
consecutive_non_responses=2,
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
睡眠探测:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from autarkia_cadence import SleepPolicy
|
|
111
|
+
|
|
112
|
+
policy = SleepPolicy().resolve_sleep_probe_policy(
|
|
113
|
+
now=time.time(),
|
|
114
|
+
sleep_session_active=True,
|
|
115
|
+
allow_goodnight_probe=True,
|
|
116
|
+
reduced_mode_active=False,
|
|
117
|
+
reduced_mode_reason="none",
|
|
118
|
+
last_goodnight_ts=goodnight_ts,
|
|
119
|
+
last_goodnight_probe_ts=0,
|
|
120
|
+
default_next_check=300,
|
|
121
|
+
min_gap_seconds=600,
|
|
122
|
+
goodnight_probe_gap_seconds=7200,
|
|
123
|
+
goodnight_low_disturb_gap_seconds=7200,
|
|
124
|
+
)
|
|
125
|
+
policy["allow_send"] # 晚安后 10–30 分钟的第一次探测
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 模块分工
|
|
129
|
+
|
|
130
|
+
| 组件 | 模块 | 决定什么 |
|
|
131
|
+
| --- | --- | --- |
|
|
132
|
+
| `CadenceGuard` / `CadenceVerdict` | `autarkia_cadence.guard` | 放行 / 推迟、原因、重试延迟 |
|
|
133
|
+
| `CadenceConfig` | `autarkia_cadence.config` | 全部阈值,注入式 |
|
|
134
|
+
| `classify_topic`、`group_for_topic_label` | `autarkia_cadence.topics`、`.topic_groups` | 话题标签与冷却分组 |
|
|
135
|
+
| `SchedulerLogic` | `autarkia_cadence.scheduling` | 下一次检查间隔 |
|
|
136
|
+
| `SilenceGate` / `SilenceContext` | `autarkia_cadence.silence` | 强制发送的覆盖条件 |
|
|
137
|
+
| `SleepPolicy` | `autarkia_cadence.sleep` | 晚安探测、专注低打扰 |
|
|
138
|
+
| `ScheduleConfigLoader` | `autarkia_cadence.schedule_config` | 下发时间表 + 静默时段 |
|
|
139
|
+
| `parse_hhmm`、`is_in_range_minutes` | `autarkia_cadence.windows` | 支持跨零点的时钟窗口 |
|
|
140
|
+
|
|
141
|
+
推迟原因(`reason`):`cadence_no_intent`、`formal_focus_session_active`、`last_proactive_unanswered`、`cadence_global_cooldown`、`cadence_question_cooldown`、`cadence_same_topic_group`、`cadence_curious_question_daily_cap`。
|
|
142
|
+
|
|
143
|
+
### 设计说明
|
|
144
|
+
|
|
145
|
+
- **豁免要显式声明,不靠推断**:硬事件意图(`reminder`、`good_morning_proactive` 等)写在 `CadenceConfig.exempt_intents` 里;未登记的意图一律走正常节奏检查。
|
|
146
|
+
- **用话题组,不做文本相似度**:确定性分桶更便宜,也更难被绕开。
|
|
147
|
+
- **专注状态是注入的**:库里没有学习子系统,你传一个 callable;provider 抛异常按「未在专注」处理。
|
|
148
|
+
- **时间全部由外部传入**:闸门内部不读墙钟,测试是纯的。
|
|
149
|
+
|
|
150
|
+
### 系列与链接
|
|
151
|
+
|
|
152
|
+
- 项目页:<https://qishihao.icu/projects/autarkia>
|
|
153
|
+
- 运行时仓库:<https://github.com/hakituo/Autarkia>
|
|
154
|
+
- 本包:<https://github.com/hakituo/Autarkia-Cadence>
|
|
155
|
+
- 组件库路线图:<https://github.com/hakituo/Autarkia-tool/blob/main/ROADMAP.md>
|
|
156
|
+
- 作者:Shihao Qi — <https://qishihao.icu>
|
|
157
|
+
|
|
158
|
+
### 许可
|
|
159
|
+
|
|
160
|
+
MIT
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## English
|
|
165
|
+
|
|
166
|
+
Layered cooldowns, topic-group suppression, daily question caps, adaptive check intervals with equal-jitter backoff, quiet hours, and sleep-window probes — a deterministic "should it speak at all, right now" line drawn **before** any LLM call.
|
|
167
|
+
|
|
168
|
+
Extracted from the [Autarkia](https://qishihao.icu/projects/autarkia) agent runtime, where it keeps a self-hosted companion from turning into a notification machine over months of unattended operation. See the [component roadmap](https://github.com/hakituo/Autarkia-tool/blob/main/ROADMAP.md) for the rest of the series.
|
|
169
|
+
|
|
170
|
+
### Why
|
|
171
|
+
|
|
172
|
+
Once a companion can message you first, the hard question is not "what should it say" but **"should it speak at all, right now"**. Prompt instructions do not hold. This library holds a deterministic line before any LLM call:
|
|
173
|
+
|
|
174
|
+
- **Global cooldown** — minimum gap between any two proactive messages.
|
|
175
|
+
- **Question cooldown** — after asking something, don't ask again soon.
|
|
176
|
+
- **Same topic group** — "did you eat?" / "did the delivery arrive?" / "food ready?" collapse into one `meal` group, so rephrasing cannot bypass the window.
|
|
177
|
+
- **Unanswered suppression** — if the last proactive message got no reply, ordinary questions are suppressed.
|
|
178
|
+
- **Daily soft cap** — a bounded number of curious questions per day.
|
|
179
|
+
- **Focus gate** — a formal focus session hard-blocks ordinary messages; real reminders and scheduled tasks stay exempt.
|
|
180
|
+
- **Post-generation recheck** — a `share_thought` that ends with a question is treated as a question. The intent name cannot hide a follow-up.
|
|
181
|
+
|
|
182
|
+
It also ships the scheduling arithmetic around the gate: adaptive intervals driven by energy/hunger/emotion, equal-jitter non-response backoff, push-schedule/quiet-hours loading with an injectable source, clock-window helpers, goodnight probe policy, and the long-silence override gate.
|
|
183
|
+
|
|
184
|
+
Zero runtime dependencies. Pure functions and plain data; no I/O, no network, no LLM, no global config.
|
|
185
|
+
|
|
186
|
+
### Install
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
pip install autarkia-cadence
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Quick start
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
import time
|
|
196
|
+
from autarkia_cadence import CadenceGuard
|
|
197
|
+
|
|
198
|
+
guard = CadenceGuard(
|
|
199
|
+
focus_session_provider=lambda: my_focus_service.has_active_session(),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
verdict = guard.evaluate(
|
|
203
|
+
now=time.time(),
|
|
204
|
+
chosen_action="curious_question",
|
|
205
|
+
proactive_state={
|
|
206
|
+
"last_proactive_at": time.time() - 45 * 60,
|
|
207
|
+
"last_proactive_type": "share_thought",
|
|
208
|
+
"last_proactive_replied": True,
|
|
209
|
+
"last_sent_topic": "curious_question:food",
|
|
210
|
+
},
|
|
211
|
+
candidate_topic_label="curious_question:food",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if not verdict.allowed:
|
|
215
|
+
print(verdict.reason) # "cadence_same_topic_group"
|
|
216
|
+
print(verdict.retry_after_minutes) # 140
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The guard never writes state. You persist `last_proactive_*`, `last_sent_topic` and `today_sent_events`; the guard only reads them.
|
|
220
|
+
|
|
221
|
+
#### Recheck after generation
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
verdict = guard.evaluate_final_text(
|
|
225
|
+
now=time.time(),
|
|
226
|
+
intent="share_thought",
|
|
227
|
+
final_text="在炒菜。你做饭还是点外卖?",
|
|
228
|
+
proactive_state=state,
|
|
229
|
+
)
|
|
230
|
+
# blocked as a question if the previous proactive message is unanswered
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### Adaptive intervals
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from autarkia_cadence import SchedulerConfig, SchedulerLogic
|
|
237
|
+
|
|
238
|
+
scheduler = SchedulerLogic(SchedulerConfig(active_interval_seconds=300))
|
|
239
|
+
delay = scheduler.calculate_dynamic_interval(
|
|
240
|
+
bio_state={"life": {"energy": 20, "hunger": 90}},
|
|
241
|
+
emotion_state={"primary_emotion": "sad", "intensity": 0.8},
|
|
242
|
+
consecutive_non_responses=2,
|
|
243
|
+
)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
#### Sleep probes
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from autarkia_cadence import SleepPolicy
|
|
250
|
+
|
|
251
|
+
policy = SleepPolicy().resolve_sleep_probe_policy(
|
|
252
|
+
now=time.time(),
|
|
253
|
+
sleep_session_active=True,
|
|
254
|
+
allow_goodnight_probe=True,
|
|
255
|
+
reduced_mode_active=False,
|
|
256
|
+
reduced_mode_reason="none",
|
|
257
|
+
last_goodnight_ts=goodnight_ts,
|
|
258
|
+
last_goodnight_probe_ts=0,
|
|
259
|
+
default_next_check=300,
|
|
260
|
+
min_gap_seconds=600,
|
|
261
|
+
goodnight_probe_gap_seconds=7200,
|
|
262
|
+
goodnight_low_disturb_gap_seconds=7200,
|
|
263
|
+
)
|
|
264
|
+
policy["allow_send"] # first probe 10-30 minutes after goodnight
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Concepts
|
|
268
|
+
|
|
269
|
+
| Piece | Module | What it decides |
|
|
270
|
+
| --- | --- | --- |
|
|
271
|
+
| `CadenceGuard` / `CadenceVerdict` | `autarkia_cadence.guard` | allow / defer, reason, retry delay |
|
|
272
|
+
| `CadenceConfig` | `autarkia_cadence.config` | all thresholds, injected |
|
|
273
|
+
| `classify_topic`, `group_for_topic_label` | `autarkia_cadence.topics`, `.topic_groups` | topic labels and cooldown groups |
|
|
274
|
+
| `SchedulerLogic` | `autarkia_cadence.scheduling` | next check interval |
|
|
275
|
+
| `SilenceGate` / `SilenceContext` | `autarkia_cadence.silence` | force-send overrides |
|
|
276
|
+
| `SleepPolicy` | `autarkia_cadence.sleep` | goodnight probes, focus low-disturb |
|
|
277
|
+
| `ScheduleConfigLoader` | `autarkia_cadence.schedule_config` | push schedule + quiet hours |
|
|
278
|
+
| `parse_hhmm`, `is_in_range_minutes` | `autarkia_cadence.windows` | clock windows with midnight wrap |
|
|
279
|
+
|
|
280
|
+
#### Defer reasons
|
|
281
|
+
|
|
282
|
+
`cadence_no_intent`, `formal_focus_session_active`, `last_proactive_unanswered`, `cadence_global_cooldown`, `cadence_question_cooldown`, `cadence_same_topic_group`, `cadence_curious_question_daily_cap`.
|
|
283
|
+
|
|
284
|
+
### Design notes
|
|
285
|
+
|
|
286
|
+
- **Explicit exemption, not inference.** Hard-event intents (`reminder`, `good_morning_proactive`, ...) are declared in `CadenceConfig.exempt_intents`. Unknown intents go through normal cadence.
|
|
287
|
+
- **Topic groups, not text similarity.** Deterministic buckets are cheaper and harder to game than fuzzy matching.
|
|
288
|
+
- **Injected focus state.** The library has no study subsystem; pass a callable. Provider errors mean "not active".
|
|
289
|
+
- **All time is passed in.** No wall-clock reads inside the guard; tests are pure.
|
|
290
|
+
|
|
291
|
+
### The bigger picture
|
|
292
|
+
|
|
293
|
+
- Project page: <https://qishihao.icu/projects/autarkia>
|
|
294
|
+
- Runtime repository: <https://github.com/hakituo/Autarkia>
|
|
295
|
+
- This package: <https://github.com/hakituo/Autarkia-Cadence>
|
|
296
|
+
- Roadmap: <https://github.com/hakituo/Autarkia-tool/blob/main/ROADMAP.md>
|
|
297
|
+
- Author: Shihao Qi — <https://qishihao.icu>
|
|
298
|
+
|
|
299
|
+
### License
|
|
300
|
+
|
|
301
|
+
MIT
|