mcs-core 0.1.0__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 (115) hide show
  1. mcs/__init__.py +52 -0
  2. mcs/core/__init__.py +18 -0
  3. mcs/core/builder.py +433 -0
  4. mcs/core/calibrated_estimator.py +61 -0
  5. mcs/core/content_merge.py +81 -0
  6. mcs/core/context_renderer.py +245 -0
  7. mcs/core/errors.py +63 -0
  8. mcs/core/mcs.py +251 -0
  9. mcs/core/plugin.py +109 -0
  10. mcs/core/plugin_manager.py +113 -0
  11. mcs/core/query_engine.py +864 -0
  12. mcs/core/store.py +364 -0
  13. mcs/core/token_budget.py +180 -0
  14. mcs/core/write_pipeline.py +1044 -0
  15. mcs/diagnostics/__init__.py +5 -0
  16. mcs/diagnostics/graph_quality.py +278 -0
  17. mcs/entities/__init__.py +54 -0
  18. mcs/entities/config.py +325 -0
  19. mcs/entities/decisions.py +217 -0
  20. mcs/entities/graph.py +170 -0
  21. mcs/interfaces/__init__.py +23 -0
  22. mcs/interfaces/arbitration_plugin.py +50 -0
  23. mcs/interfaces/compaction_plugin.py +64 -0
  24. mcs/interfaces/edge_extension.py +63 -0
  25. mcs/interfaces/entry_plugin.py +54 -0
  26. mcs/interfaces/index.py +52 -0
  27. mcs/interfaces/llm.py +304 -0
  28. mcs/interfaces/maintenance.py +35 -0
  29. mcs/interfaces/node_extension.py +57 -0
  30. mcs/interfaces/postprocess_plugin.py +43 -0
  31. mcs/interfaces/preprocess_plugin.py +25 -0
  32. mcs/interfaces/priority_scorer.py +44 -0
  33. mcs/interfaces/query_preprocess_plugin.py +43 -0
  34. mcs/interfaces/storage_schema_ext.py +35 -0
  35. mcs/interfaces/trim_plugin.py +61 -0
  36. mcs/interfaces/write_preprocess_plugin.py +43 -0
  37. mcs/plugins/__init__.py +7 -0
  38. mcs/plugins/entry/__init__.py +5 -0
  39. mcs/plugins/entry/hub_fallback.py +142 -0
  40. mcs/plugins/index/__init__.py +8 -0
  41. mcs/plugins/index/alias_index.py +179 -0
  42. mcs/plugins/llm/__init__.py +7 -0
  43. mcs/plugins/llm/claude_llm.py +191 -0
  44. mcs/plugins/llm/deepseek_llm.py +163 -0
  45. mcs/plugins/llm/ollama_llm.py +259 -0
  46. mcs/plugins/maintenance/__init__.py +6 -0
  47. mcs/plugins/maintenance/dedup_maintenance.py +186 -0
  48. mcs/plugins/maintenance/fanout_reducer.py +905 -0
  49. mcs/plugins/maintenance/graph_summary.py +96 -0
  50. mcs/plugins/maintenance/summary_regen.py +92 -0
  51. mcs/plugins/postprocess/__init__.py +6 -0
  52. mcs/plugins/postprocess/rerank.py +197 -0
  53. mcs/plugins/postprocess/summary.py +70 -0
  54. mcs/plugins/preprocess/__init__.py +9 -0
  55. mcs/plugins/preprocess/cross_doc_linker.py +387 -0
  56. mcs/plugins/preprocess/source_tracking.py +372 -0
  57. mcs/plugins/trim/__init__.py +5 -0
  58. mcs/plugins/trim/llm_seed_selector.py +108 -0
  59. mcs/plugins/trim/priority_trim.py +71 -0
  60. mcs/presets/__init__.py +21 -0
  61. mcs/presets/phase1.py +226 -0
  62. mcs/prompts/__init__.py +141 -0
  63. mcs/prompts/adjudicate.py +61 -0
  64. mcs/prompts/arbitrate.py +35 -0
  65. mcs/prompts/decide_directions.py +41 -0
  66. mcs/prompts/decide_hub.py +141 -0
  67. mcs/prompts/extract_concepts.py +131 -0
  68. mcs/prompts/extract_work_events.py +111 -0
  69. mcs/prompts/gen_aliases.py +36 -0
  70. mcs/prompts/gen_graph_summary.py +30 -0
  71. mcs/prompts/gen_summary.py +24 -0
  72. mcs/prompts/generalize.py +40 -0
  73. mcs/prompts/judge_relations.py +207 -0
  74. mcs/prompts/merge.py +101 -0
  75. mcs/prompts/merge_content.py +66 -0
  76. mcs/prompts/navigate_hub.py +89 -0
  77. mcs/prompts/select_facts.py +144 -0
  78. mcs/prompts/select_nodes.py +60 -0
  79. mcs/prompts/split.py +128 -0
  80. mcs/prompts/synthesize.py +27 -0
  81. mcs/rendering.py +64 -0
  82. mcs/stores/__init__.py +11 -0
  83. mcs/stores/in_memory.py +450 -0
  84. mcs/stores/sqlite_store.py +920 -0
  85. mcs/utils/__init__.py +6 -0
  86. mcs/utils/env_expand.py +57 -0
  87. mcs/utils/imports.py +62 -0
  88. mcs/utils/text_utils.py +196 -0
  89. mcs/utils/timestamps.py +62 -0
  90. mcs/utils/tokenizer.py +51 -0
  91. mcs_agent/__init__.py +50 -0
  92. mcs_agent/__main__.py +10 -0
  93. mcs_agent/app.py +165 -0
  94. mcs_agent/builder.py +290 -0
  95. mcs_agent/context.py +374 -0
  96. mcs_agent/llm.py +41 -0
  97. mcs_agent/llms/__init__.py +17 -0
  98. mcs_agent/llms/anthropic.py +219 -0
  99. mcs_agent/llms/base.py +42 -0
  100. mcs_agent/llms/callable.py +36 -0
  101. mcs_agent/llms/openai.py +101 -0
  102. mcs_agent/llms/registry.py +30 -0
  103. mcs_agent/loop.py +359 -0
  104. mcs_agent/memory.py +1102 -0
  105. mcs_agent/tools.py +537 -0
  106. mcs_agent/trace.py +125 -0
  107. mcs_core-0.1.0.dist-info/METADATA +231 -0
  108. mcs_core-0.1.0.dist-info/RECORD +115 -0
  109. mcs_core-0.1.0.dist-info/WHEEL +5 -0
  110. mcs_core-0.1.0.dist-info/entry_points.txt +3 -0
  111. mcs_core-0.1.0.dist-info/licenses/LICENSE +21 -0
  112. mcs_core-0.1.0.dist-info/top_level.txt +3 -0
  113. mcs_mcp/__init__.py +17 -0
  114. mcs_mcp/__main__.py +8 -0
  115. mcs_mcp/server.py +292 -0
mcs/__init__.py ADDED
@@ -0,0 +1,52 @@
1
+ """MCS - Maximum-Context Subgraph:可扩展的知识图谱与检索引擎。
2
+
3
+ 顶层 ``MCS`` 类将图存储、LLM 后端、插件链、读写管线组装在一起。
4
+ 推荐使用 Builder 构建 MCS 实例:
5
+
6
+ # 快捷方式(推荐)
7
+ from mcs.presets import create_mcs
8
+ mcs = create_mcs(llm="deepseek", db_path="mcs.db")
9
+
10
+ # 或完整自定义
11
+ from mcs.presets import Phase1Builder
12
+ from mcs.entities.config import MCSConfig
13
+
14
+ config = MCSConfig.knowledge_graph(write_llm="deepseek", read_llm="deepseek")
15
+ config.plugin_configs["deepseek_llm"]["api_key"] = "..."
16
+ builder = Phase1Builder(config)
17
+ mcs = builder.build() # 返回即用的 MCS 实例
18
+
19
+ # 使用
20
+ mcs.ingest("深度学习是机器学习的一个子领域...")
21
+ nodes = mcs.query("什么是深度学习?")
22
+ mcs.shutdown()
23
+
24
+ MCS 实例由 Builder 一次性构建完成,无需调用 initialize()。
25
+
26
+ 参见 ``openspec/specs/`` 获取各能力的契约定义。
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ __version__ = "0.1.0"
32
+
33
+ from mcs.core.builder import MCSBuilder
34
+ from mcs.core.mcs import MCS
35
+ from mcs.core.plugin import Plugin, PluginType
36
+ from mcs.core.store import StoreInterface
37
+ from mcs.entities.config import MCSConfig
38
+ from mcs.presets import Phase1Builder, create_mcs, get_phase1_plugin_registry
39
+ from mcs.stores.in_memory import InMemoryStore
40
+ from mcs.stores.sqlite_store import SQLiteStore
41
+
42
+ __all__ = [
43
+ "MCS",
44
+ "MCSConfig",
45
+ "MCSBuilder",
46
+ "Phase1Builder",
47
+ "StoreInterface",
48
+ "InMemoryStore",
49
+ "SQLiteStore",
50
+ "create_mcs",
51
+ "get_phase1_plugin_registry",
52
+ ]
mcs/core/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """Core engine - stable graph operations and pipeline state machines.
2
+
3
+ Contains:
4
+ - ``mcs``: MCS top-level orchestrator with dual PluginManager architecture
5
+ - ``builder``: MCSBuilder abstract base class for building MCS instances
6
+ - ``errors``: Exception hierarchy
7
+ - ``plugin``: Plugin base class and PluginType enum
8
+ - ``plugin_manager``: PluginManager and PluginContext
9
+ - ``query_engine``: QueryEngine for read pipeline
10
+ - ``store``: StoreInterface ABC
11
+ - ``token_budget``: TokenBudget
12
+ - ``write_pipeline``: WritePipeline for ingest pipeline
13
+ - ``context_renderer``: ContextRenderer for LLM input rendering
14
+
15
+ 纯数据模型(Node/Edge/Subgraph、Decision 系列、MCSConfig)已迁至 ``mcs.entities``。
16
+
17
+ See architecture.md §2.
18
+ """
mcs/core/builder.py ADDED
@@ -0,0 +1,433 @@
1
+ """MCSBuilder 抽象基类 — 全量组装契约。
2
+
3
+ Builder 接管从配置到完成态 MCS 的完整构建流程,包括:
4
+ - Store 初始化
5
+ - 插件实例化与注册
6
+ - PluginContext 构建与插件初始化
7
+ - QueryEngine 和 WritePipeline 构建
8
+ - Load-on-startup
9
+
10
+ MCS 构造后即处于 ready 状态,无需调用 initialize()。
11
+
12
+ 参见 openspec/specs/mcs-builder/spec.md "MCSBuilder 抽象基类定义构建契约"。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from abc import ABC, abstractmethod
19
+ from typing import TYPE_CHECKING
20
+
21
+ if TYPE_CHECKING:
22
+ from mcs.core.plugin import Plugin
23
+ from mcs.entities.config import MCSConfig
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class MCSBuilder(ABC):
29
+ """抽象构建器 — 全量组装 MCS。
30
+
31
+ 子类需实现 ``get_plugin_class()`` 方法,根据插件名称返回插件类。
32
+ ``build()`` 方法封装从配置到初始化完成的整个构建流程。
33
+ """
34
+
35
+ def __init__(self, config: "MCSConfig"):
36
+ """初始化构建器。
37
+
38
+ Args:
39
+ config: MCS 配置对象
40
+ """
41
+ self.config = config
42
+
43
+ @abstractmethod
44
+ def get_plugin_class(self, name: str) -> type["Plugin"] | None:
45
+ """根据插件名称返回插件类。
46
+
47
+ Args:
48
+ name: 插件名称(如 "sqlite_storage", "deepseek_llm")
49
+
50
+ Returns:
51
+ 插件类,若未找到则返回 None
52
+ """
53
+ ...
54
+
55
+ def build(self) -> "MCS":
56
+ """构建并返回即用的 MCS 实例。
57
+
58
+ 执行完整的 14 步初始化流程。
59
+
60
+ Returns:
61
+ 已完成初始化、可直接使用的 MCS 实例
62
+ """
63
+ from mcs.core.context_renderer import ContextRenderer
64
+ from mcs.core.mcs import MCS
65
+ from mcs.core.plugin_manager import PluginContext, PluginManager
66
+ from mcs.core.query_engine import QueryEngine
67
+ from mcs.core.token_budget import TokenBudget
68
+ from mcs.core.write_pipeline import WritePipeline
69
+ from mcs.interfaces.priority_scorer import DefaultPriorityScorer
70
+ from mcs.stores.in_memory import InMemoryStore
71
+ from mcs.stores.sqlite_store import SQLiteStore
72
+
73
+ # 1. 实例化 Store
74
+ store = self._init_store()
75
+
76
+ # 2. 实例化双 PluginManager
77
+ write_manager = PluginManager()
78
+ read_manager = PluginManager()
79
+
80
+ # 3. 按配置实例化并注册插件(获取 write_llm / read_llm)
81
+ write_llm, read_llm = self._register_plugins(write_manager, read_manager)
82
+
83
+ # 4. 用 write_llm.count_tokens 构造 TokenBudget
84
+ # write_llm 必非 None:_register_llm_plugins 在 write_llm 缺失时直接 raise
85
+ token_budget = TokenBudget(
86
+ self.config.token_budget,
87
+ counter=write_llm.count_tokens,
88
+ )
89
+
90
+ # 5. 初始化 SQLiteStore(若使用)
91
+ if isinstance(store, SQLiteStore) and store.conn is None:
92
+ from mcs.core.plugin import PluginType
93
+ schema_exts = write_manager.get_all(PluginType.STORAGE_SCHEMA_EXT)
94
+ node_exts = {
95
+ p.get_name(): p
96
+ for p in write_manager.get_all(PluginType.NODE_EXTENSION)
97
+ }
98
+ edge_exts = {
99
+ p.get_name(): p
100
+ for p in write_manager.get_all(PluginType.EDGE_EXTENSION)
101
+ }
102
+ store.initialize(
103
+ schema_extensions=schema_exts,
104
+ node_extensions=node_exts,
105
+ edge_extensions=edge_exts,
106
+ priority_scorer=DefaultPriorityScorer(),
107
+ )
108
+
109
+ # 6. 创建 ContextRenderer(传入 read_manager)
110
+ context_renderer = ContextRenderer(read_manager)
111
+
112
+ # 7. 构建 PluginContext 并初始化所有插件
113
+ self._init_plugin_context(
114
+ store, token_budget, context_renderer, write_manager, read_manager
115
+ )
116
+
117
+ # 8. 应用 prompt_overrides 到 LLM
118
+ self._apply_prompt_overrides(write_llm, read_llm)
119
+
120
+ # 9. 构建 QueryEngine(read_manager + read_llm)
121
+ query_engine = QueryEngine(
122
+ store=store,
123
+ llm=read_llm,
124
+ plugin_manager=read_manager,
125
+ token_budget=token_budget,
126
+ max_rounds=self.config.max_rounds,
127
+ max_accumulated_nodes=self.config.max_accumulated_nodes,
128
+ )
129
+
130
+ # 10. 构建 WritePipeline(write_manager + write_llm + query_engine)
131
+ write_pipeline = WritePipeline(
132
+ store=store,
133
+ llm=write_llm,
134
+ query_engine=query_engine,
135
+ plugin_manager=write_manager,
136
+ token_budget=token_budget,
137
+ config=self.config,
138
+ )
139
+
140
+ # 11. 构建 MCS
141
+ mcs = MCS(
142
+ write_pipeline=write_pipeline,
143
+ query_engine=query_engine,
144
+ store=store,
145
+ write_manager=write_manager,
146
+ read_manager=read_manager,
147
+ )
148
+
149
+ # 12. 执行 load-on-startup
150
+ self._load_on_startup(store, read_manager)
151
+
152
+ return mcs
153
+
154
+ def _init_store(self) -> "StoreInterface":
155
+ """根据配置实例化 Store。
156
+
157
+ Returns:
158
+ Store 实例
159
+ """
160
+ from mcs.stores.in_memory import InMemoryStore
161
+ from mcs.stores.sqlite_store import SQLiteStore
162
+
163
+ sqlite_config = self.config.plugin_configs.get("sqlite_storage", {})
164
+ db_path = sqlite_config.get("path", "")
165
+
166
+ if db_path and db_path != ":memory:":
167
+ return SQLiteStore(sqlite_config)
168
+ return InMemoryStore()
169
+
170
+ def _register_plugins(
171
+ self,
172
+ write_manager: "PluginManager",
173
+ read_manager: "PluginManager",
174
+ ) -> tuple["Plugin", "Plugin"]:
175
+ """按 shared/write/read 分类实例化并注册插件到双 manager。
176
+
177
+ Args:
178
+ write_manager: 写入侧插件管理器
179
+ read_manager: 读取侧插件管理器
180
+
181
+ Returns:
182
+ (write_llm, read_llm) 插件实例元组
183
+ """
184
+ # 1. 共享插件:同一实例注册到两个 manager
185
+ for plugin_name in self.config.shared_plugins:
186
+ plugin = self._instantiate_plugin(plugin_name)
187
+ if plugin is None:
188
+ continue
189
+ write_manager.register(plugin)
190
+ read_manager.register(plugin)
191
+
192
+ # 2. 写入专用:只注册到 write_manager
193
+ for plugin_name in self.config.write_plugins:
194
+ plugin = self._instantiate_plugin(plugin_name)
195
+ if plugin is None:
196
+ continue
197
+ write_manager.register(plugin)
198
+
199
+ # 3. 读取专用:只注册到 read_manager
200
+ for plugin_name in self.config.read_plugins:
201
+ plugin = self._instantiate_plugin(plugin_name)
202
+ if plugin is None:
203
+ continue
204
+ read_manager.register(plugin)
205
+
206
+ # 4. 处理 LLM 分离
207
+ return self._register_llm_plugins(write_manager, read_manager)
208
+
209
+ def _instantiate_plugin(self, plugin_name: str) -> "Plugin | None":
210
+ """从注册表实例化插件。
211
+
212
+ Args:
213
+ plugin_name: 插件名称
214
+
215
+ Returns:
216
+ 插件实例,若未找到则返回 None
217
+ """
218
+ cls = self.get_plugin_class(plugin_name)
219
+ if cls is None:
220
+ return None
221
+ plugin_config = self.config.plugin_configs.get(plugin_name, {})
222
+ try:
223
+ return cls(plugin_config)
224
+ except TypeError:
225
+ return cls()
226
+
227
+ def _register_llm_plugins(
228
+ self,
229
+ write_manager: "PluginManager",
230
+ read_manager: "PluginManager",
231
+ ) -> tuple["Plugin", "Plugin"]:
232
+ """处理 LLM 分离:write_llm / read_llm 从对应 manager 查找;相同时共享实例。
233
+
234
+ Args:
235
+ write_manager: 写入侧插件管理器
236
+ read_manager: 读取侧插件管理器
237
+
238
+ Returns:
239
+ (write_llm, read_llm) 插件实例元组
240
+ """
241
+ write_llm_name = self.config.write_llm
242
+ read_llm_name = self.config.read_llm
243
+
244
+ if not write_llm_name:
245
+ raise RuntimeError(
246
+ "未指定 write_llm。请在 MCSConfig 中设置 write_llm。"
247
+ )
248
+ if not read_llm_name:
249
+ raise RuntimeError(
250
+ "未指定 read_llm。请在 MCSConfig 中设置 read_llm。"
251
+ )
252
+
253
+ if write_llm_name == read_llm_name:
254
+ # 同一 LLM:共享实例
255
+ # 先检查是否已通过 register_plugin 注册
256
+ existing = write_manager.get_by_name(write_llm_name) or \
257
+ read_manager.get_by_name(read_llm_name)
258
+ if existing is not None:
259
+ # 已注册,只需确保两个 manager 都有
260
+ if write_manager.get_by_name(write_llm_name) is None:
261
+ write_manager.register(existing)
262
+ if read_manager.get_by_name(read_llm_name) is None:
263
+ read_manager.register(existing)
264
+ else:
265
+ # 从 registry 实例化
266
+ plugin = self._instantiate_plugin(write_llm_name)
267
+ if plugin is None:
268
+ raise RuntimeError(
269
+ f"未找到 LLM 插件: {write_llm_name!r}。"
270
+ "请确保插件注册表中包含该插件。"
271
+ )
272
+ write_manager.register(plugin)
273
+ read_manager.register(plugin)
274
+ else:
275
+ # 不同 LLM:各自独立
276
+ write_plugin = write_manager.get_by_name(write_llm_name) or \
277
+ read_manager.get_by_name(write_llm_name)
278
+ if write_plugin is None:
279
+ write_plugin = self._instantiate_plugin(write_llm_name)
280
+ if write_plugin is None:
281
+ raise RuntimeError(
282
+ f"未找到写入 LLM 插件: {write_llm_name!r}。"
283
+ "请确保插件注册表中包含该插件。"
284
+ )
285
+ if write_manager.get_by_name(write_llm_name) is None:
286
+ write_manager.register(write_plugin)
287
+
288
+ read_plugin = read_manager.get_by_name(read_llm_name) or \
289
+ write_manager.get_by_name(read_llm_name)
290
+ if read_plugin is None:
291
+ read_plugin = self._instantiate_plugin(read_llm_name)
292
+ if read_plugin is None:
293
+ raise RuntimeError(
294
+ f"未找到读取 LLM 插件: {read_llm_name!r}。"
295
+ "请确保插件注册表中包含该插件。"
296
+ )
297
+ if read_manager.get_by_name(read_llm_name) is None:
298
+ read_manager.register(read_plugin)
299
+
300
+ # 解析 LLM 引用
301
+ write_llm = write_manager.get_by_name(write_llm_name)
302
+ read_llm = read_manager.get_by_name(read_llm_name)
303
+
304
+ return write_llm, read_llm
305
+
306
+ def _init_plugin_context(
307
+ self,
308
+ store: "StoreInterface",
309
+ token_budget: "TokenBudget",
310
+ context_renderer: "ContextRenderer",
311
+ write_manager: "PluginManager",
312
+ read_manager: "PluginManager",
313
+ ) -> None:
314
+ """创建 PluginContext 并初始化所有插件。
315
+
316
+ Args:
317
+ store: 存储实例
318
+ token_budget: Token 预算
319
+ context_renderer: 上下文渲染器
320
+ write_manager: 写入侧插件管理器
321
+ read_manager: 读取侧插件管理器
322
+ """
323
+ from mcs.core.plugin_manager import PluginContext
324
+
325
+ # 写入侧插件初始化
326
+ write_ctx = PluginContext(
327
+ store=store,
328
+ config=self.config,
329
+ token_budget=token_budget,
330
+ context_renderer=context_renderer,
331
+ plugin_manager=write_manager,
332
+ )
333
+ write_manager.initialize_all(write_ctx)
334
+
335
+ # 读取侧插件初始化
336
+ read_ctx = PluginContext(
337
+ store=store,
338
+ config=self.config,
339
+ token_budget=token_budget,
340
+ context_renderer=context_renderer,
341
+ plugin_manager=read_manager,
342
+ )
343
+ read_manager.initialize_all(read_ctx)
344
+
345
+ def _apply_prompt_overrides(
346
+ self,
347
+ write_llm: "Plugin",
348
+ read_llm: "Plugin",
349
+ ) -> None:
350
+ """将用户 prompt 覆盖应用到 LLM。
351
+
352
+ Args:
353
+ write_llm: 写入 LLM 插件
354
+ read_llm: 读取 LLM 插件
355
+ """
356
+ for purpose, overrides in (self.config.prompt_overrides or {}).items():
357
+ write_llm.register_prompt(
358
+ purpose,
359
+ system=overrides.get("system"),
360
+ template=overrides.get("template"),
361
+ parser=overrides.get("parser"),
362
+ )
363
+ # 如果 read_llm 与 write_llm 不同,也应用覆盖
364
+ if read_llm is not write_llm:
365
+ read_llm.register_prompt(
366
+ purpose,
367
+ system=overrides.get("system"),
368
+ template=overrides.get("template"),
369
+ parser=overrides.get("parser"),
370
+ )
371
+
372
+ def _load_on_startup(
373
+ self,
374
+ store: "StoreInterface",
375
+ read_manager: "PluginManager",
376
+ ) -> None:
377
+ """若 Store 为空且 SQLite 可用,加载已有数据并重建 Index。
378
+
379
+ Args:
380
+ store: 存储实例
381
+ read_manager: 读取侧插件管理器(用于获取 Index 插件)
382
+ """
383
+ from mcs.core.plugin import PluginType
384
+ from mcs.stores.sqlite_store import SQLiteStore
385
+
386
+ if store.get_all_nodes():
387
+ return
388
+ if not isinstance(store, SQLiteStore):
389
+ return
390
+ try:
391
+ store.load()
392
+ # reload 后重建所有 IndexInterface 索引
393
+ for index in read_manager.get_all(PluginType.INDEX):
394
+ try:
395
+ index.build(store)
396
+ except NotImplementedError:
397
+ continue
398
+ except Exception:
399
+ logger.warning("Load-on-startup failed", exc_info=True)
400
+
401
+ def _collect_registry(self) -> dict[str, type["Plugin"]]:
402
+ """从 shared + write + read + LLM 收集插件注册表。
403
+
404
+ Returns:
405
+ 插件名称 → 插件类的映射
406
+ """
407
+ all_names: list[str] = []
408
+
409
+ # 收集所有插件名称(去重)
410
+ seen: set[str] = set()
411
+ for name in (
412
+ self.config.shared_plugins +
413
+ self.config.write_plugins +
414
+ self.config.read_plugins
415
+ ):
416
+ if name not in seen:
417
+ all_names.append(name)
418
+ seen.add(name)
419
+
420
+ # LLM 也加入(即使不在 plugins 列表中)
421
+ for llm in [self.config.write_llm, self.config.read_llm]:
422
+ if llm and llm not in seen:
423
+ all_names.append(llm)
424
+ seen.add(llm)
425
+
426
+ # 查找插件类
427
+ registry: dict[str, type["Plugin"]] = {}
428
+ for name in all_names:
429
+ cls = self.get_plugin_class(name)
430
+ if cls is not None:
431
+ registry[name] = cls
432
+
433
+ return registry
@@ -0,0 +1,61 @@
1
+ """校准经验式 token 估算器——精确方案的兜底。
2
+
3
+ 当 LLM 插件的精确计数(API / tiktoken)不可用时,使用按模型族调整系数的
4
+ 经验式替代当前硬编码的 CJK 1:1 + 非CJK 4:1。
5
+
6
+ 设计原则:宁可高估(过早触发裂变)也不低估(破坏核心不变量)。
7
+ unknown 模型采用 claude/gpt 系数(最保守)。
8
+
9
+ 系数来源:下表系数为文献/经验值(CJK 系数取各模型族中文 token/字 的保守上界)。
10
+ ``bench/calibration/calibrate_token_estimator.py`` 提供校准工具(对照精确计数拟合
11
+ 系数),尚未跑实测定标;定标后更新 ``COEFFICIENTS``。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+
17
+ class CalibratedEstimator:
18
+ """按模型族调整系数的经验式 token 估算器。
19
+
20
+ 每个模型族有独立的 CJK 系数和非 CJK 除数:
21
+
22
+ estimate(text) = cjk_count * cjk_coeff + non_cjk_count // non_cjk_divisor
23
+
24
+ 系数表为文献/经验值(详见 design.md §校准方法论),待 bench/calibration 实测定标。
25
+ """
26
+
27
+ # 模型族 → (CJK 系数, 非CJK 字符/token 除数)。文献/经验值,待实测定标
28
+ COEFFICIENTS: dict[str, tuple[float, int]] = {
29
+ "claude": (1.7, 3),
30
+ "gpt": (1.7, 3),
31
+ "deepseek": (1.3, 4),
32
+ "ollama": (1.3, 4),
33
+ "unknown": (1.7, 3), # 保守——与 claude/gpt 相同
34
+ }
35
+
36
+ def __init__(self, model_family: str = "unknown") -> None:
37
+ self._model_family = model_family
38
+ cjk_coeff, non_cjk_divisor = self.COEFFICIENTS.get(
39
+ model_family, self.COEFFICIENTS["unknown"]
40
+ )
41
+ self._cjk_coeff = cjk_coeff
42
+ self._non_cjk_divisor = non_cjk_divisor
43
+
44
+ @property
45
+ def model_family(self) -> str:
46
+ """返回构造时指定的模型族名称。"""
47
+ return self._model_family
48
+
49
+ def estimate(self, text: str | None) -> int:
50
+ """估算 ``text`` 的 token 数量。
51
+
52
+ - CJK 字符按 ``cjk_coeff`` 计(如 claude ×1.7)
53
+ - 非 CJK 字符按 ``non_cjk_divisor`` 除(如 claude ÷3)
54
+ - 空值/None 返回 0
55
+ - 非空文本至少返回 1
56
+ """
57
+ if not text:
58
+ return 0
59
+ cjk = sum(1 for ch in text if "一" <= ch <= "鿿")
60
+ non_cjk = len(text) - cjk
61
+ return max(1, int(cjk * self._cjk_coeff + non_cjk // self._non_cjk_divisor))
@@ -0,0 +1,81 @@
1
+ """公共 content 合并 helper。
2
+
3
+ 统一 3 条字面合并路径的 content 处理(write path / query read-repair / 后台 dedup),
4
+ 落实 unified-graph-schema「图质量最终收敛」content 合并守则:不机械换行追加。
5
+
6
+ 行为:
7
+ - 子串关系(target ⊇ incoming → 跳过;incoming ⊇ target → 替换)→ 零成本,所有路径共用。
8
+ - 非子串 + merge_llm 传入 → LLM 语义合并成一个稳定定义(守时间归属)。
9
+ - 非子串 + merge_llm=None → 返回 target(不碰;调用方决定挂起 / 保留 dup)。
10
+
11
+ ``substring_relation`` 抽出子串包含判定,供 ``merge_content`` 与后台 dedup 共用——
12
+ 单点定义,防两处分支口径漂移。
13
+
14
+ 调用方分流(按 LLM 可用性):
15
+ - write path `_dispatch_merge` 传 merge_llm(每次 ingest 同名对齐,本就 LLM)。
16
+ - query read-repair `_try_read_repair` 不传(读路径零 LLM;被并方节点保留)。
17
+ - 后台 dedup 不传(子串才合删 dup,非子串保留 dup;彻底合并靠 write path)。
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ from typing import Callable, Optional
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def substring_relation(target: str, incoming: str) -> Optional[str]:
29
+ """两段 content 的子串包含关系(``merge_content`` 与 dedup 共用,防口径漂移)。
30
+
31
+ 返回:
32
+ - ``"target"``:incoming 为空,或 incoming ⊆ target(target 更全 / 相等)。
33
+ - ``"incoming"``:target 为空,或 target ⊆ incoming(incoming 更全)。
34
+ - ``None``:两方都非空且互不包含(非子串)。
35
+
36
+ 调用方据此区分「可零成本合并」(返回非 None)与「需 LLM / 保留 dup」(None)。
37
+ """
38
+ t = target or ""
39
+ i = incoming or ""
40
+ if not i.strip():
41
+ return "target"
42
+ if not t.strip():
43
+ return "incoming"
44
+ if i in t:
45
+ return "target"
46
+ if t in i:
47
+ return "incoming"
48
+ return None
49
+
50
+
51
+ def merge_content(
52
+ target: str,
53
+ incoming: str,
54
+ merge_llm: Optional[Callable[[str, str], str]] = None,
55
+ ) -> str:
56
+ """合并两段 content 成一个稳定定义(不机械换行追加)。
57
+
58
+ - 子串关系零成本(经 ``substring_relation``:incoming ⊆ target → target;
59
+ target ⊆ incoming → incoming;含一方空)。
60
+ - 非子串 + merge_llm:LLM 语义合并;异常 / 空返回 → 降级保留 target + warning。
61
+ - 非子串 + merge_llm=None:返回 target(不碰)。
62
+ """
63
+ rel = substring_relation(target, incoming)
64
+ if rel == "target":
65
+ return target
66
+ if rel == "incoming":
67
+ return incoming
68
+
69
+ # 非子串(两方都非空、互不包含)
70
+ if merge_llm is None:
71
+ return target
72
+
73
+ try:
74
+ merged = merge_llm(target, incoming)
75
+ except Exception:
76
+ logger.warning("merge_content: LLM 合并失败,保留 target content", exc_info=True)
77
+ return target
78
+ if not merged or not merged.strip():
79
+ logger.warning("merge_content: LLM 返回空,保留 target content")
80
+ return target
81
+ return merged.strip()