From 112111408ea94122e14ab497c7d2d16c8ea24858 Mon Sep 17 00:00:00 2001 From: iomgaa Date: Mon, 6 Jul 2026 22:36:23 -0400 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E6=B7=BB=E5=8A=A0=E5=85=B1?= =?UTF-8?q?=E4=BA=AB=20Protocol=20=E7=AB=AF=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LLMProvider / VLMProvider / TelemetryRecorder,全部 runtime_checkable。 Co-Authored-By: Claude Opus 4.6 (1M context) --- core/protocols.py | 64 +++++++++++++++++++++++++++++ tests/unit/test_core_protocols.py | 67 +++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 core/protocols.py create mode 100644 tests/unit/test_core_protocols.py diff --git a/core/protocols.py b/core/protocols.py new file mode 100644 index 0000000..1b0a400 --- /dev/null +++ b/core/protocols.py @@ -0,0 +1,64 @@ +"""共享 Protocol 端口定义。 + +LLMProvider / VLMProvider / TelemetryRecorder 是跨子包共享接口, +被 core/agent/、core/evolution/、app/ 各模块引用。 +adapters/ 提供具体实现。 +""" +from __future__ import annotations + +from pathlib import Path +from typing import Any, Protocol, runtime_checkable + +from core.types import LLMResponse + + +@runtime_checkable +class LLMProvider(Protocol): + """LLM 文本调用端口。""" + + async def chat( + self, + messages: list[dict[str, Any]], + *, + session_id: str | None = None, + parent_call_id: str | None = None, + ) -> LLMResponse: ... + + +@runtime_checkable +class VLMProvider(Protocol): + """VLM 图文调用端口。""" + + async def chat_with_images( + self, + messages: list[dict[str, Any]], + images: list[str | Path], + *, + session_id: str | None = None, + parent_call_id: str | None = None, + ) -> LLMResponse: ... + + +@runtime_checkable +class TelemetryRecorder(Protocol): + """LLM 调用遥测记录端口。""" + + async def record_llm_call( + self, + *, + call_id: str, + parent_call_id: str | None, + session_id: str | None, + model_name: str, + provider: str, + messages: str, + response: str, + thinking: str, + prompt_tokens: int, + completion_tokens: int, + latency_ms: int, + ttft_ms: float | None, + max_inter_token_ms: float | None, + cache_hit: bool, + error: str | None, + ) -> None: ... diff --git a/tests/unit/test_core_protocols.py b/tests/unit/test_core_protocols.py new file mode 100644 index 0000000..4db157b --- /dev/null +++ b/tests/unit/test_core_protocols.py @@ -0,0 +1,67 @@ +"""core/protocols.py 单元测试 — 验证 Protocol 可 runtime_checkable。""" +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import pytest + +from core.protocols import LLMProvider, TelemetryRecorder, VLMProvider +from core.types import LLMResponse + + +class _FakeLLM: + async def chat( + self, + messages: list[dict[str, Any]], + *, + session_id: str | None = None, + parent_call_id: str | None = None, + ) -> LLMResponse: + return LLMResponse( + content="ok", thinking="", model="m", provider="p", + prompt_tokens=1, completion_tokens=1, latency_ms=1, + ttft_ms=None, max_inter_token_ms=None, cache_hit=False, call_id="c", + ) + + +class _FakeVLM: + async def chat_with_images( + self, + messages: list[dict[str, Any]], + images: list[str | Path], + *, + session_id: str | None = None, + parent_call_id: str | None = None, + ) -> LLMResponse: + return LLMResponse( + content="ok", thinking="", model="m", provider="p", + prompt_tokens=1, completion_tokens=1, latency_ms=1, + ttft_ms=None, max_inter_token_ms=None, cache_hit=False, call_id="c", + ) + + +class _FakeTelemetry: + async def record_llm_call( + self, *, call_id: str, parent_call_id: str | None, session_id: str | None, + model_name: str, provider: str, messages: str, response: str, thinking: str, + prompt_tokens: int, completion_tokens: int, latency_ms: int, + ttft_ms: float | None, max_inter_token_ms: float | None, + cache_hit: bool, error: str | None, + ) -> None: + pass + + +def test_fake_llm_satisfies_protocol() -> None: + assert isinstance(_FakeLLM(), LLMProvider) + +def test_fake_vlm_satisfies_protocol() -> None: + assert isinstance(_FakeVLM(), VLMProvider) + +def test_fake_telemetry_satisfies_protocol() -> None: + assert isinstance(_FakeTelemetry(), TelemetryRecorder) + +def test_plain_object_does_not_satisfy() -> None: + assert not isinstance(object(), LLMProvider) + assert not isinstance(object(), VLMProvider) + assert not isinstance(object(), TelemetryRecorder)