OpenMed 插件 SDK 实战:从一个可复制的示例包理解 entry-point 契约、离线合规校验与隐私边界
2026/9/18 19:55:18 网站建设 项目流程

OpenMed 插件 SDK 实战:从一个可复制的示例包理解 entry-point 契约、离线合规校验与隐私边界

【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed

本文基于 OpenMed 仓库中的示例插件包 examples/openmed-plugin-example/README.md 展开,完整讲解一个最小完整 OpenMed 插件分发的构成:确定性玩具识别器、隐私安全导出器、Python entry-point 元数据与离线合规测试。读完后,你将掌握如何按照openmed.plugins入口点契约开发自己的插件包、如何使用离线 conformance kit 进行自我认证,以及插件在本地优先(local-first)策略下必须遵守的隐私边界。需要说明的是:这是一个供扩展作者参考的示例,不是临床模型也不是医疗设备。

示例包定位:最小完整插件分发

examples/openmed-plugin-example是一个可以直接拷贝改名的包,展示了 OpenMed 插件 SDK 的最小完整形态。整个包只依赖本地运行时行为:不发起任何网络调用、不启用遥测、不写任何文件。目录结构非常精简:

examples/openmed-plugin-example/ ├── src/openmed_example_plugin/__init__.py # 两个组件 + 无参工厂 ├── tests/ │ ├── test_conformance.py # 离线自我认证测试 │ └── fixtures/malformed_plugin.py # 故意写坏的反例夹具 ├── pyproject.toml # entry-point 元数据 ├── LICENSE └── README.md

示例中唯一的"识别对象"是虚构标记OPENMED_SYNTHETIC_PERSON——识别器只对这个合成标记做出响应,README 明确要求不要将真实患者数据用于其测试。这个设计贯穿了 conformance kit 的探针文本,后文会看到两者共用同一个合成面。

离线自我认证:两条命令 + 一个离线安装方式

前提是从仓库根目录运行,且已安装 OpenMed 及其开发依赖。README 给出的两条命令分别验证"命令行工具形态"和"测试断言形态":

PYTHONPATH="examples/openmed-plugin-example/src${PYTHONPATH:+:${PYTHONPATH}}" \ python -m openmed.plugins.conformance \ openmed_example_plugin:plugin_components PYTHONPATH="examples/openmed-plugin-example/src${PYTHONPATH:+:${PYTHONPATH}}" \ python -m pytest \ examples/openmed-plugin-example/tests/test_conformance.py -q

两条命令的共同特点是只使用已提交的源码和合成值:不会枚举已安装的插件、不会访问包索引(package index),因此可以在完全离线的环境中执行。

对于已备好的离线环境,README 还给出了一个可编辑安装的命令,其中三个开关各有明确含义:

python -m pip install --no-index --no-deps --no-build-isolation -e \ examples/openmed-plugin-example
  • --no-index:明确禁用 PyPI 等包索引,避免联网解析;
  • --no-deps:不解析依赖(示例包声明的openmed>=2.0.0依赖在此环境中已预先满足);
  • --no-build-isolation:不复现独立的构建隔离环境,直接使用当前环境中的构建后端。

entry-point 契约:pyproject.toml 中的注册声明

插件包通过[project.entry-points."openmed.plugins"]这一稳定的入口点组注册组件。示例的 pyproject.toml 声明了:

[project] name = "openmed-example-plugin" version = "0.1.0" requires-python = ">=3.10" license = "Apache-2.0" dependencies = ["openmed>=2.0.0"] [project.entry-points."openmed.plugins"] example = "openmed_example_plugin:plugin_components" [tool.hatch.build.targets.wheel] packages = ["src/openmed_example_plugin"]

openmed_example_plugin:plugin_components指向一个无参组件工厂

def plugin_components() -> tuple[ToyRecognizer, ToyExporter]: """Return the components loaded from the package entry point.""" return ToyRecognizer(), ToyExporter()

从源码看,入口点引用的对象并不局限于无参工厂。registry.py 中的_coerce_components会递归处理三种形态:单个组件对象、可调用工厂、组件可迭代对象。conformance kit 的_coerce_components采用了同样的归一化逻辑,并对工厂抛异常、迭代器抛异常等情况分别给出component_contract失败原因而不是直接崩溃。

组件实现:元数据与两个契约方法

ToyRecognizer:只识别虚构标记的本地识别器

示例源码 中的ToyRecognizer不加载任何权重、不使用网络,仅用str.find循环定位标记并构造 span:

SYNTHETIC_PERSON_MARKER = "OPENMED_SYNTHETIC_PERSON" class ToyRecognizer: metadata = { "plugin_id": "openmed-example-plugin", "component_id": "toy-recognizer", "kind": "recognizer", "sdk_version": "1.0.0", "license": "Apache-2.0", "network_egress": False, "labels": ("PERSON",), "languages": ("en",), "name": "Toy synthetic marker recognizer", "description": "Recognizes one fictional conformance marker locally.", "metadata": { "fixture_policy": "synthetic_only", "local_first": True, }, } def recognize(self, text: str, **kwargs) -> Sequence[OpenMedSpan]: ...

它返回的每个OpenMedSpan都遵循几个关键点:

  • 偏移量指向传入的源文本start/end是相对于text的字符偏移,conformance kit 会校验0 <= start < end <= len(text));
  • text_hash使用openmed.core.schemas.span中的hmac_text_hash(marker, _HASH_KEY)生成 HMAC 哈希,而非原文,保证 span 记录本身不含源文本面(surface);
  • canonical_label="PERSON"必须落在元数据声明的labels内;
  • detector字段采用plugin:openmed-example-plugin:toy-recognizer的限定 ID 形式,与 SDK 中qualified_id = plugin_id:component_id的命名规则一致;
  • evidencemetadata中只放安全说明(如"source": "synthetic_literal_marker"),绝不复制源文本。

ToyExporter:只序列化隐私安全字段

ToyExporter的契约方法export(spans, **kwargs)返回一个 JSON 兼容的映射:

def export(self, spans, **kwargs) -> Mapping[str, Any]: return { "schema": "openmed.example-plugin.spans.v1", "spans": [span.to_dict() for span in spans], }

它的元数据同样声明network_egress = FalseApache-2.0许可,但languages("*",)——导出器不产出语言相关的 span,声明通配语言是合理做法。README 对此的概括是:导出器序列化偏移量、哈希、标签和安全溯源信息,永不序列化源文本面

静态元数据字段契约

每个组件都通过类属性metadata暴露静态元数据。示例中用到的字段与 protocols.py 中PluginComponentMetadata的完整字段定义对齐:

字段契约示例取值
plugin_id非空、稳定、不得含:openmed-example-plugin
component_id包内唯一、非空、不得含:toy-recognizer/toy-exporter
kind五类组件之一recognizer/exporter
sdk_version目标 SDK 的语义化版本,主版本须匹配1.0.0
licenseSPDX 许可表达式,须为宽松许可Apache-2.0
network_egress布尔值,声明是否可发起网络调用False
labels规范 OpenMed 标签序列;识别器与匿名化提供者必须至少声明一个("PERSON",)
languages语言标签序列或"*"("en",)/("*",)
name/description可选的人类可读信息
metadata可选的静态、非 PHI 映射{"fixture_policy": "synthetic_only"}

类型校验非常严格:_metadata_field_failure要求plugin_idcomponent_idkindsdk_versionlicense必须是字符串,network_egress必须是布尔,labels/languages必须是字符串序列(裸字符串会被拒绝,因为str本身也是Sequence)。SDK 主版本不匹配会以protocol_version_mismatch报告——openmed.plugins.protocolsPLUGIN_SDK_MAJOR = 1,所以1.x系列版本都能通过。

离线 conformance kit:合成探针如何驱动校验

python -m openmed.plugins.conformance的实现在 conformance.py,它的头部文档注释明确:只验证静态元数据、只用确定性合成值调用公开方法,不枚举入口点、不安装包、不打开 socket、不持久化源文本

探针文本与 span 校验

conformance kit 内部定义了一个与示例插件完全同源的探针面:

_PROBE_SURFACE = "OPENMED_SYNTHETIC_PERSON" _PROBE_TEXT = f"conformance {_PROBE_SURFACE} fixture"

对识别器,kit 会调用recognize(_PROBE_TEXT)并逐项检查(见_probe_recognizer):

  • 返回值必须是Sequence,且每个元素必须是OpenMedSpan
  • 偏移量必须落在探针文本范围内(span.start < 0span.end > len(_PROBE_TEXT)即失败);
  • canonical_label必须在该组件声明的labels之内;
  • _contains_surface递归扫描 span 的evidencemetadata子树,一旦在其中发现探针源文本面,即以recognize() copied source text into span metadata or evidence判为runtime_contract失败——这是"span 记录不得含原文"这一隐私约束的机器化执行。

对导出器,kit 调用export((_PROBE_SPAN,)),先验证返回类型(字符串、bytes、Mapping、或 Mapping 序列),再用同样的_contains_surface检查导出产物中不含探针面(_probe_exporter)。

失败原因码:稳定、机器可读、安全

所有失败都被归一化为PluginConformanceFailure(reason, message, component_id),其中message保证不含探针源文本。conformance kit 定义了一组稳定 reason(conformance.py L21-L30):

reason触发场景
invalid_metadata元数据字段缺失、类型错误、空 id、id 含:
unknown_component_kindkind不在五种支持类型中
protocol_version_mismatchsdk_version主版本不支持
non_permissive_license许可表达式不是宽松 SPDX 许可
network_egress_not_local_first声明了network_egress = True(自我认证路径下)
invalid_label标签不在规范 OpenMed 标签 schema 中
missing_labels识别器未声明任何标签
duplicate_component限定组件 id 在包内重复
component_contract工厂/迭代器抛异常或缺少必需方法
runtime_contract探针调用抛异常或返回值违反契约

宽松许可白名单(registry.py 中的PERMISSIVE_LICENSES,conformance kit 有等价的兜底常量)包括Apache-2.0MITBSD-2-ClauseBSD-3-ClauseISCCC0-1.0CC-BY-4.00BSDUnlicenseZlib_is_permissive_license会解析复合 SPDX 表达式,要求其中每一个许可 token 都在白名单内,AND/OR/WITH等连接词被过滤后参与判断。

反例夹具:故意写坏的插件如何被拒绝

tests/fixtures/malformed_plugin.py 是一个故意错误的夹具,它把网络策略声明成了字符串:

metadata = { ... "network_egress": "false", # 字符串,而非布尔 False ... }

这个差异恰好演示了开发者能收到的反馈精度。对应测试 test_conformance.py 断言了三件事:

def test_malformed_fixture_has_specific_failure() -> None: report = check_plugin_conformance(_malformed_components()) assert not report.passed assert report.failures[0].reason == REASON_INVALID_METADATA assert report.failures[0].message == "network_egress must be a boolean" with pytest.raises( PluginConformanceError, match="invalid_metadata: network_egress must be a boolean", ): assert_plugin_conforms(_malformed_components())

check_plugin_conformance返回报告但不抛异常,assert_plugin_conforms则在失败时抛出PluginConformanceError(其消息形如OpenMed plugin conformance failed:后跟逐条- reason: component_id: message)。同一文件中的正向测试还断言示例包report.passed为真且report.components_checked == 2。值得注意的是,夹具中recognize方法的 docstring 特别说明:元数据校验失败发生在探针探测之前,所以它的空实现根本不会被执行——元数据契约先于行为契约被验证

五种组件类型与方法契约

虽然示例只实现了 recognizer 和 exporter,但 SDK 定义了完整的五类组件(protocols.py),conformance kit 对每类都有对应的方法名要求(_COMPONENT_METHODS,conformance.py L56-L62)和探针:

kind必需公共方法探针行为
recognizerrecognize(text, **kwargs)用探针文本调用,校验 span 类型、偏移、标签、无原文泄漏
anonymizer_providerreplacement_for(span, surface, **kwargs)替换必须非空且不包含源文本面
exporterexport(spans, **kwargs)校验返回类型与产物不含源文本面
interop_adapterto_openmed_spans/from_openmed_spans双向转换通过规范 span 进行
language_packlanguage_code()/canonical_labels()语言码非空,标签须在规范 schema 内

这些方法与 protocols.py 中typing.Protocol声明的契约一一对应,例如ExporterPlugin.export的返回类型注解为str | bytes | Mapping[str, Any] | Sequence[Mapping[str, Any]],与 conformance kit 中_probe_exporter接受的"有效输出"判断完全一致。

与运行时注册表的衔接(补充背景)

自我认证通过后,真正在 OpenMed 进程中加载插件的是 registry.py 的PluginRegistry。理解这段实现有助于理解示例包各项声明的意义:

  • 发现始终是调用方触发、惰性、进程级作用域的:discover_plugins()才枚举openmed.plugins组,导入openmed本身不会导入插件依赖;
  • 每个入口点独立隔离加载,损坏或策略受限的插件变成结构化的PluginQuarantineRecord,不会拖垮其他插件或 OpenMed 本体;
  • 默认策略(allow_network_egress=Falseallow_non_permissive_licenses=False)只自动加载声明无网络出网、且许可表达式完全宽松的组件;network_egress=True或受限许可的组件会因network_egress_opt_in_required/non_permissive_license_opt_in_required被隔离,除非调用方通过opt_in_plugins(支持plugin_id或限定plugin_id:component_id)显式点名放行;
  • 重复的限定组件 id 会以duplicate_component隔离——这正是 README 改编指南第 2 条"保持每个component_id在分发内唯一"的底层原因。

换句话说,示例包在元数据里写死network_egress = False与宽松许可,是为了走"默认策略即可自动加载"这条最快路径,而无需调用方做任何 opt-in。

改编此包的五步流程

README 给出的改编步骤(Adapt this package)与上述实现细节相互印证,可以逐条对照执行:

  1. 拷贝目录并重命名:项目名(name)、导入包名(openmed_example_plugin)、plugin_id、entry-point 名都要一起改,保持四者一致;
  2. 保证component_id唯一:分发内每个组件的component_id不得重复,否则 conformance 与 registry 都会以duplicate_component拒绝;
  3. 替换玩具方法、保留契约:把recognize/export等方法的实现换成真实逻辑,但保持元数据字段与方法签名不变——kind决定了 kit 会检查哪些方法,返回类型必须仍是OpenMedSpan序列 / 结构化映射等契约类型;
  4. 保持夹具合成化:测试数据必须是合成值;原始源文本面(raw source surfaces)永远不得进入日志、span metadata、导出产物、缓存或遥测——这是 conformance kit 用_contains_surfaceevidence/metadata子树与导出产物中递归强制的不变量;
  5. 发布前运行 conformance 命令:以仓库中给出的两条命令为准,确保离线、合成输入下全部通过。

适用前提与限制

  • 本流程要求 OpenMed 已安装(示例包声明dependencies = ["openmed>=2.0.0"]requires-python = ">=3.10"),conformance 命令通过PYTHONPATH指向src目录即可运行,无需先安装示例包本身;
  • 自我认证路径是纯离线的:不枚举已装插件、不访问包索引;只有显式执行pip install -e时涉及安装,且离线环境必须加--no-index --no-deps --no-build-isolation
  • 注册表验证的是兼容性声明与策略声明,如 plugin-sdk.md 所述,它不是针对不可信代码的沙箱——插件代码在安装并 opt-in 后运行于 OpenMed 进程内,这一信任边界应作为分发前的评估前提;
  • 更完整的 SDK 稳定性策略(entry-point 契约、语义化版本规则、弃用窗口等)见 docs/plugin-sdk.md,其中明确:任何对入口点组重命名、稳定元数据字段/组件类型删改、必需方法契约变更、OpenMedSpan偏移或标签语义变更,都要求 SDK 主版本号递增。

小结

examples/openmed-plugin-example用极小的代价演示了 OpenMed 插件体系的全部关键机制:一个openmed.pluginsentry-point 注册声明、两个带静态元数据的组件、一套只用合成探针驱动的离线 conformance kit,以及一个字符串"false"与布尔False之差就能触发invalid_metadata: network_egress must be a boolean的精确反馈闭环。对于扩展作者而言,以这个包为模板,严格遵守"合成夹具、无源文本面泄漏、本地优先"三条底线,即可完成从开发到自我认证的完整闭环。

【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询