perceptkit 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. perceptkit/__init__.py +85 -0
  2. perceptkit/algorithms/__init__.py +40 -0
  3. perceptkit/algorithms/attribution.py +147 -0
  4. perceptkit/algorithms/glance.py +236 -0
  5. perceptkit/algorithms/history.py +663 -0
  6. perceptkit/algorithms/identity.py +43 -0
  7. perceptkit/algorithms/observation.py +44 -0
  8. perceptkit/algorithms/streaks.py +111 -0
  9. perceptkit/algorithms/trend_models.py +184 -0
  10. perceptkit/algorithms/wake.py +149 -0
  11. perceptkit/catalog.py +252 -0
  12. perceptkit/conformance/__init__.py +28 -0
  13. perceptkit/conformance/memory.py +364 -0
  14. perceptkit/conformance/report.py +170 -0
  15. perceptkit/conformance/suite.py +419 -0
  16. perceptkit/conformance/wake.py +151 -0
  17. perceptkit/contracts/__init__.py +97 -0
  18. perceptkit/contracts/_time.py +89 -0
  19. perceptkit/contracts/availability.py +77 -0
  20. perceptkit/contracts/context.py +50 -0
  21. perceptkit/contracts/delivery.py +167 -0
  22. perceptkit/contracts/errors.py +22 -0
  23. perceptkit/contracts/event.py +137 -0
  24. perceptkit/contracts/observation.py +172 -0
  25. perceptkit/contracts/receipt.py +129 -0
  26. perceptkit/contracts/records.py +367 -0
  27. perceptkit/contracts/report.py +127 -0
  28. perceptkit/contracts/versioning.py +63 -0
  29. perceptkit/fields.py +184 -0
  30. perceptkit/kit.py +223 -0
  31. perceptkit/manifest/__init__.py +57 -0
  32. perceptkit/manifest/checks.py +323 -0
  33. perceptkit/manifest/mapping.py +96 -0
  34. perceptkit/manifest/minimal.py +1282 -0
  35. perceptkit/manifest/types.py +211 -0
  36. perceptkit/manifest/units.py +84 -0
  37. perceptkit/ports/__init__.py +19 -0
  38. perceptkit/ports/storage.py +288 -0
  39. perceptkit/ports/wake.py +43 -0
  40. perceptkit/processing/__init__.py +49 -0
  41. perceptkit/processing/aggregate.py +80 -0
  42. perceptkit/processing/dispatch.py +356 -0
  43. perceptkit/processing/normalize.py +458 -0
  44. perceptkit/processing/pipeline.py +406 -0
  45. perceptkit/processing/recompute.py +170 -0
  46. perceptkit/processing/recurrence.py +166 -0
  47. perceptkit/processing/scheduled.py +233 -0
  48. perceptkit/prompts.py +75 -0
  49. perceptkit/queries/__init__.py +32 -0
  50. perceptkit/queries/api.py +457 -0
  51. perceptkit/retention.py +84 -0
  52. perceptkit/rules/__init__.py +19 -0
  53. perceptkit/rules/engine.py +112 -0
  54. perceptkit/rules/evaluators.py +228 -0
  55. perceptkit/rules/types.py +236 -0
  56. perceptkit-0.2.2.dist-info/METADATA +439 -0
  57. perceptkit-0.2.2.dist-info/RECORD +59 -0
  58. perceptkit-0.2.2.dist-info/WHEEL +4 -0
  59. perceptkit-0.2.2.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,166 @@
1
+ """重复日程的滚动展开。
2
+
3
+ 产品规范 §12:「Calendar 无限重复事件 | 保存 rule/series,并滚动展开查询窗口」。
4
+ 先前只有 ``recurrence_identity`` 这个字段,没有展开逻辑 —— 于是"每周一的例会"
5
+ 要么被展开到无限未来存进库,要么就查不到。
6
+
7
+ ## 为什么只做一个子集
8
+
9
+ 完整的 RFC 5545 RRULE 是一整个库的量(BYSETPOS、EXDATE、跨夏令时的
10
+ BYHOUR、闰月…),而这个包是零依赖的。硬做等于在包里重新实现一个日历库,
11
+ **而重复日程算错日期是那种「看起来完全正常」的错**:用户会准时出现在
12
+ 一个不存在的会议上。
13
+
14
+ 所以这里只认一个小而确定的子集,**不认识的一律明确拒绝**,
15
+ 由宿主自己展开好再传进来。拒绝是安全的,猜不是。
16
+
17
+ 认 每天 / 每周(可指定星期几)/ 每月同一天,带 interval、until、count
18
+ 不认 BYSETPOS(「每月第二个周二」)、EXDATE(「除了这几天」)、
19
+ 按年、以及任何我们没把握的写法
20
+ """
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+ from datetime import date, datetime, timedelta
25
+ from typing import Any, Mapping
26
+
27
+ #: 一次展开最多产出多少条。**必须有上限** —— "每天,永不结束"配上一个
28
+ #: 十年的查询窗口就是三千多条直接塞进模型上下文。
29
+ MAX_INSTANCES = 200
30
+
31
+ SUPPORTED_FREQ = ("daily", "weekly", "monthly")
32
+
33
+ #: 我们明确不支持的写法。列出来是为了让拒绝的理由能说清是哪一条,
34
+ #: 而不是笼统一句"不支持"。
35
+ UNSUPPORTED_KEYS = {
36
+ "bysetpos": "「每月第二个周二」这类按序号选的规则",
37
+ "exdate": "「除了这几天」的例外列表",
38
+ "byyearday": "按一年中的第几天",
39
+ "byweekno": "按第几周",
40
+ }
41
+
42
+
43
+ class RecurrenceUnsupported(ValueError):
44
+ """这条重复规则我们不认识。**拒绝,不猜。**"""
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class RecurrenceRule:
49
+ """一条重复规则的最小描述。"""
50
+
51
+ freq: str
52
+ interval: int = 1
53
+ #: 每周重复时指定星期几,0=周一。空 = 跟着起始日那天。
54
+ byweekday: tuple[int, ...] = ()
55
+ #: 到这天为止(含)。``None`` = 无限重复。
56
+ until: date | None = None
57
+ #: 最多重复多少次。``None`` = 不限次数。
58
+ count: int | None = None
59
+
60
+ @classmethod
61
+ def parse(cls, raw: Mapping[str, Any]) -> "RecurrenceRule":
62
+ bad = [k for k in raw if k.lower() in UNSUPPORTED_KEYS]
63
+ if bad:
64
+ why = "、".join(UNSUPPORTED_KEYS[k.lower()] for k in bad)
65
+ raise RecurrenceUnsupported(
66
+ f"这条规则用到了{why},我们不展开它。"
67
+ "算错重复日程的日期是那种「看起来完全正常」的错 —— "
68
+ "用户会准时出现在一个不存在的会议上。请宿主自己展开好再传进来"
69
+ )
70
+ freq = str(raw.get("freq", "")).lower()
71
+ if freq not in SUPPORTED_FREQ:
72
+ raise RecurrenceUnsupported(
73
+ f"freq={raw.get('freq')!r} 不在 {list(SUPPORTED_FREQ)} 里"
74
+ )
75
+ # 刻意不写 `raw.get("interval", 1) or 1` —— 那会把 interval=0
76
+ # 静默变成 1,正是"不许猜"要防的那种改写。
77
+ raw_interval = raw.get("interval")
78
+ interval = 1 if raw_interval is None else int(raw_interval)
79
+ if interval < 1:
80
+ raise RecurrenceUnsupported(
81
+ f"interval={interval} 必须 >= 1(0 或负数没有意义,"
82
+ "而把它当成 1 就是替上游改写了规则)"
83
+ )
84
+
85
+ until = raw.get("until")
86
+ if isinstance(until, datetime):
87
+ until = until.date()
88
+ elif isinstance(until, str):
89
+ until = date.fromisoformat(until[:10])
90
+
91
+ return cls(
92
+ freq=freq, interval=interval,
93
+ byweekday=tuple(int(d) for d in (raw.get("byweekday") or ())),
94
+ until=until,
95
+ count=int(raw["count"]) if raw.get("count") is not None else None,
96
+ )
97
+
98
+
99
+ def _step(current: date, rule: RecurrenceRule) -> date:
100
+ if rule.freq == "daily":
101
+ return current + timedelta(days=rule.interval)
102
+ if rule.freq == "weekly":
103
+ return current + timedelta(weeks=rule.interval)
104
+ # monthly:同一天。**遇到没有那一天的月份就跳过**(1 月 31 日 → 2 月没有
105
+ # 31 号)。不做"退回月末",那是另一种语义,各家日历还不一致。
106
+ y, m = current.year, current.month + rule.interval
107
+ y += (m - 1) // 12
108
+ m = (m - 1) % 12 + 1
109
+ try:
110
+ return current.replace(year=y, month=m)
111
+ except ValueError:
112
+ return date(y, m, 28) + timedelta(days=4) # 落到下个月,下轮再跳
113
+
114
+
115
+ def expand(
116
+ start: datetime, rule: RecurrenceRule, *,
117
+ window_start: date, window_end: date,
118
+ max_instances: int = MAX_INSTANCES,
119
+ ) -> list[datetime]:
120
+ """把一条重复规则在给定窗口内展开成具体的发生时刻。
121
+
122
+ **保留原始的时刻和时区** —— 只挪日期,不碰时分和 offset。
123
+ 每周一早上十点的会,跨夏令时之后仍然是"本地时间早上十点"。
124
+ """
125
+ out: list[datetime] = []
126
+ if window_end < window_start:
127
+ return out
128
+
129
+ day = start.date()
130
+ emitted = 0
131
+ # 无限重复配上一个很远的窗口,光是走到窗口起点就可能要几万步。
132
+ # 步数上限按窗口跨度 + 已产出条数给,够用且不会失控。
133
+ budget = (window_end - window_start).days + max_instances * 2 + 366
134
+
135
+ weekdays = set(rule.byweekday) if rule.freq == "weekly" and rule.byweekday else None
136
+ # 「每两周的周一」要按【周】数间隔,不是按天。以起始日那一周的周一为基准,
137
+ # 只有相隔整数个 interval 周的那些周才算数 —— 按天走会把每周都算上。
138
+ week0 = start.date() - timedelta(days=start.date().weekday())
139
+
140
+ while budget > 0 and day <= window_end:
141
+ budget -= 1
142
+ if rule.until is not None and day > rule.until:
143
+ break
144
+ if rule.count is not None and emitted >= rule.count:
145
+ break
146
+
147
+ hit = day >= start.date()
148
+ if hit and weekdays is not None:
149
+ weeks_apart = ((day - timedelta(days=day.weekday())) - week0).days // 7
150
+ hit = day.weekday() in weekdays and weeks_apart % rule.interval == 0
151
+ if hit:
152
+ emitted += 1
153
+ if day >= window_start:
154
+ out.append(datetime.combine(day, start.timetz()))
155
+ if len(out) >= max_instances:
156
+ break
157
+
158
+ day = (day + timedelta(days=1)) if weekdays is not None else _step(day, rule)
159
+
160
+ return out
161
+
162
+
163
+ __all__ = [
164
+ "MAX_INSTANCES", "SUPPORTED_FREQ", "UNSUPPORTED_KEYS",
165
+ "RecurrenceUnsupported", "RecurrenceRule", "expand",
166
+ ]
@@ -0,0 +1,233 @@
1
+ """由时钟驱动的两种规则 —— 主管线跑不到它们。
2
+
3
+ 主管线是**数据驱动**的:有观测进来才跑。这对九种规则里的七种都对,
4
+ 但另外两种不行:
5
+
6
+ streak "连续三天睡眠不足" —— 要读历史,而且【一天只该判一次】
7
+ 前台每 30 秒一条观测,跟着观测跑就是一天几千次,
8
+ 而这件事一天只可能变化一次。
9
+ → 挂在【某天的聚合算完】那一刻
10
+
11
+ absence "你三天没记录体重了" —— **没有数据才该触发**
12
+ 跟着观测跑的话,它永远等不到自己被调用的那一刻。
13
+ → 由定时器驱动
14
+
15
+ **宿主不用为此多起一个东西。** 投递那条线本来就需要一个定时循环
16
+ (``dispatch_pending`` 得有人定期调),这两个搭在同一个循环上就行。
17
+
18
+ ⚠️ 产品规范把 ``absence`` 列进了内置规则,但**整份文档没说它由谁驱动** ——
19
+ 其他八种都是"数据来了 → 判断",只有它是"数据没来 → 判断"。这是规范的一处缺口。
20
+ """
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass, field
24
+ from datetime import date, datetime, timedelta
25
+ from typing import Any, Callable, Mapping, Sequence
26
+
27
+ from ..contracts.context import IngestContext
28
+ from ..contracts.event import PerceptionEvent
29
+ from ..contracts.records import StoredObservation
30
+ from ..manifest.types import SignalDefinition
31
+ from ..ports.storage import StoragePort
32
+ from ..rules.types import EventDefinition, RuleResult
33
+ from .dispatch import RuleOutcome, definitions_for_signal, evaluate_and_enqueue
34
+ from .normalize import NormalizedObservation
35
+
36
+ #: 算连续天数时最多往回看多少天。不设上限的话,一条"连续 N 天"的规则
37
+ #: 会在每次日聚合时把整个历史读一遍。
38
+ MAX_STREAK_LOOKBACK_DAYS = 90
39
+
40
+ _OPS: dict[str, Callable[[float, float], bool]] = {
41
+ "gte": lambda a, b: a >= b,
42
+ "gt": lambda a, b: a > b,
43
+ "lte": lambda a, b: a <= b,
44
+ "lt": lambda a, b: a < b,
45
+ }
46
+
47
+
48
+ @dataclass
49
+ class ScheduledOutcome:
50
+ events: list[PerceptionEvent] = field(default_factory=list)
51
+ misses: list[tuple[str, str | None]] = field(default_factory=list)
52
+
53
+
54
+ def _daily_value(doc: Mapping[str, Any], field_key: str, strategy: str) -> float | None:
55
+ """从一天的聚合文档里取出那个字段的代表数字。
56
+
57
+ 不同聚合方式的文档形状不一样 —— 这里只处理数值型的那几种,
58
+ 其余返回 ``None``(比如"按状态分时长"就没有单一代表值)。
59
+ """
60
+ cell = doc.get(field_key)
61
+ if isinstance(cell, (int, float)) and not isinstance(cell, bool):
62
+ return float(cell)
63
+ if not isinstance(cell, Mapping):
64
+ return None
65
+ if strategy in ("daily_total", "cumulative"):
66
+ raw = cell.get("total")
67
+ elif strategy == "numeric_dist":
68
+ # 用平均值当代表:min/max 太容易被单次异常读数带偏。
69
+ if cell.get("count"):
70
+ raw = cell["sum"] / cell["count"]
71
+ else:
72
+ raw = None
73
+ else:
74
+ raw = cell.get("value")
75
+ return float(raw) if isinstance(raw, (int, float)) else None
76
+
77
+
78
+ def streak_length(
79
+ storage: StoragePort,
80
+ definition: EventDefinition,
81
+ signal: SignalDefinition,
82
+ *,
83
+ subject_id: str,
84
+ through: date,
85
+ max_days: int = MAX_STREAK_LOOKBACK_DAYS,
86
+ ) -> int:
87
+ """从 ``through`` 往回数,连续多少天满足这条规则的**每天条件**。
88
+
89
+ **缺数据的那天直接断掉,不跳过。** "连续三天睡眠不足"里如果有一天没戴表,
90
+ 那就不是连续三天 —— 把缺失当成"满足"或"跳过"都是在替用户编事实。
91
+ """
92
+ fd = signal.field_map().get(definition.field_name or "")
93
+ if fd is None:
94
+ return 0
95
+ op = _OPS.get(definition.operator or "lt")
96
+ threshold = definition.value
97
+ if op is None or not isinstance(threshold, (int, float)):
98
+ return 0
99
+
100
+ start = through - timedelta(days=max_days - 1)
101
+ by_day = {
102
+ a.local_date: a.typed_aggregate
103
+ for a in storage.get_aggregate(
104
+ subject_id=subject_id, signal=signal.key,
105
+ start_date=start, end_date=through, aggregation_kind="daily",
106
+ )
107
+ }
108
+
109
+ count = 0
110
+ day = through
111
+ while day >= start:
112
+ doc = by_day.get(day)
113
+ if doc is None:
114
+ break # 那天没数据 —— 连续断了
115
+ value = _daily_value(doc, definition.field_name or "", fd.aggregation_strategy)
116
+ if value is None or not op(value, float(threshold)):
117
+ break
118
+ count += 1
119
+ day -= timedelta(days=1)
120
+ return count
121
+
122
+
123
+ def _fake_observation(
124
+ signal: SignalDefinition, *, subject_id: str, when: datetime, day: date,
125
+ ) -> NormalizedObservation:
126
+ """给 ``evaluate_and_enqueue`` 造一个载体。
127
+
128
+ 定时求值没有真实观测(这正是它存在的理由),但事件仍然要落到某个
129
+ signal / 某一天上。造一条**不落库**的载体,只用来把上下文传下去。
130
+ """
131
+ stored = StoredObservation(
132
+ observation_id=f"scheduled:{signal.key}:{day.isoformat()}",
133
+ subject_id=subject_id, signal=signal.key, signal_schema_version=signal.schema_version,
134
+ source="scheduler", occurred_at=when, received_at=when,
135
+ availability="observed", effective_local_date=day, typed_value={},
136
+ )
137
+ return NormalizedObservation(
138
+ stored=stored, identity_digest=stored.observation_id,
139
+ fact_key=stored.observation_id, content_digest="",
140
+ )
141
+
142
+
143
+ def evaluate_daily(
144
+ *,
145
+ storage: StoragePort,
146
+ subject_id: str,
147
+ local_date: date,
148
+ now: datetime,
149
+ signals: Mapping[str, SignalDefinition],
150
+ definitions: Sequence[EventDefinition],
151
+ extra_evaluators: Mapping[str, Callable[..., RuleResult]] | None = None,
152
+ ) -> ScheduledOutcome:
153
+ """某一天的聚合算完之后调一次,跑 ``streak`` 这类按天判的规则。
154
+
155
+ 宿主在跨日时调用(每个 subject 一次),不要跟着观测跑。
156
+ """
157
+ outcome = ScheduledOutcome()
158
+ context = IngestContext(subject_id=subject_id, received_at=now)
159
+
160
+ for definition in definitions:
161
+ if definition.condition_type != "streak" or not definition.enabled:
162
+ continue
163
+ signal = signals.get(definition.signal)
164
+ if signal is None:
165
+ continue
166
+ length = streak_length(
167
+ storage, definition, signal, subject_id=subject_id, through=local_date,
168
+ )
169
+ item = _fake_observation(signal, subject_id=subject_id, when=now, day=local_date)
170
+ rules: RuleOutcome = evaluate_and_enqueue(
171
+ item, context=context, storage=storage, definitions=[definition],
172
+ extra_evaluators=extra_evaluators,
173
+ extra_context={"streak_length": length},
174
+ signal_definition=signal,
175
+ )
176
+ outcome.events.extend(rules.events)
177
+ outcome.misses.extend(rules.misses)
178
+ return outcome
179
+
180
+
181
+ def evaluate_absence(
182
+ *,
183
+ storage: StoragePort,
184
+ subject_id: str,
185
+ now: datetime,
186
+ signals: Mapping[str, SignalDefinition],
187
+ definitions: Sequence[EventDefinition],
188
+ extra_evaluators: Mapping[str, Callable[..., RuleResult]] | None = None,
189
+ ) -> ScheduledOutcome:
190
+ """定时调,跑 ``absence``(该来的没来)这类规则。
191
+
192
+ **静默时长从当前值的观测时刻算起** —— 用当前值而不是翻观测明细,
193
+ 因为明细可能已经按保留期清理掉了,而当前值一定还在。
194
+ """
195
+ outcome = ScheduledOutcome()
196
+ context = IngestContext(subject_id=subject_id, received_at=now)
197
+
198
+ for definition in definitions:
199
+ if definition.condition_type != "absence" or not definition.enabled:
200
+ continue
201
+ signal = signals.get(definition.signal)
202
+ if signal is None:
203
+ continue
204
+
205
+ projections = storage.get_current(
206
+ subject_id=subject_id, signals=[definition.signal]
207
+ ).get(definition.signal) or []
208
+ if not projections:
209
+ # 从来没有过数据。**不触发** —— "你三天没记录体重了"对一个
210
+ # 从没记过体重的用户来说是句莫名其妙的话。
211
+ outcome.misses.append((definition.definition_id, "这个信号从来没有过数据"))
212
+ continue
213
+
214
+ last = max(p.observed_at for p in projections)
215
+ silent = (now - last).total_seconds()
216
+ item = _fake_observation(
217
+ signal, subject_id=subject_id, when=now, day=now.date(),
218
+ )
219
+ rules = evaluate_and_enqueue(
220
+ item, context=context, storage=storage, definitions=[definition],
221
+ extra_evaluators=extra_evaluators,
222
+ extra_context={"silent_seconds": silent},
223
+ signal_definition=signal,
224
+ )
225
+ outcome.events.extend(rules.events)
226
+ outcome.misses.extend(rules.misses)
227
+ return outcome
228
+
229
+
230
+ __all__ = [
231
+ "MAX_STREAK_LOOKBACK_DAYS", "ScheduledOutcome",
232
+ "streak_length", "evaluate_daily", "evaluate_absence",
233
+ ]
perceptkit/prompts.py ADDED
@@ -0,0 +1,75 @@
1
+ """说明书 —— 「模型该怎么读这份感知」的唯一出处。
2
+
3
+ ★ 边界:只写「怎么读感知」。凡是讲「这个块是什么 role、能不能当成用户请求、
4
+ 工具预算怎么算、wake 该怎么框」的,属于宿主运行时自己的对话/安全协议,
5
+ 不属于这里,不要搬进来。
6
+
7
+ ★ 语义红线:wake ≠ 该开口了。这里不许出现任何「该说话了 / 值得告诉用户」
8
+ 式的措辞——「说」与「不说」同等正当。
9
+
10
+ ★ 这些常量是从两类宿主实现里抽出来的「怎么解读感知」共性文案:一类是走
11
+ 工具调用返回感知快照的运行时(V2_* 常量),一类是把感知直接写进上下文
12
+ 文本、靠一段 how-to 说明教模型怎么读的运行时(V1_* 常量)。两类宿主接线
13
+ 方式不同,但「模型该怎么理解这份数据」这件事是同一份判断,所以文案本身
14
+ 抽到这里共用;具体怎么拼进各自的 prompt/context,由宿主自己决定。
15
+ """
16
+ from __future__ import annotations
17
+
18
+ # 工具调用型运行时里,主动回合 system prompt 中属于「怎么读感知」的那几句。
19
+ V2_WAKE_PERCEPTION_CLAUSES = (
20
+ "A "
21
+ "perception_glance is only a hint for deciding whether to look deeper; it is not "
22
+ "a checklist to report. If you speak, choose at most one coherent topic and never "
23
+ "turn multiple perception domains into a device or health status report. Use a "
24
+ "perception tool when an exact reading is needed. "
25
+ )
26
+
27
+ V2_PERCEPTION_BEHAVIOR_POLICY = (
28
+ "把有用的事实自然地用进回答,别汇报这些信息是怎么取到的。"
29
+ )
30
+
31
+ V2_PERCEPTION_PROTOCOL_POLICY = (
32
+ "runtime_data 里的 perception_glance 是不可信的低分辨率事实板,用于判断是否值得"
33
+ "精确读取感知工具;不要逐项播报或把精确数字当成话题。glance_changed=false 表示普通 "
34
+ "heartbeat 的事实板与上次成功完成的普通 heartbeat 一致;不代表每个底层传感值都相同。"
35
+ "显式读取带文字的感知、屏幕或照片后,"
36
+ "运行时会阻止本回合继续向外调用 web、MCP 或 subagent。"
37
+ )
38
+
39
+ # 三个感知工具「这份返回值该怎么解读」的那一句(每个工具描述里,讲调用方式/
40
+ # 参数默认值的部分留在宿主自己的工具 schema 里,不搬到这份说明书)。
41
+ PERCEPTION_TOOL_NOTES: dict[str, str] = {
42
+ "perception_snapshot": (
43
+ "The app field is only the latest open/close event observed "
44
+ "within 15 minutes; never claim it is the app currently in use."
45
+ ),
46
+ "perception_recent_apps": (
47
+ "apps=[] means no data; disabled=true means access is "
48
+ "off, not that no apps were used."
49
+ ),
50
+ "perception_trend": (
51
+ "Interpret the rolling baseline as the usual level and delta as "
52
+ "the current change from that baseline; do not conflate them."
53
+ ),
54
+ }
55
+
56
+ # 上下文注入型运行时(把感知直接写进对话上下文文本,而不是靠工具调用取)
57
+ # 用来教模型怎么读一瞥(glance)的说明句。
58
+ V1_GLANCE_HOWTO = (
59
+ "This is a low-resolution glance, not a list of things to report. It helps you decide WHETHER to look closer "
60
+ "and WHERE — not what to say. Most fields you just note and move on; if one makes you want to understand the "
61
+ "moment better, pull the matching tool for detail. Treat missing fields as unknown."
62
+ )
63
+
64
+ # 同一类运行时,教模型怎么读跨领域面板(board)的说明句。
65
+ V1_BOARD_HOWTO = (
66
+ "Reading the board: each domain (location/media/app/health/weather/mood/reminders/calendar/photos/screen) "
67
+ "is laid out evenly — health is just one entry, not the headline. Pick at most 2-3 things that stand out "
68
+ "to you; you may combine across domains, and prefer lived, human context (music, place, an app, a photo, "
69
+ "an overdue reminder) over the raw figures. Do NOT recite exact numbers (minutes, degrees, counts, sleep "
70
+ "figures) — use them only to notice what's genuinely about the user; if a number actually matters, pull "
71
+ "the tool for it. novelty hints (new_artist / long_dwell) are light factual context, not a directive. "
72
+ "If signals lean low or vulnerable (late hour, sad music, poor sleep), be lighter, not heavier — don't "
73
+ "diagnose, don't stack worries; one warm, light touch is enough. If nothing stands out, staying quiet is "
74
+ "equally fine."
75
+ )
@@ -0,0 +1,32 @@
1
+ """读取侧 —— agent 主动来查的八个函数。
2
+
3
+ 和写入侧方向相反、共用同一份存储。**不是转发器**:里面装着 TTL 判定、
4
+ 趋势模型选择、缺数据显式化、隐私投影四样每个宿主都必须一样的逻辑。
5
+
6
+ MCP 工具那层(工具名、描述、给谁开、返回怎么写)留在宿主 —— 全是产品决策。
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from .api import (
11
+ DEFAULT_LIMIT,
12
+ MAX_LIMIT,
13
+ CurrentView,
14
+ DailyView,
15
+ get_current,
16
+ get_daily_aggregates,
17
+ get_last_known,
18
+ get_trend,
19
+ list_calendar_events,
20
+ list_events,
21
+ list_reminders,
22
+ list_timeline,
23
+ project,
24
+ visible_fields,
25
+ )
26
+
27
+ __all__ = [
28
+ "get_current", "get_last_known", "list_timeline", "get_daily_aggregates",
29
+ "get_trend", "list_calendar_events", "list_reminders", "list_events",
30
+ "CurrentView", "DailyView", "visible_fields", "project",
31
+ "DEFAULT_LIMIT", "MAX_LIMIT",
32
+ ]