dsh-plugin-xujie-memory 0.5.0 → 0.6.0
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.
- package/CHANGELOG.md +28 -1
- package/LICENSE +21 -21
- package/README.md +33 -8
- package/README_EN.md +27 -4
- package/cordis.patch.yml +49 -49
- package/lib/index.js +121 -0
- package/package.json +1 -1
- package/python/specs/characters/aloof.json +14 -14
- package/python/specs/characters/gentle.json +14 -14
- package/python/specs/characters/tsundere.json +14 -14
- package/python/specs/debt.v1.json +31 -31
- package/python/specs/nickname.v1.json +16 -16
- package/python/specs/preference.v1.json +16 -16
- package/python/xujie_memory/admission.py +211 -211
- package/python/xujie_memory/audit.py +377 -377
- package/python/xujie_memory/benchmark.py +281 -281
- package/python/xujie_memory/character.py +199 -199
- package/python/xujie_memory/console.py +41 -41
- package/python/xujie_memory/consolidate.py +356 -356
- package/python/xujie_memory/due.py +121 -121
- package/python/xujie_memory/emotion.py +573 -573
- package/python/xujie_memory/errors.py +98 -98
- package/python/xujie_memory/importance.py +104 -104
- package/python/xujie_memory/judge.py +350 -350
- package/python/xujie_memory/memory.py +55 -0
- package/python/xujie_memory/orchestrator.py +690 -666
- package/python/xujie_memory/phrasing.py +292 -292
- package/python/xujie_memory/server.py +24 -0
- package/python/xujie_memory/specs.py +170 -170
- package/python/xujie_memory/summary.py +238 -238
|
@@ -1,170 +1,170 @@
|
|
|
1
|
-
"""关系规格(RelationSpec)。
|
|
2
|
-
|
|
3
|
-
设计文档 7.1 的理由:哪些关系可自动接受、负值是否允许、谁能写,
|
|
4
|
-
不能由模型临时决定,也不应散落在代码里。每种关系用一份版本化规格声明,
|
|
5
|
-
内核只执行规格,不自行推断。
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
from dataclasses import dataclass, field
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import Any
|
|
14
|
-
|
|
15
|
-
from .errors import SpecNotFound
|
|
16
|
-
|
|
17
|
-
RISK_TIERS = ("R0", "R1", "R2", "R3")
|
|
18
|
-
ADMISSION_MODES = ("shadow", "assisted", "auto")
|
|
19
|
-
EFFECT_KINDS = ("set", "delta", "invalidate")
|
|
20
|
-
MODALITIES = (
|
|
21
|
-
"asserted",
|
|
22
|
-
# 请求 / 祈使。「给我免三碗」不是在陈述事实,也不是假设——
|
|
23
|
-
# 说话人是真的在要求这件事。缺了这一格,它会被判成 hypothetical
|
|
24
|
-
# 然后 record_only,用户明确要求的事反而不记(实测踩到)。
|
|
25
|
-
"request",
|
|
26
|
-
"hypothetical",
|
|
27
|
-
"conditional",
|
|
28
|
-
"quoted",
|
|
29
|
-
"denied",
|
|
30
|
-
"planned",
|
|
31
|
-
"joking",
|
|
32
|
-
"ironic",
|
|
33
|
-
"in_world",
|
|
34
|
-
"unclear",
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@dataclass(frozen=True)
|
|
39
|
-
class RelationSpec:
|
|
40
|
-
relation: str
|
|
41
|
-
version: str
|
|
42
|
-
risk_tier: str
|
|
43
|
-
required_slots: tuple[str, ...]
|
|
44
|
-
allowed_modalities: tuple[str, ...]
|
|
45
|
-
writers: tuple[str, ...]
|
|
46
|
-
effect_kind: str
|
|
47
|
-
admission_mode: str = "shadow"
|
|
48
|
-
value_type: str = "integer" # integer / number / string / enum
|
|
49
|
-
# XM-12: unit 只用于展示,不参与 state_key,也不参与校验。
|
|
50
|
-
# 编码器给出与规格不符的单位时不会报错——这是有意的取舍,见 ISSUES。
|
|
51
|
-
# 真要校验,应判 disputed 而不是拒绝(拒绝会丢掉证据)。
|
|
52
|
-
unit: str | None = None
|
|
53
|
-
allow_negative: bool = False
|
|
54
|
-
enum_values: tuple[str, ...] = ()
|
|
55
|
-
confirmation: str = "none" # none / implicit / explicit / authority
|
|
56
|
-
validity: str = "permanent" # permanent / ttl / reconfirm
|
|
57
|
-
ttl_seconds: int | None = None
|
|
58
|
-
conflict_policy: str = "dispute" # dispute / reject / last_write_wins
|
|
59
|
-
conservation: str | None = None # None / "repayment":追踪累计偿付量
|
|
60
|
-
feeds: tuple[str, ...] = ()
|
|
61
|
-
derived_from: tuple[str, ...] = ()
|
|
62
|
-
note: str = ""
|
|
63
|
-
|
|
64
|
-
# ------------------------------------------------------------- 自检
|
|
65
|
-
def validate(self) -> None:
|
|
66
|
-
if self.risk_tier not in RISK_TIERS:
|
|
67
|
-
raise ValueError(f"{self.relation}: bad risk_tier {self.risk_tier!r}")
|
|
68
|
-
if self.admission_mode not in ADMISSION_MODES:
|
|
69
|
-
raise ValueError(f"{self.relation}: bad admission_mode {self.admission_mode!r}")
|
|
70
|
-
if self.effect_kind not in EFFECT_KINDS:
|
|
71
|
-
raise ValueError(f"{self.relation}: bad effect_kind {self.effect_kind!r}")
|
|
72
|
-
for m in self.allowed_modalities:
|
|
73
|
-
if m not in MODALITIES:
|
|
74
|
-
raise ValueError(f"{self.relation}: unknown modality {m!r}")
|
|
75
|
-
if not self.required_slots:
|
|
76
|
-
raise ValueError(f"{self.relation}: required_slots must not be empty")
|
|
77
|
-
# R3 永远不允许自动提交(设计文档 7.3 / 15.5)
|
|
78
|
-
if self.risk_tier == "R3" and self.admission_mode == "auto":
|
|
79
|
-
raise ValueError(
|
|
80
|
-
f"{self.relation}: R3 relation must not be 'auto' "
|
|
81
|
-
"(permissions/deletion go through the control channel only)"
|
|
82
|
-
)
|
|
83
|
-
if self.value_type == "enum" and not self.enum_values:
|
|
84
|
-
raise ValueError(f"{self.relation}: enum value_type requires enum_values")
|
|
85
|
-
if self.conservation not in (None, "repayment"):
|
|
86
|
-
raise ValueError(f"{self.relation}: unknown conservation {self.conservation!r}")
|
|
87
|
-
|
|
88
|
-
@property
|
|
89
|
-
def spec_version(self) -> str:
|
|
90
|
-
return f"{self.relation}@{self.version}"
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
@dataclass
|
|
94
|
-
class SpecRegistry:
|
|
95
|
-
_by_relation: dict[str, RelationSpec] = field(default_factory=dict)
|
|
96
|
-
|
|
97
|
-
# ------------------------------------------------------------- 加载
|
|
98
|
-
@classmethod
|
|
99
|
-
def from_dir(cls, path: str | Path) -> "SpecRegistry":
|
|
100
|
-
reg = cls()
|
|
101
|
-
for f in sorted(Path(path).glob("*.json")):
|
|
102
|
-
reg.add(RelationSpec(**json.loads(f.read_text(encoding="utf-8"))))
|
|
103
|
-
return reg
|
|
104
|
-
|
|
105
|
-
@classmethod
|
|
106
|
-
def from_specs(cls, specs: list[RelationSpec]) -> "SpecRegistry":
|
|
107
|
-
reg = cls()
|
|
108
|
-
for s in specs:
|
|
109
|
-
reg.add(s)
|
|
110
|
-
return reg
|
|
111
|
-
|
|
112
|
-
def add(self, spec: RelationSpec) -> None:
|
|
113
|
-
spec.validate()
|
|
114
|
-
if spec.relation in self._by_relation:
|
|
115
|
-
raise ValueError(f"duplicate relation spec: {spec.relation}")
|
|
116
|
-
self._by_relation[spec.relation] = spec
|
|
117
|
-
|
|
118
|
-
# ------------------------------------------------------------- 查询
|
|
119
|
-
def get(self, relation: str) -> RelationSpec:
|
|
120
|
-
try:
|
|
121
|
-
return self._by_relation[relation]
|
|
122
|
-
except KeyError:
|
|
123
|
-
raise SpecNotFound(f"no spec registered for relation {relation!r}") from None
|
|
124
|
-
|
|
125
|
-
def all(self) -> list[RelationSpec]:
|
|
126
|
-
return list(self._by_relation.values())
|
|
127
|
-
|
|
128
|
-
# ------------------------------------------------------------- 依赖图
|
|
129
|
-
def dependency_edges(self) -> list[tuple[str, str]]:
|
|
130
|
-
"""返回 (input_relation, derived_relation) 边列表,用于环检测。"""
|
|
131
|
-
edges: list[tuple[str, str]] = []
|
|
132
|
-
for spec in self._by_relation.values():
|
|
133
|
-
for src in spec.derived_from:
|
|
134
|
-
edges.append((src, spec.relation))
|
|
135
|
-
return edges
|
|
136
|
-
|
|
137
|
-
def assert_acyclic(self) -> None:
|
|
138
|
-
"""依赖图必须为 DAG(设计文档 9.5)。"""
|
|
139
|
-
from .errors import DependencyCycle
|
|
140
|
-
|
|
141
|
-
graph: dict[str, set[str]] = {}
|
|
142
|
-
for src, dst in self.dependency_edges():
|
|
143
|
-
graph.setdefault(src, set()).add(dst)
|
|
144
|
-
|
|
145
|
-
WHITE, GREY, BLACK = 0, 1, 2
|
|
146
|
-
color: dict[str, int] = {}
|
|
147
|
-
|
|
148
|
-
def visit(node: str, stack: list[str]) -> None:
|
|
149
|
-
color[node] = GREY
|
|
150
|
-
stack.append(node)
|
|
151
|
-
for nxt in graph.get(node, ()):
|
|
152
|
-
c = color.get(nxt, WHITE)
|
|
153
|
-
if c == GREY:
|
|
154
|
-
cycle = " -> ".join(stack[stack.index(nxt):] + [nxt])
|
|
155
|
-
raise DependencyCycle(f"dependency cycle: {cycle}")
|
|
156
|
-
if c == WHITE:
|
|
157
|
-
visit(nxt, stack)
|
|
158
|
-
stack.pop()
|
|
159
|
-
color[node] = BLACK
|
|
160
|
-
|
|
161
|
-
for node in list(graph):
|
|
162
|
-
if color.get(node, WHITE) == WHITE:
|
|
163
|
-
visit(node, [])
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def slot_value(spec: RelationSpec, slots: dict[str, Any], name: str) -> Any:
|
|
167
|
-
"""取槽位;缺失时抛错,不做默认值填充(I5)。"""
|
|
168
|
-
if name not in slots:
|
|
169
|
-
raise KeyError(f"missing required slot {name!r} for relation {spec.relation!r}")
|
|
170
|
-
return slots[name]
|
|
1
|
+
"""关系规格(RelationSpec)。
|
|
2
|
+
|
|
3
|
+
设计文档 7.1 的理由:哪些关系可自动接受、负值是否允许、谁能写,
|
|
4
|
+
不能由模型临时决定,也不应散落在代码里。每种关系用一份版本化规格声明,
|
|
5
|
+
内核只执行规格,不自行推断。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .errors import SpecNotFound
|
|
16
|
+
|
|
17
|
+
RISK_TIERS = ("R0", "R1", "R2", "R3")
|
|
18
|
+
ADMISSION_MODES = ("shadow", "assisted", "auto")
|
|
19
|
+
EFFECT_KINDS = ("set", "delta", "invalidate")
|
|
20
|
+
MODALITIES = (
|
|
21
|
+
"asserted",
|
|
22
|
+
# 请求 / 祈使。「给我免三碗」不是在陈述事实,也不是假设——
|
|
23
|
+
# 说话人是真的在要求这件事。缺了这一格,它会被判成 hypothetical
|
|
24
|
+
# 然后 record_only,用户明确要求的事反而不记(实测踩到)。
|
|
25
|
+
"request",
|
|
26
|
+
"hypothetical",
|
|
27
|
+
"conditional",
|
|
28
|
+
"quoted",
|
|
29
|
+
"denied",
|
|
30
|
+
"planned",
|
|
31
|
+
"joking",
|
|
32
|
+
"ironic",
|
|
33
|
+
"in_world",
|
|
34
|
+
"unclear",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class RelationSpec:
|
|
40
|
+
relation: str
|
|
41
|
+
version: str
|
|
42
|
+
risk_tier: str
|
|
43
|
+
required_slots: tuple[str, ...]
|
|
44
|
+
allowed_modalities: tuple[str, ...]
|
|
45
|
+
writers: tuple[str, ...]
|
|
46
|
+
effect_kind: str
|
|
47
|
+
admission_mode: str = "shadow"
|
|
48
|
+
value_type: str = "integer" # integer / number / string / enum
|
|
49
|
+
# XM-12: unit 只用于展示,不参与 state_key,也不参与校验。
|
|
50
|
+
# 编码器给出与规格不符的单位时不会报错——这是有意的取舍,见 ISSUES。
|
|
51
|
+
# 真要校验,应判 disputed 而不是拒绝(拒绝会丢掉证据)。
|
|
52
|
+
unit: str | None = None
|
|
53
|
+
allow_negative: bool = False
|
|
54
|
+
enum_values: tuple[str, ...] = ()
|
|
55
|
+
confirmation: str = "none" # none / implicit / explicit / authority
|
|
56
|
+
validity: str = "permanent" # permanent / ttl / reconfirm
|
|
57
|
+
ttl_seconds: int | None = None
|
|
58
|
+
conflict_policy: str = "dispute" # dispute / reject / last_write_wins
|
|
59
|
+
conservation: str | None = None # None / "repayment":追踪累计偿付量
|
|
60
|
+
feeds: tuple[str, ...] = ()
|
|
61
|
+
derived_from: tuple[str, ...] = ()
|
|
62
|
+
note: str = ""
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------- 自检
|
|
65
|
+
def validate(self) -> None:
|
|
66
|
+
if self.risk_tier not in RISK_TIERS:
|
|
67
|
+
raise ValueError(f"{self.relation}: bad risk_tier {self.risk_tier!r}")
|
|
68
|
+
if self.admission_mode not in ADMISSION_MODES:
|
|
69
|
+
raise ValueError(f"{self.relation}: bad admission_mode {self.admission_mode!r}")
|
|
70
|
+
if self.effect_kind not in EFFECT_KINDS:
|
|
71
|
+
raise ValueError(f"{self.relation}: bad effect_kind {self.effect_kind!r}")
|
|
72
|
+
for m in self.allowed_modalities:
|
|
73
|
+
if m not in MODALITIES:
|
|
74
|
+
raise ValueError(f"{self.relation}: unknown modality {m!r}")
|
|
75
|
+
if not self.required_slots:
|
|
76
|
+
raise ValueError(f"{self.relation}: required_slots must not be empty")
|
|
77
|
+
# R3 永远不允许自动提交(设计文档 7.3 / 15.5)
|
|
78
|
+
if self.risk_tier == "R3" and self.admission_mode == "auto":
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"{self.relation}: R3 relation must not be 'auto' "
|
|
81
|
+
"(permissions/deletion go through the control channel only)"
|
|
82
|
+
)
|
|
83
|
+
if self.value_type == "enum" and not self.enum_values:
|
|
84
|
+
raise ValueError(f"{self.relation}: enum value_type requires enum_values")
|
|
85
|
+
if self.conservation not in (None, "repayment"):
|
|
86
|
+
raise ValueError(f"{self.relation}: unknown conservation {self.conservation!r}")
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def spec_version(self) -> str:
|
|
90
|
+
return f"{self.relation}@{self.version}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class SpecRegistry:
|
|
95
|
+
_by_relation: dict[str, RelationSpec] = field(default_factory=dict)
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------------------- 加载
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_dir(cls, path: str | Path) -> "SpecRegistry":
|
|
100
|
+
reg = cls()
|
|
101
|
+
for f in sorted(Path(path).glob("*.json")):
|
|
102
|
+
reg.add(RelationSpec(**json.loads(f.read_text(encoding="utf-8"))))
|
|
103
|
+
return reg
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_specs(cls, specs: list[RelationSpec]) -> "SpecRegistry":
|
|
107
|
+
reg = cls()
|
|
108
|
+
for s in specs:
|
|
109
|
+
reg.add(s)
|
|
110
|
+
return reg
|
|
111
|
+
|
|
112
|
+
def add(self, spec: RelationSpec) -> None:
|
|
113
|
+
spec.validate()
|
|
114
|
+
if spec.relation in self._by_relation:
|
|
115
|
+
raise ValueError(f"duplicate relation spec: {spec.relation}")
|
|
116
|
+
self._by_relation[spec.relation] = spec
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------- 查询
|
|
119
|
+
def get(self, relation: str) -> RelationSpec:
|
|
120
|
+
try:
|
|
121
|
+
return self._by_relation[relation]
|
|
122
|
+
except KeyError:
|
|
123
|
+
raise SpecNotFound(f"no spec registered for relation {relation!r}") from None
|
|
124
|
+
|
|
125
|
+
def all(self) -> list[RelationSpec]:
|
|
126
|
+
return list(self._by_relation.values())
|
|
127
|
+
|
|
128
|
+
# ------------------------------------------------------------- 依赖图
|
|
129
|
+
def dependency_edges(self) -> list[tuple[str, str]]:
|
|
130
|
+
"""返回 (input_relation, derived_relation) 边列表,用于环检测。"""
|
|
131
|
+
edges: list[tuple[str, str]] = []
|
|
132
|
+
for spec in self._by_relation.values():
|
|
133
|
+
for src in spec.derived_from:
|
|
134
|
+
edges.append((src, spec.relation))
|
|
135
|
+
return edges
|
|
136
|
+
|
|
137
|
+
def assert_acyclic(self) -> None:
|
|
138
|
+
"""依赖图必须为 DAG(设计文档 9.5)。"""
|
|
139
|
+
from .errors import DependencyCycle
|
|
140
|
+
|
|
141
|
+
graph: dict[str, set[str]] = {}
|
|
142
|
+
for src, dst in self.dependency_edges():
|
|
143
|
+
graph.setdefault(src, set()).add(dst)
|
|
144
|
+
|
|
145
|
+
WHITE, GREY, BLACK = 0, 1, 2
|
|
146
|
+
color: dict[str, int] = {}
|
|
147
|
+
|
|
148
|
+
def visit(node: str, stack: list[str]) -> None:
|
|
149
|
+
color[node] = GREY
|
|
150
|
+
stack.append(node)
|
|
151
|
+
for nxt in graph.get(node, ()):
|
|
152
|
+
c = color.get(nxt, WHITE)
|
|
153
|
+
if c == GREY:
|
|
154
|
+
cycle = " -> ".join(stack[stack.index(nxt):] + [nxt])
|
|
155
|
+
raise DependencyCycle(f"dependency cycle: {cycle}")
|
|
156
|
+
if c == WHITE:
|
|
157
|
+
visit(nxt, stack)
|
|
158
|
+
stack.pop()
|
|
159
|
+
color[node] = BLACK
|
|
160
|
+
|
|
161
|
+
for node in list(graph):
|
|
162
|
+
if color.get(node, WHITE) == WHITE:
|
|
163
|
+
visit(node, [])
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def slot_value(spec: RelationSpec, slots: dict[str, Any], name: str) -> Any:
|
|
167
|
+
"""取槽位;缺失时抛错,不做默认值填充(I5)。"""
|
|
168
|
+
if name not in slots:
|
|
169
|
+
raise KeyError(f"missing required slot {name!r} for relation {spec.relation!r}")
|
|
170
|
+
return slots[name]
|