corespine 0.0.1__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.
- corespine/__init__.py +69 -0
- corespine/config/__init__.py +1 -0
- corespine/config/env.py +77 -0
- corespine/conformance/__init__.py +1 -0
- corespine/conformance/harness.py +123 -0
- corespine/errors.py +81 -0
- corespine/llm/__init__.py +1 -0
- corespine/llm/provider.py +120 -0
- corespine/observability/__init__.py +1 -0
- corespine/observability/trace.py +90 -0
- corespine/py.typed +0 -0
- corespine/queue/__init__.py +1 -0
- corespine/queue/task_queue.py +93 -0
- corespine/seam/__init__.py +1 -0
- corespine/seam/registry.py +112 -0
- corespine-0.0.1.dist-info/METADATA +79 -0
- corespine-0.0.1.dist-info/RECORD +18 -0
- corespine-0.0.1.dist-info/WHEEL +4 -0
corespine/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""corespine —— Spine 家族的【薄】共享核(ADR 0001 D3/D5)。
|
|
2
|
+
|
|
3
|
+
只装 domain-neutral 的底层原语:缝注册表 / 隐私安全 observability / LLM 缝 /
|
|
4
|
+
env 配置 / 任务队列 / conformance 基座。刻意保持极小,按证据(rule of three)增长——
|
|
5
|
+
绝不放任何 RAG- 或 agent-特定的东西。详见 CLAUDE.md 宪章与 docs/adr/0001。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from corespine.config.env import env_key, load_from_env
|
|
9
|
+
from corespine.conformance.harness import CaseResult, ConformanceSuite, InvariantPack
|
|
10
|
+
from corespine.errors import ConfigError, CorespineError, SeamError, error_to_dict
|
|
11
|
+
from corespine.llm.provider import (
|
|
12
|
+
ChatCompletion,
|
|
13
|
+
Choice,
|
|
14
|
+
FunctionCall,
|
|
15
|
+
LLMProvider,
|
|
16
|
+
MockProvider,
|
|
17
|
+
ResponseMessage,
|
|
18
|
+
ToolCall,
|
|
19
|
+
Usage,
|
|
20
|
+
)
|
|
21
|
+
from corespine.observability.trace import (
|
|
22
|
+
FORBIDDEN_KEYS,
|
|
23
|
+
InProcessPrivacyTraceSink,
|
|
24
|
+
TraceError,
|
|
25
|
+
TraceEvent,
|
|
26
|
+
TraceSink,
|
|
27
|
+
)
|
|
28
|
+
from corespine.queue.task_queue import FakeQueue, JobStatus, TaskQueue
|
|
29
|
+
from corespine.seam.registry import Registry, lazy_extra_import
|
|
30
|
+
|
|
31
|
+
__version__ = "0.0.1"
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
# seam
|
|
35
|
+
"Registry",
|
|
36
|
+
"lazy_extra_import",
|
|
37
|
+
# observability
|
|
38
|
+
"TraceSink",
|
|
39
|
+
"TraceEvent",
|
|
40
|
+
"TraceError",
|
|
41
|
+
"InProcessPrivacyTraceSink",
|
|
42
|
+
"FORBIDDEN_KEYS",
|
|
43
|
+
# llm(OpenAI chat-completions 规范)
|
|
44
|
+
"LLMProvider",
|
|
45
|
+
"MockProvider",
|
|
46
|
+
"ChatCompletion",
|
|
47
|
+
"Choice",
|
|
48
|
+
"ResponseMessage",
|
|
49
|
+
"ToolCall",
|
|
50
|
+
"FunctionCall",
|
|
51
|
+
"Usage",
|
|
52
|
+
# config
|
|
53
|
+
"load_from_env",
|
|
54
|
+
"env_key",
|
|
55
|
+
# queue
|
|
56
|
+
"TaskQueue",
|
|
57
|
+
"JobStatus",
|
|
58
|
+
"FakeQueue",
|
|
59
|
+
# conformance
|
|
60
|
+
"ConformanceSuite",
|
|
61
|
+
"InvariantPack",
|
|
62
|
+
"CaseResult",
|
|
63
|
+
# errors
|
|
64
|
+
"CorespineError",
|
|
65
|
+
"error_to_dict",
|
|
66
|
+
"ConfigError",
|
|
67
|
+
"SeamError",
|
|
68
|
+
"__version__",
|
|
69
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""corespine.config —— env 驱动配置基底:PREFIX_* -> frozen dataclass。"""
|
corespine/config/env.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""env 驱动配置的基底助手:把 PREFIX_* 环境变量读进一个 frozen dataclass。
|
|
2
|
+
|
|
3
|
+
范式同 ragspine `ServiceConfig.from_env`——集中、声明式、可注入(测试传入自己的
|
|
4
|
+
env mapping,不碰进程环境)。但本助手是 domain-neutral 的:不预设任何具体字段,
|
|
5
|
+
只提供机制——"按字段名从 PREFIX_<FIELD> 读取 + 按注解类型转换 + 缺失用 dataclass
|
|
6
|
+
默认值"。app 声明自己的 frozen dataclass,调一次 load_from_env 即可。
|
|
7
|
+
|
|
8
|
+
支持的字段类型:str / int / float / bool(及其 `X | None` 可选形式)。bool 解析为
|
|
9
|
+
{1,true,yes,on} 为真、{0,false,no,off,""} 为假(均大小写不敏感)。未声明默认值
|
|
10
|
+
的字段若缺对应 env,则抛 ValueError(把缺失的 env 名报清楚)。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import dataclasses
|
|
16
|
+
import os
|
|
17
|
+
import types
|
|
18
|
+
from collections.abc import Mapping
|
|
19
|
+
from typing import TypeVar, Union, get_args, get_origin, get_type_hints
|
|
20
|
+
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
|
|
23
|
+
_TRUE = frozenset({"1", "true", "yes", "on"})
|
|
24
|
+
_FALSE = frozenset({"0", "false", "no", "off", ""})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def env_key(prefix: str, field_name: str) -> str:
|
|
28
|
+
"""字段名 -> 环境变量名:PREFIX_FIELDNAME(字段名大写,前缀以下划线相连)。"""
|
|
29
|
+
return f"{prefix.rstrip('_')}_{field_name.upper()}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _unwrap_optional(tp: object) -> object:
|
|
33
|
+
"""`X | None` / Optional[X] -> X;其余原样返回。"""
|
|
34
|
+
origin = get_origin(tp)
|
|
35
|
+
if origin is Union or origin is types.UnionType:
|
|
36
|
+
non_none = [a for a in get_args(tp) if a is not type(None)]
|
|
37
|
+
if len(non_none) == 1:
|
|
38
|
+
return non_none[0]
|
|
39
|
+
return tp
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _coerce(raw: str, tp: object) -> object:
|
|
43
|
+
"""按目标类型把 env 字符串转成值;str / 未知类型原样。"""
|
|
44
|
+
base = _unwrap_optional(tp)
|
|
45
|
+
if base is bool:
|
|
46
|
+
low = raw.strip().lower()
|
|
47
|
+
if low in _TRUE:
|
|
48
|
+
return True
|
|
49
|
+
if low in _FALSE:
|
|
50
|
+
return False
|
|
51
|
+
raise ValueError(f"无法把 {raw!r} 解析为 bool")
|
|
52
|
+
if base is int:
|
|
53
|
+
return int(raw)
|
|
54
|
+
if base is float:
|
|
55
|
+
return float(raw)
|
|
56
|
+
return raw
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_from_env(cls: type[T], *, prefix: str, env: Mapping[str, str] | None = None) -> T:
|
|
60
|
+
"""按 dataclass 字段从 PREFIX_* 读取并构造实例(缺失则用字段默认值)。
|
|
61
|
+
|
|
62
|
+
env 可注入(默认读 os.environ);get_type_hints 解析注解,故 `from __future__
|
|
63
|
+
import annotations` 下的字符串注解也能正确取到真实类型。
|
|
64
|
+
"""
|
|
65
|
+
if not dataclasses.is_dataclass(cls):
|
|
66
|
+
raise TypeError(f"{cls!r} 不是 dataclass")
|
|
67
|
+
source = os.environ if env is None else env
|
|
68
|
+
hints = get_type_hints(cls)
|
|
69
|
+
kwargs: dict[str, object] = {}
|
|
70
|
+
for f in dataclasses.fields(cls):
|
|
71
|
+
key = env_key(prefix, f.name)
|
|
72
|
+
if key in source:
|
|
73
|
+
kwargs[f.name] = _coerce(source[key], hints.get(f.name, f.type))
|
|
74
|
+
elif f.default is dataclasses.MISSING and f.default_factory is dataclasses.MISSING:
|
|
75
|
+
raise ValueError(f"缺少必填配置 {key}(字段 {f.name!r} 无默认值)")
|
|
76
|
+
# 否则:留空,交给 dataclass 自身的默认值。
|
|
77
|
+
return cls(**kwargs)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""corespine.conformance —— 可复用 conformance 基座(机制,不含具体不变量)。"""
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""可复用的 conformance 基座(机制,不含任何具体不变量)。
|
|
2
|
+
|
|
3
|
+
一个 Protocol -> 一套共享测试:把"每个实现都必须跑同一套不变量"做成可参数化的机制。
|
|
4
|
+
给定 (1) 一份实现注册表(名字 -> 工厂)与 (2) 一个不变量包(名字 -> 检查函数),
|
|
5
|
+
ConformanceSuite 把二者【绑成笛卡尔积】,逐 (实现 × 不变量) 执行,任一失败即定位到
|
|
6
|
+
具体格子。
|
|
7
|
+
|
|
8
|
+
这正是 ragspine tests/conformance 的泛化:让"敢放手让第三方填广度、却让脊柱不变量
|
|
9
|
+
烂不掉"成立——没过 conformance 的 adapter 直接 CI 红,而非生产事故。
|
|
10
|
+
|
|
11
|
+
【机制,非保证】:本模块【不】定义任何具体不变量。anti-fabrication / provenance /
|
|
12
|
+
isolation 这些是各 app 自己的事(ADR 0001 D6),由 app 把自己的 InvariantPack 喂进来;
|
|
13
|
+
harness 只负责"跑全套 + 报告哪个格子坏了"。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from functools import partial
|
|
21
|
+
from typing import Generic, TypeVar
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
# 一个不变量检查:拿到一个【新构造的实现实例】,验证通过则正常返回、违反则抛异常。
|
|
26
|
+
# 检查应只读、与实现内部无关(只验外部可观测行为)。
|
|
27
|
+
Invariant = Callable[[T], None]
|
|
28
|
+
|
|
29
|
+
# 一个工厂:无参构造一个全新实例(每个格子各拿一个,杜绝实现间状态串味)。
|
|
30
|
+
Factory = Callable[[], T]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class InvariantPack(Generic[T]):
|
|
35
|
+
"""一组具名不变量。app 用它装自己的保证;harness 只负责跑。
|
|
36
|
+
|
|
37
|
+
add() 返回自身,便于链式登记:InvariantPack("x").add(...).add(...)。
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
name: str
|
|
41
|
+
invariants: dict[str, Invariant[T]] = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
def add(self, name: str, check: Invariant[T]) -> InvariantPack[T]:
|
|
44
|
+
self.invariants[name] = check
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
def names(self) -> list[str]:
|
|
48
|
+
return list(self.invariants)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class CaseResult:
|
|
53
|
+
"""单个 (实现 × 不变量) 格子的执行结果。"""
|
|
54
|
+
|
|
55
|
+
impl: str
|
|
56
|
+
invariant: str
|
|
57
|
+
passed: bool
|
|
58
|
+
error: str | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ConformanceSuite(Generic[T]):
|
|
62
|
+
"""把 实现注册表 × 不变量包 绑成可执行 / 可参数化的 conformance 套件。"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, implementations: dict[str, Factory[T]], pack: InvariantPack[T]) -> None:
|
|
65
|
+
if not implementations:
|
|
66
|
+
raise ValueError("conformance 套件至少需要一个实现")
|
|
67
|
+
self._impls = dict(implementations)
|
|
68
|
+
self._pack = pack
|
|
69
|
+
|
|
70
|
+
def cases(self) -> list[tuple[str, str]]:
|
|
71
|
+
"""全部 (实现名, 不变量名) 组合——可直接喂给 pytest.mark.parametrize。"""
|
|
72
|
+
return [(impl, inv) for impl in self._impls for inv in self._pack.names()]
|
|
73
|
+
|
|
74
|
+
def ids(self) -> list[str]:
|
|
75
|
+
"""与 cases() 对齐的可读用例 id(形如 impl/invariant)。"""
|
|
76
|
+
return [f"{impl}/{inv}" for impl, inv in self.cases()]
|
|
77
|
+
|
|
78
|
+
def parametrize_kwargs(self) -> dict[str, object]:
|
|
79
|
+
"""产出可直接喂给 pytest.mark.parametrize(**...) 的 kwargs(pytest-free)。
|
|
80
|
+
|
|
81
|
+
本方法【只返回纯数据】(str / list[Callable] / list[str]),core 不 import
|
|
82
|
+
pytest——pytest 依赖留在消费者的测试里。返回三键:
|
|
83
|
+
|
|
84
|
+
- argnames: 固定为 "case"(单形参,值是一个【已绑定好该格子的零参 thunk】);
|
|
85
|
+
- argvalues: 每格一个 thunk,调用即跑 check(impl, inv)——满足则静默返回,违反则
|
|
86
|
+
原样抛出(通常是 AssertionError);
|
|
87
|
+
- ids: 与 argvalues 对齐的可读用例 id,形如 "impl-inv"。
|
|
88
|
+
|
|
89
|
+
这样消费者的整套 glue 收敛成两行,无需手写 cases() 遍历或 fixture(params=...):
|
|
90
|
+
|
|
91
|
+
@pytest.mark.parametrize(**suite.parametrize_kwargs())
|
|
92
|
+
def test_conformance(case):
|
|
93
|
+
case()
|
|
94
|
+
"""
|
|
95
|
+
# partial 立即绑定 impl/inv(规避 lambda 闭包晚绑定),且类型明确(mypy --strict 友好)。
|
|
96
|
+
argvalues: list[Callable[[], None]] = [
|
|
97
|
+
partial(self.check, impl, inv) for impl, inv in self.cases()
|
|
98
|
+
]
|
|
99
|
+
ids = [f"{impl}-{inv}" for impl, inv in self.cases()]
|
|
100
|
+
return {"argnames": "case", "argvalues": argvalues, "ids": ids}
|
|
101
|
+
|
|
102
|
+
def check(self, impl: str, invariant: str) -> None:
|
|
103
|
+
"""跑单个格子:新建该实现实例,对其执行该不变量(失败则原样抛出)。
|
|
104
|
+
|
|
105
|
+
每个格子都【新建实例】,杜绝实现间状态串味——与 ragspine 每用例新空库一致。
|
|
106
|
+
"""
|
|
107
|
+
instance = self._impls[impl]()
|
|
108
|
+
self._pack.invariants[invariant](instance)
|
|
109
|
+
|
|
110
|
+
def run(self) -> list[CaseResult]:
|
|
111
|
+
"""跑全部格子并收集结果(不抛;用于离线自检 / 报告)。"""
|
|
112
|
+
results: list[CaseResult] = []
|
|
113
|
+
for impl, inv in self.cases():
|
|
114
|
+
try:
|
|
115
|
+
self.check(impl, inv)
|
|
116
|
+
results.append(CaseResult(impl, inv, True))
|
|
117
|
+
except Exception as exc: # noqa: BLE001 — 收集失败,不中断其余格子
|
|
118
|
+
results.append(CaseResult(impl, inv, False, f"{type(exc).__name__}: {exc}"))
|
|
119
|
+
return results
|
|
120
|
+
|
|
121
|
+
def passed(self) -> bool:
|
|
122
|
+
"""便捷:全部格子是否通过(离线自检用)。"""
|
|
123
|
+
return all(result.passed for result in self.run())
|
corespine/errors.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""家族统一异常 + 错误归一(domain-neutral 原语)。
|
|
2
|
+
|
|
3
|
+
证据(rule of three):ragspine 的 `JobError(stage, retryable)` 与 `_error_dict_from_exc`、
|
|
4
|
+
pdfspine 的 `error.kind()` / `LimitKind`——两个消费者都在各自重造"带机器可判别码的异常 +
|
|
5
|
+
把异常拍成可序列化 dict"这同一块稳定面。这里只把【恰好那块】提上来:
|
|
6
|
+
|
|
7
|
+
- 一个统一基类 `CorespineError`:带稳定、可 grep 的 `code` 与 `retryable` 标志;
|
|
8
|
+
- 一个归一函数 `error_to_dict`:把【任意】异常拍成可被 `json.dumps` 序列化的 dict。
|
|
9
|
+
|
|
10
|
+
这里是【机制】:基类与归一形状。具体有哪些 code、哪条可重试,由各 app 自己绑
|
|
11
|
+
(ADR 0001 D6)。core 不预设任何业务语义,只给两个自然子类做示范。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CorespineError(Exception):
|
|
18
|
+
"""Spine 家族统一异常基类。
|
|
19
|
+
|
|
20
|
+
`code` 是稳定、机器可 grep 的判别符(子类覆盖,如 "config.invalid"/"seam.unknown");
|
|
21
|
+
`retryable` 标记此错是否值得重试。两者都既可在子类作类属性覆盖,也可在构造时实例级覆盖。
|
|
22
|
+
任意关键字参数收进 `self.context`(普通 dict),用于携带可序列化的诊断上下文。
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
code: str = "error"
|
|
26
|
+
retryable: bool = False
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
message: str = "",
|
|
31
|
+
*,
|
|
32
|
+
code: str | None = None,
|
|
33
|
+
retryable: bool | None = None,
|
|
34
|
+
**context: object,
|
|
35
|
+
) -> None:
|
|
36
|
+
super().__init__(message)
|
|
37
|
+
# 仅在显式传入时做实例级覆盖,否则沿用类属性默认。
|
|
38
|
+
if code is not None:
|
|
39
|
+
self.code = code
|
|
40
|
+
if retryable is not None:
|
|
41
|
+
self.retryable = retryable
|
|
42
|
+
self.context: dict[str, object] = dict(context)
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict[str, object]:
|
|
45
|
+
"""归一为可被 json.dumps 序列化的 dict。"""
|
|
46
|
+
return {
|
|
47
|
+
"type": type(self).__name__,
|
|
48
|
+
"code": self.code,
|
|
49
|
+
"message": str(self),
|
|
50
|
+
"retryable": self.retryable,
|
|
51
|
+
"context": dict(self.context),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def error_to_dict(exc: BaseException) -> dict[str, object]:
|
|
56
|
+
"""把【任意】异常归一为可序列化 dict(统一错误形状)。
|
|
57
|
+
|
|
58
|
+
CorespineError 走其 `to_dict()`;其余异常给出同形状的保守默认
|
|
59
|
+
(code="error"、retryable=False、context={}),便于跨进程/跨缝统一处理与日志。
|
|
60
|
+
"""
|
|
61
|
+
if isinstance(exc, CorespineError):
|
|
62
|
+
return exc.to_dict()
|
|
63
|
+
return {
|
|
64
|
+
"type": type(exc).__name__,
|
|
65
|
+
"code": "error",
|
|
66
|
+
"message": str(exc),
|
|
67
|
+
"retryable": False,
|
|
68
|
+
"context": {},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ConfigError(CorespineError):
|
|
73
|
+
"""配置非法(缺必填、类型不符等)。"""
|
|
74
|
+
|
|
75
|
+
code = "config.invalid"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SeamError(CorespineError):
|
|
79
|
+
"""缝相关错误(未知实现、工厂构造失败等)。"""
|
|
80
|
+
|
|
81
|
+
code = "seam.unknown"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""corespine.llm —— LLM 缝:LLMProvider 协议 + 离线确定性 MockProvider。"""
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""LLM 缝:LLMProvider 协议(OpenAI chat-completions 规范)+ 确定性离线 MockProvider。
|
|
2
|
+
|
|
3
|
+
【对外唯一规范 = OpenAI chat completions 形状】——输入是 OpenAI 风格的 messages(list[dict]:
|
|
4
|
+
role / content / 可带 tool_calls / tool_call_id)与 OpenAI function-tool 形状的 tools;输出是
|
|
5
|
+
OpenAI 形状的 ChatCompletion(choices[].message.{content, tool_calls[].function.{name, arguments}}、
|
|
6
|
+
finish_reason、usage.{prompt_tokens, completion_tokens, total_tokens})。
|
|
7
|
+
|
|
8
|
+
为什么以 OpenAI 形状作规范:它已是事实标准(LiteLLM / OpenRouter / vLLM / Ollama / Together /
|
|
9
|
+
Groq 等都吐这个),所以"规范化到 OpenAI"= 兼容面最广;用户永远只按 OpenAI 规范写代码。后端是
|
|
10
|
+
Anthropic 或其它非 OpenAI 兼容模型时,由各 app 的适配器【在内部转成 OpenAI 形状再吐出】,用户无感。
|
|
11
|
+
domain-neutral:这是当下 LLM 的通用 wire 形状,不含任何 RAG / agent 特定概念;tool-use 循环仍归 app。
|
|
12
|
+
|
|
13
|
+
核心 import 本模块零 SDK;真实 provider(Anthropic / OpenAI 等)由各 app 在自己的缝里延迟 import
|
|
14
|
+
接入。MockProvider 是离线确定性默认:同样的 messages 恒定产出同样的响应(纯函数、零网络、零 key),
|
|
15
|
+
让"装上即可端到端跑"成立、测试可复现;它【不】伪造 tool_calls(离线默认不假装会 function-calling)。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Any, Protocol, runtime_checkable
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class FunctionCall:
|
|
27
|
+
"""工具调用的函数部分(OpenAI 形状):name + arguments(JSON 字符串,与 OpenAI 完全一致)。"""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
arguments: str = "{}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class ToolCall:
|
|
35
|
+
"""一次工具调用(OpenAI 形状):id + type + function。"""
|
|
36
|
+
|
|
37
|
+
id: str
|
|
38
|
+
function: FunctionCall
|
|
39
|
+
type: str = "function"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class ResponseMessage:
|
|
44
|
+
"""模型返回的消息(OpenAI 形状):role + content(可空)+ tool_calls(可空)。"""
|
|
45
|
+
|
|
46
|
+
role: str = "assistant"
|
|
47
|
+
content: str | None = None
|
|
48
|
+
tool_calls: tuple[ToolCall, ...] | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class Choice:
|
|
53
|
+
"""一个候选(OpenAI 形状):index + message + finish_reason(stop / tool_calls / length …)。"""
|
|
54
|
+
|
|
55
|
+
index: int
|
|
56
|
+
message: ResponseMessage
|
|
57
|
+
finish_reason: str = "stop"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class Usage:
|
|
62
|
+
"""token 用量(OpenAI 形状):prompt / completion / total。"""
|
|
63
|
+
|
|
64
|
+
prompt_tokens: int = 0
|
|
65
|
+
completion_tokens: int = 0
|
|
66
|
+
total_tokens: int = 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class ChatCompletion:
|
|
71
|
+
"""一次对话补全结果(OpenAI 形状):choices + usage + 元数据,字段与 OpenAI 完全一致。"""
|
|
72
|
+
|
|
73
|
+
choices: tuple[Choice, ...]
|
|
74
|
+
usage: Usage | None = None
|
|
75
|
+
model: str = ""
|
|
76
|
+
id: str = ""
|
|
77
|
+
created: int = 0
|
|
78
|
+
object: str = "chat.completion"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@runtime_checkable
|
|
82
|
+
class LLMProvider(Protocol):
|
|
83
|
+
"""provider 协议(OpenAI 规范):给 OpenAI 形状的 messages(可选 tools),拿回 OpenAI ChatCompletion。"""
|
|
84
|
+
|
|
85
|
+
def chat(
|
|
86
|
+
self, messages: list[dict[str, Any]], *, tools: list[dict[str, Any]] | None = None
|
|
87
|
+
) -> ChatCompletion: ...
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _text_of(message: dict[str, Any]) -> str:
|
|
91
|
+
"""从一条 OpenAI message dict 取纯文本内容(content 为 None 时视作空串)。"""
|
|
92
|
+
return str(message.get("content") or "")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class MockProvider:
|
|
96
|
+
"""确定性离线 provider:零网络、零 key,输出 OpenAI 形状的 ChatCompletion,由 messages 派生、可复现。
|
|
97
|
+
|
|
98
|
+
回显最后一条 user 消息,并附一段由【整段对话】计算的稳定 hex 指纹——使不同对话(含不同 system /
|
|
99
|
+
历史)产出不同、但对同一对话恒定的文本;既便于断言,又不引入随机性。绝不伪造 tool_calls:离线
|
|
100
|
+
默认不假装会 function-calling(真工具调用需接真实 provider)。
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, *, prefix: str = "mock") -> None:
|
|
104
|
+
self._prefix = prefix
|
|
105
|
+
|
|
106
|
+
def chat(
|
|
107
|
+
self, messages: list[dict[str, Any]], *, tools: list[dict[str, Any]] | None = None
|
|
108
|
+
) -> ChatCompletion:
|
|
109
|
+
# 指纹覆盖整段对话(role + content),\x00/\x01 作分隔杜绝拼接歧义;取前 12 位 hex。
|
|
110
|
+
joined = "\x00".join(f"{m.get('role', '')}\x01{_text_of(m)}" for m in messages)
|
|
111
|
+
digest = hashlib.sha256(joined.encode()).hexdigest()[:12]
|
|
112
|
+
last_user = next((_text_of(m) for m in reversed(messages) if m.get("role") == "user"), "")
|
|
113
|
+
text = f"[{self._prefix}:{digest}] {last_user.strip()}"
|
|
114
|
+
usage = Usage(
|
|
115
|
+
prompt_tokens=len(joined),
|
|
116
|
+
completion_tokens=len(text),
|
|
117
|
+
total_tokens=len(joined) + len(text),
|
|
118
|
+
)
|
|
119
|
+
choice = Choice(index=0, message=ResponseMessage(role="assistant", content=text), finish_reason="stop")
|
|
120
|
+
return ChatCompletion(choices=(choice,), usage=usage, model=f"{self._prefix}", id=f"chatcmpl-{digest}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""corespine.observability —— 隐私安全 trace:只记元数据,拒绝正文。"""
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""隐私安全的可观测性原语:TraceSink 协议 + 默认的进程内实现。
|
|
2
|
+
|
|
3
|
+
约定("trace 按受限数据对待"):trace 只允许记【非敏感元数据】——事件 code、计数、
|
|
4
|
+
耗时、布尔标志这类。绝不记原始答案正文 / 字段取值 / chunk 正文——它们一旦进 trace
|
|
5
|
+
就成了受限数据的泄露面。
|
|
6
|
+
|
|
7
|
+
默认实现 InProcessPrivacyTraceSink 把这条约定做成"构造即保证":任何带禁词键
|
|
8
|
+
(answer / value / text / content / ...)的载荷会被【直接拒绝】(抛 TraceError),
|
|
9
|
+
而不是悄悄记下去。隐私 by construction,而非靠 reviewer 自觉。
|
|
10
|
+
|
|
11
|
+
domain-neutral:不预设任何具体事件 code;每个 app 用自己的 code 词表,harness 只管
|
|
12
|
+
"只记元数据、拒绝正文"这条机制。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Protocol, runtime_checkable
|
|
19
|
+
|
|
20
|
+
from corespine.errors import CorespineError
|
|
21
|
+
|
|
22
|
+
# 禁止出现在 trace 载荷里的键(承载受限正文 / 取值的字段名,归一为小写后比对)。
|
|
23
|
+
# 命中任一即拒绝整条 trace——宁可报错,也不让正文流进可观测链路。
|
|
24
|
+
FORBIDDEN_KEYS = frozenset(
|
|
25
|
+
{
|
|
26
|
+
"answer",
|
|
27
|
+
"value",
|
|
28
|
+
"text",
|
|
29
|
+
"content",
|
|
30
|
+
"prompt",
|
|
31
|
+
"completion",
|
|
32
|
+
"chunk",
|
|
33
|
+
"chunk_text",
|
|
34
|
+
"body",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TraceError(CorespineError):
|
|
40
|
+
"""trace 载荷违反隐私约定(携带受限正文 / 取值字段)时抛出。
|
|
41
|
+
|
|
42
|
+
继承家族统一基类 CorespineError,带稳定可 grep 的 code,便于跨缝统一捕获 / 归一。
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
code = "trace.forbidden"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class TraceEvent:
|
|
50
|
+
"""一条被记录的 trace:事件 code + 非敏感字段快照(只读)。"""
|
|
51
|
+
|
|
52
|
+
code: str
|
|
53
|
+
fields: dict[str, object] = field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@runtime_checkable
|
|
57
|
+
class TraceSink(Protocol):
|
|
58
|
+
"""trace 出口的最小结构接口:发射一条事件(code + 非敏感字段)。"""
|
|
59
|
+
|
|
60
|
+
def emit(self, code: str, **fields: object) -> None: ...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class InProcessPrivacyTraceSink:
|
|
64
|
+
"""进程内默认 TraceSink:把事件存进内存列表,且拒绝任何携带受限内容的载荷。
|
|
65
|
+
|
|
66
|
+
隐私 by construction:emit 时先扫字段键,命中 FORBIDDEN_KEYS 立即抛 TraceError、
|
|
67
|
+
绝不记录;通过校验的事件以 TraceEvent 追加到内部列表。只记 code / 计数 / 耗时 / 标志。
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self) -> None:
|
|
71
|
+
self._events: list[TraceEvent] = []
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def events(self) -> list[TraceEvent]:
|
|
75
|
+
"""已记录事件的只读视图(返回副本,外部改动不污染内部状态)。"""
|
|
76
|
+
return list(self._events)
|
|
77
|
+
|
|
78
|
+
def codes(self) -> list[str]:
|
|
79
|
+
"""已记录事件的 code 序列(按记录顺序)。"""
|
|
80
|
+
return [event.code for event in self._events]
|
|
81
|
+
|
|
82
|
+
def emit(self, code: str, **fields: object) -> None:
|
|
83
|
+
"""记一条 trace;载荷含受限字段即抛 TraceError(不记录)。"""
|
|
84
|
+
offending = sorted(k for k in fields if k.strip().lower() in FORBIDDEN_KEYS)
|
|
85
|
+
if offending:
|
|
86
|
+
raise TraceError(
|
|
87
|
+
f"trace 载荷含受限字段 {offending}:trace 只记 code / 计数 / 耗时,"
|
|
88
|
+
"不得携带答案正文 / 字段取值 / chunk 正文。"
|
|
89
|
+
)
|
|
90
|
+
self._events.append(TraceEvent(code=code, fields=dict(fields)))
|
corespine/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""corespine.queue —— 任务队列缝:TaskQueue 协议 + 同步 FakeQueue 默认。"""
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""任务队列缝:TaskQueue 协议 + 同步内联的 FakeQueue 默认实现。
|
|
2
|
+
|
|
3
|
+
domain-neutral 的 worker / job 队列抽象。Protocol 只约定 enqueue / get 两件事;真实
|
|
4
|
+
后端(RQ+Redis / Celery 等)由各 app 在自己的缝里延迟 import 接入,核心零依赖。
|
|
5
|
+
|
|
6
|
+
FakeQueue 是离线默认:enqueue 时【同步内联】执行 job 并记录结果 / 失败,不需要任何
|
|
7
|
+
外部服务,让测试与离线流程可复现。job 函数签名约定为 fn(payload: dict) -> dict;
|
|
8
|
+
失败被捕获记进 JobStatus(不外抛),与真实异步后端"失败也是一种终态"的语义一致。
|
|
9
|
+
job 既可传可调用对象,也可传点路径字符串(末段为可调用对象)。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import uuid
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from importlib import import_module
|
|
18
|
+
from typing import Any, Protocol, cast, runtime_checkable
|
|
19
|
+
|
|
20
|
+
# job 终态常量(与常见异步后端语义对齐)。
|
|
21
|
+
JOB_FINISHED = "finished"
|
|
22
|
+
JOB_FAILED = "failed"
|
|
23
|
+
|
|
24
|
+
# job 函数:纯可序列化 dict 进、dict 出。
|
|
25
|
+
JobFunc = Callable[[dict[str, Any]], dict[str, Any]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class JobStatus:
|
|
30
|
+
"""一个 job 的状态快照:id + 状态 + 结果 / 错误(成功记 result,失败记 error)。"""
|
|
31
|
+
|
|
32
|
+
id: str
|
|
33
|
+
status: str
|
|
34
|
+
result: dict[str, Any] | None = None
|
|
35
|
+
error: dict[str, Any] | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@runtime_checkable
|
|
39
|
+
class TaskQueue(Protocol):
|
|
40
|
+
"""任务队列的最小结构接口:投递一个 job、按 id 查状态。"""
|
|
41
|
+
|
|
42
|
+
def enqueue(
|
|
43
|
+
self,
|
|
44
|
+
func: JobFunc | str,
|
|
45
|
+
payload: dict[str, Any],
|
|
46
|
+
*,
|
|
47
|
+
job_id: str | None = None,
|
|
48
|
+
) -> str: ...
|
|
49
|
+
|
|
50
|
+
def get(self, job_id: str) -> JobStatus | None: ...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _resolve(func: JobFunc | str) -> JobFunc:
|
|
54
|
+
"""可调用对象原样;点路径字符串 -> 末段可调用对象。"""
|
|
55
|
+
if callable(func):
|
|
56
|
+
return func
|
|
57
|
+
module_path, _, attr = func.rpartition(".")
|
|
58
|
+
if not module_path:
|
|
59
|
+
raise ValueError(f"func 必须是可调用对象或点路径:{func!r}")
|
|
60
|
+
mod = import_module(module_path)
|
|
61
|
+
return cast("JobFunc", getattr(mod, attr))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class FakeQueue:
|
|
65
|
+
"""同步内存队列:enqueue 时内联执行 job(离线 / 测试用,无需任何外部服务)。"""
|
|
66
|
+
|
|
67
|
+
def __init__(self) -> None:
|
|
68
|
+
self._jobs: dict[str, JobStatus] = {}
|
|
69
|
+
|
|
70
|
+
def enqueue(
|
|
71
|
+
self,
|
|
72
|
+
func: JobFunc | str,
|
|
73
|
+
payload: dict[str, Any],
|
|
74
|
+
*,
|
|
75
|
+
job_id: str | None = None,
|
|
76
|
+
) -> str:
|
|
77
|
+
# 幂等:显式 job_id 且已知 -> 直接返回,不重跑。
|
|
78
|
+
if job_id is not None and job_id in self._jobs:
|
|
79
|
+
return job_id
|
|
80
|
+
jid = job_id or uuid.uuid4().hex[:12]
|
|
81
|
+
try:
|
|
82
|
+
result = _resolve(func)(payload)
|
|
83
|
+
self._jobs[jid] = JobStatus(id=jid, status=JOB_FINISHED, result=result)
|
|
84
|
+
except Exception as exc: # noqa: BLE001 — 内联失败记进状态,不外抛
|
|
85
|
+
self._jobs[jid] = JobStatus(
|
|
86
|
+
id=jid,
|
|
87
|
+
status=JOB_FAILED,
|
|
88
|
+
error={"type": type(exc).__name__, "message": str(exc)},
|
|
89
|
+
)
|
|
90
|
+
return jid
|
|
91
|
+
|
|
92
|
+
def get(self, job_id: str) -> JobStatus | None:
|
|
93
|
+
return self._jobs.get(job_id)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""corespine.seam —— 缝注册表:name->factory 解析 + entry-point 自动发现。"""
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""缝(seam)注册表:把"选哪个实现"从改代码降为一个 spec 字符串。
|
|
2
|
+
|
|
3
|
+
这是 ragspine `make_vector_store` 的泛化形式——domain-neutral,不绑任何具体缝。
|
|
4
|
+
一个 Registry 实例代表一条缝(如 "vector_store" / "llm" / "queue"):
|
|
5
|
+
|
|
6
|
+
- register(name, factory):登记一个 名字->工厂 的内置实现;
|
|
7
|
+
- make(spec, **kwargs):大小写/留白/连字符不敏感地把 spec 解析到工厂并构造实例;
|
|
8
|
+
- 内置名找不到时,回落到 importlib.metadata 的 entry-point 自动发现
|
|
9
|
+
(group 形如 "corespine.<seam>"),让第三方装包即扩展,无需改核心代码;
|
|
10
|
+
- 仍找不到则抛 ValueError,把【当前可用的全部名字】列清楚,绝不让人猜。
|
|
11
|
+
|
|
12
|
+
lazy_extra_import 是配套助手:延迟 import 一个可选依赖,缺失时把裸 ImportError
|
|
13
|
+
翻译成"pip install <pkg>[<extra>]"的友好提示——支撑"离线精简默认 + 可选重依赖"
|
|
14
|
+
的范式(核心默认路径零重依赖,真实后端的 SDK 仅在选用时才 import)。
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import importlib
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from importlib import metadata
|
|
22
|
+
from typing import Any, Generic, TypeVar, cast
|
|
23
|
+
|
|
24
|
+
T = TypeVar("T")
|
|
25
|
+
|
|
26
|
+
# 一个工厂:任意关键字参数 -> 一个实现实例。
|
|
27
|
+
Factory = Callable[..., T]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _normalize(spec: str) -> str:
|
|
31
|
+
"""名字归一:去首尾留白 + 转小写 + 把连字符/空格统一成下划线。
|
|
32
|
+
|
|
33
|
+
使 "In-Process" / " in_process " / "IN PROCESS" 都解析到同一个键。
|
|
34
|
+
"""
|
|
35
|
+
return spec.strip().lower().replace("-", "_").replace(" ", "_")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Registry(Generic[T]):
|
|
39
|
+
"""一条缝的 名字->工厂 注册表 + spec 解析 + entry-point 自动发现。"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, seam: str) -> None:
|
|
42
|
+
# seam 名既用于 entry-point group("corespine.<seam>"),也用于报错信息。
|
|
43
|
+
self._seam = seam
|
|
44
|
+
self._factories: dict[str, Factory[T]] = {}
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def seam(self) -> str:
|
|
48
|
+
return self._seam
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def group(self) -> str:
|
|
52
|
+
"""entry-point 自动发现使用的 group 名。"""
|
|
53
|
+
return f"corespine.{self._seam}"
|
|
54
|
+
|
|
55
|
+
def register(self, name: str, factory: Factory[T]) -> None:
|
|
56
|
+
"""登记一个内置实现;名字归一后入表(同名重复登记则后者覆盖)。"""
|
|
57
|
+
self._factories[_normalize(name)] = factory
|
|
58
|
+
|
|
59
|
+
def names(self) -> list[str]:
|
|
60
|
+
"""当前【全部可用】名字:内置 + entry-point 发现,去重后按字典序。"""
|
|
61
|
+
return sorted(set(self._factories) | set(self._discover()))
|
|
62
|
+
|
|
63
|
+
def _discover(self) -> dict[str, metadata.EntryPoint]:
|
|
64
|
+
"""从 importlib.metadata entry points 发现第三方实现(group=corespine.<seam>)。
|
|
65
|
+
|
|
66
|
+
延迟到解析时才扫,不在 import 期付出代价;每个 ep 的 load() 也按需触发。
|
|
67
|
+
返回 归一名 -> EntryPoint(其 load() 给出真正的工厂可调用对象)。
|
|
68
|
+
"""
|
|
69
|
+
eps = metadata.entry_points(group=self.group)
|
|
70
|
+
return {_normalize(ep.name): ep for ep in eps}
|
|
71
|
+
|
|
72
|
+
def make(self, spec: str, **kwargs: Any) -> T:
|
|
73
|
+
"""把 spec 解析到工厂并构造实例(大小写/留白/连字符不敏感)。
|
|
74
|
+
|
|
75
|
+
解析顺序:内置注册 -> entry-point 发现 -> 抛 ValueError(列清可用名)。
|
|
76
|
+
"""
|
|
77
|
+
key = _normalize(spec)
|
|
78
|
+
factory = self._factories.get(key)
|
|
79
|
+
if factory is not None:
|
|
80
|
+
return factory(**kwargs)
|
|
81
|
+
# 回落:entry-point 自动发现(第三方装包即扩展,无需改核心)。
|
|
82
|
+
discovered = self._discover()
|
|
83
|
+
ep = discovered.get(key)
|
|
84
|
+
if ep is not None:
|
|
85
|
+
# entry-point 的 load() 返回 Any;约定它给出本缝的工厂,cast 回 Factory[T]。
|
|
86
|
+
factory = cast("Factory[T]", ep.load())
|
|
87
|
+
return factory(**kwargs)
|
|
88
|
+
raise ValueError(self._unknown_message(spec, discovered))
|
|
89
|
+
|
|
90
|
+
def _unknown_message(self, spec: str, discovered: dict[str, metadata.EntryPoint]) -> str:
|
|
91
|
+
available = sorted(set(self._factories) | set(discovered))
|
|
92
|
+
listed = ", ".join(available) if available else "(无)"
|
|
93
|
+
return (
|
|
94
|
+
f"未知的 {self._seam} spec:{spec!r}。当前可用:{listed}"
|
|
95
|
+
f"(内置注册或经 entry-point group {self.group!r} 发现;"
|
|
96
|
+
"第三方实现可装包扩展)。"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def lazy_extra_import(module: str, *, pkg: str, extra: str) -> Any:
|
|
101
|
+
"""延迟 import 一个可选依赖;缺失时给出"pip install <pkg>[<extra>]"友好提示。
|
|
102
|
+
|
|
103
|
+
用于"离线精简默认 + 可选重依赖"的范式:核心默认路径零重依赖,真实后端的 SDK
|
|
104
|
+
仅在选用该 adapter 时才 import。把裸 ImportError 翻译成可直接照做的安装指引,
|
|
105
|
+
而不是让调用方对着 ModuleNotFoundError 自己猜该装哪个 extra。
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
return importlib.import_module(module)
|
|
109
|
+
except ImportError as exc:
|
|
110
|
+
raise ImportError(
|
|
111
|
+
f"缺少可选依赖 {module!r}:请先 `pip install {pkg}[{extra}]` 再重试。"
|
|
112
|
+
) from exc
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: corespine
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Spine 家族的薄共享核:domain-neutral 底层原语(缝注册表 / observability / LLM 缝 / config / queue / conformance 基座)。
|
|
5
|
+
Author: lin han
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Keywords: conformance,framework-free,llm-provider,observability,offline,registry,seam,task-queue
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# corespine
|
|
25
|
+
|
|
26
|
+
Spine 家族的**薄共享核**(见 [ADR 0001](../docs/adr/0001-spine-family-boundaries-and-dependency-direction.md))。
|
|
27
|
+
只装 **domain-neutral 的底层原语**——既不属于 RAG 也不属于 agent 的稳定地基,被 `ragspine` /
|
|
28
|
+
`agentspine` 兄弟包各自依赖,**不**含任何它们的领域概念。
|
|
29
|
+
|
|
30
|
+
> 刻意地薄。按证据(rule of three)增长,不预先造框架。详见 [`CLAUDE.md`](CLAUDE.md) 宪章。
|
|
31
|
+
|
|
32
|
+
## 缝的元模式
|
|
33
|
+
|
|
34
|
+
每条缝都长一个样,核心 import 零 SDK、离线可跑:
|
|
35
|
+
|
|
36
|
+
**Protocol + 离线确定性默认 + `Registry` / `make_*` 工厂 + 参数化 conformance**
|
|
37
|
+
|
|
38
|
+
## 里面有什么
|
|
39
|
+
|
|
40
|
+
| 模块 | 原语 |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `seam/registry.py` | `Registry`:name→factory 解析(大小写/留白/连字符不敏感)+ entry-point 自动发现(`corespine.<seam>` group)+ 未知 spec 列清可用名 + `lazy_extra_import`(缺 extra 给"pip install …"友好提示) |
|
|
43
|
+
| `observability/trace.py` | `TraceSink` 协议 + `InProcessPrivacyTraceSink`:只记 code/计数/耗时,**拒绝**任何携带正文(answer/value/text/content…)的载荷 |
|
|
44
|
+
| `llm/provider.py` | `LLMProvider` 协议 + 离线确定性 `MockProvider`(零网络、零 key、可复现) |
|
|
45
|
+
| `config/env.py` | `load_from_env`:把 `PREFIX_*` 环境变量读进一个 frozen dataclass(范式同 ragspine `from_env`) |
|
|
46
|
+
| `queue/task_queue.py` | `TaskQueue` 协议 + `FakeQueue`:同步内联执行 + 记录,离线/测试用 |
|
|
47
|
+
| `conformance/harness.py` | `ConformanceSuite` × `InvariantPack`:把"实现 × 不变量"绑成笛卡尔积逐格执行(**机制**,具体不变量由各 app 自己绑) |
|
|
48
|
+
|
|
49
|
+
## 本地开发(始终从包根)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv venv .venv
|
|
53
|
+
VIRTUAL_ENV="$(pwd)/.venv" uv pip install -e ".[dev]"
|
|
54
|
+
.venv/bin/python -m pytest -q
|
|
55
|
+
.venv/bin/python -c "import corespine"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 30 秒上手
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from corespine import Registry, MockProvider, InProcessPrivacyTraceSink, FakeQueue
|
|
62
|
+
|
|
63
|
+
# 缝:一个 spec 选实现(大小写/留白不敏感;找不到列清可用名;还能 entry-point 自动发现)
|
|
64
|
+
reg: Registry = Registry("llm")
|
|
65
|
+
reg.register("mock", lambda **kw: MockProvider(**kw))
|
|
66
|
+
provider = reg.make(" MOCK ")
|
|
67
|
+
# OpenAI chat-completions 规范:messages 进,OpenAI 形状的 ChatCompletion 出(确定性可复现)
|
|
68
|
+
out = provider.chat([{"role": "user", "content": "hello"}])
|
|
69
|
+
print(out.choices[0].message.content)
|
|
70
|
+
|
|
71
|
+
# 隐私 trace:只记元数据;塞正文会被直接拒绝(raise TraceError)
|
|
72
|
+
sink = InProcessPrivacyTraceSink()
|
|
73
|
+
sink.emit("retrieve", count=3, took_ms=12)
|
|
74
|
+
|
|
75
|
+
# 任务队列:同步内联执行
|
|
76
|
+
q = FakeQueue()
|
|
77
|
+
jid = q.enqueue(lambda p: {"doubled": p["n"] * 2}, {"n": 21})
|
|
78
|
+
print(q.get(jid).result) # {'doubled': 42}
|
|
79
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
corespine/__init__.py,sha256=iSuKpiHK7eZoTatWSniAIBg8Y10gGgOwT4eSxh09quw,1754
|
|
2
|
+
corespine/errors.py,sha256=ppMIlz46lIoOXD1MeQ2vOrsBkkkLwRxQejrLUUmUyBM,2895
|
|
3
|
+
corespine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
corespine/config/__init__.py,sha256=wLleoMh5Hi7l-_fc0UvEUl1FlVKwTqQZBKEtKbskkms,85
|
|
5
|
+
corespine/config/env.py,sha256=VWTmBFfzKTu2l0-4Usc6hYmhNZ8df3nnNcshyzNG22M,3112
|
|
6
|
+
corespine/conformance/__init__.py,sha256=tvnJwXVWcY1H48qV80tnNdYPmr5kkHhgsiSx0Ly5iD0,97
|
|
7
|
+
corespine/conformance/harness.py,sha256=AXnUhTwrCN_iGsiYF95GKiEFWkhjclyGkIdTRE1U1dc,5301
|
|
8
|
+
corespine/llm/__init__.py,sha256=BxI-sSWYK7vI95z9R806LMc6LVixXBg9eHH1_PKhqtg,88
|
|
9
|
+
corespine/llm/provider.py,sha256=t1Kv16wdkygA5D-E2kA3ocQXtOL50DEYZ2vZOo6mHMU,4872
|
|
10
|
+
corespine/observability/__init__.py,sha256=2G9gKi8WGpa57BUUcB1mB0CQIUMH6UDfT9zNrASi56I,88
|
|
11
|
+
corespine/observability/trace.py,sha256=K-Qc1sznB6vSLOHl0WMEMvljzm63A6VQiRDnambW0Vw,3337
|
|
12
|
+
corespine/queue/__init__.py,sha256=4g0cb626ensd5ATrMaSiwPBzrNb1K4g3Ye9kOTzxcFs,91
|
|
13
|
+
corespine/queue/task_queue.py,sha256=nWx38mNScHMqKA9bgv0cyJY0BiutjLyZVnQyHlR_wk8,3189
|
|
14
|
+
corespine/seam/__init__.py,sha256=LAtM1d8laijBbXkBs9XXGAQiIh8bm0E2KhyRejGiey0,92
|
|
15
|
+
corespine/seam/registry.py,sha256=m8asc3yaAuhqqh87dGr021GtyRl7i1GmC0_B12OCRyY,5003
|
|
16
|
+
corespine-0.0.1.dist-info/METADATA,sha256=J2gVJZRz1VGBjXlwpG4zbpDwWskjwxGcwRHkcE_S9Zo,3630
|
|
17
|
+
corespine-0.0.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
18
|
+
corespine-0.0.1.dist-info/RECORD,,
|