核心函数
sag_align
SAG ↔ OCTX 数据模型对齐:ID 映射、基数校验、字段长度检查、实体去重与结构化导出。
sag_align 提供 SAG 应用数据模型与 OCTX sag-structured/0.1 之间的对齐工具,
用于导入/导出场景下的 ID 映射、基数校验、字段长度检查、实体去重和结构化数据导出。
函数
| 函数 | 作用 |
|---|---|
translate_id() | UUIDv4 → UUIDv7(双向可逆映射) |
revert_id() | UUIDv7 → UUIDv4(过渡期向后兼容) |
ensure_octx_ids() | 批量转换记录中的 UUIDv4 字段 |
default_sag_field_profile() | 返回默认 SAG 字段画像 |
validate_cardinality_for_sag() | 检查 Event 是否关联多个 Chunk |
validate_field_lengths_for_sag() | 校验所有文本字段的 SAG 列长度限制 |
find_entity_dedup_matches() | 查找 Entity name 重复项 |
classify_ids() | 统计各 ID 类型的出现次数 |
validate_sag_structured() | 检查完整覆盖(Section 6 无孤立记录) |
validate_sag_constraints() | 检查结构约束(Section 3-5) |
write_structured_to_workspace() | 写入并验证完整结构化数据到 OCTX workspace |
数据类型
| 类型 | 说明 |
|---|---|
SagFieldProfile | 各字段的 SAG 数据库列长度上限 |
SagCardinalityIssue | Event 关联了 2+ 个 Chunk |
SagFieldIssue | 某记录字段超出列长度 |
SagDedupMatch | 两个 Entity 因相同 name 被标记为重复 |
SagCoverageIssue | 完整覆盖规则违规 |
SagConstraintIssue | 结构约束违规 |
SagAlignmentReport | 聚合的对齐校验报告 |
translate_id()
def translate_id(uuid_v4: str) -> str将 UUIDv4 映射为 UUIDv7(修改版本半字节 4→7)。结果是可逆的稳定身份,
可在 OCTX Package 内使用,之后用 revert_id() 恢复。
| 参数 | 作用 |
|---|---|
uuid_v4 | 有效的 UUIDv4 字符串(36 字符) |
返回值:修改版本半字节后的 UUIDv7 字符串,小写。
异常:不是合法 UUIDv4 时抛出 ValueError。
from octx.sag_align import translate_id
v7 = translate_id("550e8400-e29b-41d4-a716-446655440000")
# → "550e8400-e29b-71d4-a716-446655440000"revert_id()
def revert_id(uuid_v7: str) -> str反向 translate_id(),将 OCTX 格式 ID 恢复为 UUIDv4。
这是一个过渡期向后兼容工具,在 SAG 完全迁移到 UUIDv7 之前使用。
| 参数 | 作用 |
|---|---|
uuid_v7 | 版本半字节为 7 的 UUID 字符串 |
异常:不是有效的已转换 OCTX ID 时抛出 ValueError。
from octx.sag_align import revert_id
v4 = revert_id("550e8400-e29b-71d4-a716-446655440000")
# → "550e8400-e29b-41d4-a716-446655440000"ensure_octx_ids()
def ensure_octx_ids(
records: Iterable[dict[str, Any]],
*,
id_fields: Collection[str] = {"id", "document_id", "chunk_id", "event_id", "entity_id", "parent_id"},
) -> Iterator[dict[str, Any]]批量遍历记录,将 id_fields 集合中的 UUIDv4 字段自动转为 UUIDv7。
非字典对象、已是 UUIDv7、或非 UUID 的值原样通过。
| 参数 | 作用 |
|---|---|
records | 字典记录的可迭代对象 |
id_fields | 关键字参数;需要转换的字段名集合 |
from octx.sag_align import ensure_octx_ids
records = [{"id": "550e8400-e29b-41d4-a716-446655440000", "title": "doc"}]
result = list(ensure_octx_ids(records))
# result[0]["id"] → "550e8400-e29b-71d4-a716-446655440000"default_sag_field_profile()
def default_sag_field_profile() -> SagFieldProfile返回默认的 SagFieldProfile 实例。
from octx.sag_align import default_sag_field_profile
profile = default_sag_field_profile()
# SagFieldProfile(id=36, name=512, entity_type=100, ...)validate_cardinality_for_sag()
def validate_cardinality_for_sag(
chunk_events: Iterable[dict[str, Any]],
*,
events: Collection[str] | None = None,
) -> list[SagCardinalityIssue]检查哪些 Event 关联了 2+ 个 Chunk。SAG 数据模型每个 Event 只存一个来源 Chunk, 从 OCTX(允许 N:M)导入时需要处理这类情况。
| 参数 | 作用 |
|---|---|
chunk_events | relations/chunk-events.jsonl 的记录 |
events | 关键字参数;可选限制检查范围的 Event ID 集合 |
返回值:每个违规 Event 一条 SagCardinalityIssue,按 Event ID 排序。
from octx.sag_align import validate_cardinality_for_sag
issues = validate_cardinality_for_sag(chunk_events)
for issue in issues:
print(issue.event_id, issue.chunk_ids)validate_field_lengths_for_sag()
def validate_field_lengths_for_sag(
chunks: Iterable[dict[str, Any]],
events: Iterable[dict[str, Any]],
entities: Iterable[dict[str, Any]],
chunk_events: Iterable[dict[str, Any]],
event_entities: Iterable[dict[str, Any]],
*,
profile: SagFieldProfile | None = None,
) -> list[SagFieldIssue]检查五个 JSONL 文件中所有文本字段是否超过 SAG 数据库列长度上限。
长度 0 表示无限制(如 chunk.text)。
| 参数 | 作用 |
|---|---|
chunks | data/chunks.jsonl 记录 |
events | data/events.jsonl 记录 |
entities | data/entities.jsonl 记录 |
chunk_events | relations/chunk-events.jsonl 记录 |
event_entities | relations/event-entities.jsonl 记录 |
profile | 关键字参数;自定义字段画像,默认使用 default_sag_field_profile() |
from octx.sag_align import validate_field_lengths_for_sag
issues = validate_field_lengths_for_sag(chunks, events, entities, ce, ee)find_entity_dedup_matches()
def find_entity_dedup_matches(
entities: Iterable[dict[str, Any]],
*,
normalize: bool = True,
) -> list[SagDedupMatch]查找 entities 中 name 值完全相同的记录(NFC 归一化 + 可选 case folding)。
每个名称只报告第一对匹配。
| 参数 | 作用 |
|---|---|
entities | data/entities.jsonl 记录 |
normalize | 关键字参数;为 True 时 NFC 归一化并忽略大小写比较 |
from octx.sag_align import find_entity_dedup_matches
matches = find_entity_dedup_matches(entities)classify_ids()
def classify_ids(
records: Iterable[dict[str, Any]],
*,
id_fields: Collection[str] = {"id", "document_id", "chunk_id", "event_id", "entity_id", "parent_id"},
) -> dict[str, int]统计记录中各 ID 类型的出现次数。返回 {"octx": N, "v4": N, "other": N}。
| 参数 | 作用 |
|---|---|
records | 字典记录的可迭代对象 |
id_fields | 关键字参数;要检查的字段名集合 |
from octx.sag_align import classify_ids
stats = classify_ids(all_records)
# {"octx": 5, "v4": 2, "other": 0}validate_sag_structured()
def validate_sag_structured(
chunks: Iterable[dict[str, Any]],
events: Iterable[dict[str, Any]],
entities: Iterable[dict[str, Any]],
chunk_events: Iterable[dict[str, Any]],
event_entities: Iterable[dict[str, Any]],
*,
document_ids: Collection[str] | None = None,
) -> list[SagCoverageIssue]检查 sag-structured/0.1 Section 6 完整覆盖规则 —— 确保整条链没有孤立记录:
- document_has_chunk — 每篇 Concept Document 至少有一个 Chunk
- chunk_in_relation — 每个 Chunk 至少出现在一条 Chunk-Event relation 中
- event_in_chunk_relation — 每个 Event 至少出现在一条 Chunk-Event relation 中
- event_in_entity_relation — 每个 Event 至少出现在一条 Event-Entity relation 中
- entity_in_relation — 每个 Entity 至少出现在一条 Event-Entity relation 中
| 参数 | 作用 |
|---|---|
chunks, events, entities, chunk_events, event_entities | 五个 JSONL 文件的记录 |
document_ids | 关键字参数;可选,提供时检查 Rule 1 |
from octx.sag_align import validate_sag_structured
issues = validate_sag_structured(chunks, events, entities, ce, ee, document_ids=["doc-1"])
for issue in issues:
print(issue.rule, issue.record_id)validate_sag_constraints()
def validate_sag_constraints(
chunks: Iterable[dict[str, Any]],
events: Iterable[dict[str, Any]],
entities: Iterable[dict[str, Any]],
chunk_events: Iterable[dict[str, Any]],
event_entities: Iterable[dict[str, Any]],
) -> list[SagConstraintIssue]检查 sag-structured/0.1 Sections 3-5 的结构约束:
Chunks — 必填字段(id, document_id, ordinal, text)、text 非空、ordinal 同一 document_id 内唯一
Events — 必填字段(id, title, content)、title/content 非空、parent_id/level 一致性、子 Event level = parent.level + 1、父子关系无成环
Relations — (chunk_id, event_id) 和 (event_id, entity_id) 组合不得重复
引用完整性 — 所有 chunk_id / event_id / entity_id 引用必须存在
UUIDv7 格式 — 所有 ID 字段必须为标准小写 UUIDv7
from octx.sag_align import validate_sag_constraints
issues = validate_sag_constraints(chunks, events, entities, ce, ee)
for issue in issues:
print(issue.code, issue.record_id)write_structured_to_workspace()
def write_structured_to_workspace(
workspace: str | Path,
*,
chunks: Iterable[dict[str, Any]] = (),
events: Iterable[dict[str, Any]] = (),
entities: Iterable[dict[str, Any]] = (),
chunk_events: Iterable[dict[str, Any]] = (),
event_entities: Iterable[dict[str, Any]] = (),
profile: SagFieldProfile | None = None,
) -> SagAlignmentReport将外部结构化记录写入 OCTX workspace:
- 所有 UUIDv4 ID 自动映射为 UUIDv7
- 执行基数字段长度校验和去重检查
- 将非空 stream 写入对应 JSONL 文件
- 返回聚合的校验报告
写入完成后 workspace 可直接用于 create_octx()。
| 参数 | 作用 |
|---|---|
workspace | 已有 OCTX workspace 路径 |
chunks / events / entities / chunk_events / event_entities | 关键字参数;各 stream 记录 |
profile | 关键字参数;自定义字段画像 |
from octx.sag_align import write_structured_to_workspace
report = write_structured_to_workspace(
"my-workspace",
chunks=[...],
events=[...],
entities=[...],
chunk_events=[...],
event_entities=[...],
)SagFieldProfile
@dataclass(frozen=True)
class SagFieldProfile:
id: int = 36 # UUID 字符串长度
name: int = 512 # entity.name
entity_type: int = 100 # entity.type
title: int = 512 # event.title, document.title
summary: int = 4096 # event.summary
category: int = 128 # event.category
description: int = 4096 # entity.description, relation description
text: int = 0 # chunk.text / event.content(0 表示无限制)SagCardinalityIssue
@dataclass(frozen=True)
class SagCardinalityIssue:
event_id: str
chunk_ids: tuple[str, ...] # 该 Event 关联的所有 Chunk ID
primary_hint: str | None = None # 建议主 ChunkSagFieldIssue
@dataclass(frozen=True)
class SagFieldIssue:
path: str # JSONL 文件路径(如 "data/entities.jsonl")
line: int # 1-indexed 行号
record_id: str | None
field: str # 超限字段名
value_length: int
limit: intSagDedupMatch
@dataclass(frozen=True)
class SagDedupMatch:
entity_id_a: str
entity_id_b: str
name: str # 匹配的 name 值
same_package: bool = True # 同一包内重复SagCoverageIssue
@dataclass(frozen=True)
class SagCoverageIssue:
rule: str # 规则标识符(详见 validate_sag_structured)
record_id: str | None # 违规记录的 ID
description: str # 人类可读的说明SagConstraintIssue
@dataclass(frozen=True)
class SagConstraintIssue:
code: str # 结构约束代码(如 chunk_text_empty)
record_id: str | None # 违规记录的 ID
description: str # 人类可读的说明SagAlignmentReport
@dataclass
class SagAlignmentReport:
cardinality_issues: list[SagCardinalityIssue]
field_issues: list[SagFieldIssue]
dedup_within: list[SagDedupMatch]
coverage_issues: list[SagCoverageIssue]
constraint_issues: list[SagConstraintIssue]
id_translations: int # UUIDv4→v7 转换次数
unchanged_ids: int # 保持不变的 ID 数
# 属性
has_cardinality_conflicts: bool # cardinality_issues 非空?
has_field_issues: bool # field_issues 非空?
has_dedup_within: bool # dedup_within 非空?
has_coverage_issues: bool # coverage_issues 非空?
has_constraint_issues: bool # constraint_issues 非空?
# 方法
def merge(self, other: SagAlignmentReport) -> SagAlignmentReport
# 合并两个报告(例如来自不同 Package)