Files
tracer-cloud--opensre/core/messages/provider_adapters.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

190 lines
7.3 KiB
Python

"""Per-provider message-shaping adapters for :class:`MessageMapper`.
Each provider (Anthropic, Bedrock Converse, OpenAI-family, CLI-backed) shapes
assistant messages, tool results, and synthetic tool-call turns differently.
Rather than scatter ``isinstance`` ladders across the mapper, one adapter per
provider owns its shapes, and :func:`adapter_for` maps a client to its adapter in
a single place.
Typing: the provider clients expose two distinct ``build_assistant_message``
shapes — Anthropic/Bedrock echo the provider's raw content (``raw_content``),
while OpenAI/CLI construct from ``content`` + ``tool_calls``. Each adapter is
generic over the narrow client Protocol it needs (:class:`_RawAssistantClient`,
:class:`_ConstructAssistantClient`, :class:`_OpenAIShapedClient`), so calls are
checked against the real client rather than ``Any``.
"""
from __future__ import annotations
from typing import Any, Protocol
from core.llm.types import AgentLLMResponse, ToolCall
from core.messages.runtime_message_types import ProviderMessage, RuntimeContent
class _ToolResultClient(Protocol):
"""The tool-result surface every provider client shares."""
def build_tool_result_message(
self, tool_calls: list[ToolCall], results: list[Any]
) -> dict[str, Any]:
"""Build one provider tool-result message for a batch of calls."""
class _RawAssistantClient(_ToolResultClient, Protocol):
"""Clients that rebuild an assistant message from the provider's raw content."""
def build_assistant_message(self, raw_content: Any) -> dict[str, Any]:
"""Rebuild the assistant message from the provider's raw response content."""
class _ConstructAssistantClient(_ToolResultClient, Protocol):
"""Clients that construct an assistant message from text + tool calls."""
def build_assistant_message(self, content: str, tool_calls: list[ToolCall]) -> dict[str, Any]:
"""Construct the assistant message from text content and tool calls."""
class _OpenAIShapedClient(_ConstructAssistantClient, Protocol):
"""OpenAI-family clients: one assistant message but many tool-result messages."""
def build_tool_result_messages(
self, tool_calls: list[ToolCall], results: list[Any]
) -> list[dict[str, Any]]:
"""Build one provider tool-result message per tool call."""
class MessageAdapter[LLMT: _ToolResultClient]:
"""Base shapes shared by every provider; ``to_assistant_provider_message`` is provider-specific."""
def __init__(self, llm: LLMT) -> None:
self._llm = llm
def to_assistant_provider_message(self, response: AgentLLMResponse) -> ProviderMessage:
raise NotImplementedError
def to_tool_result_provider_messages(
self, tool_calls: list[ToolCall], results: list[Any]
) -> list[ProviderMessage]:
return [self._llm.build_tool_result_message(tool_calls, results)]
def to_synthetic_assistant_provider_message(
self, tool_calls: list[ToolCall]
) -> ProviderMessage:
names = ", ".join(tc.name for tc in tool_calls)
return {"role": "assistant", "content": f"I will start by querying: {names}"}
def app_message_content(self, content: RuntimeContent) -> RuntimeContent:
return content
class _GenericAdapter[LLMT: _ConstructAssistantClient](MessageAdapter[LLMT]):
"""OpenAI-family / unknown clients that construct assistant messages from text."""
def to_assistant_provider_message(self, response: AgentLLMResponse) -> ProviderMessage:
# raw_content carries provider-specific extras (e.g. Gemini's
# thought_signature) that must be echoed back verbatim next request.
if response.raw_content is not None:
return response.raw_content # type: ignore[no-any-return]
return self._llm.build_assistant_message(response.content, response.tool_calls)
class _AnthropicAdapter[LLMT: _RawAssistantClient](MessageAdapter[LLMT]):
def to_assistant_provider_message(self, response: AgentLLMResponse) -> ProviderMessage:
return self._llm.build_assistant_message(response.raw_content)
def to_synthetic_assistant_provider_message(
self, tool_calls: list[ToolCall]
) -> ProviderMessage:
return {
"role": "assistant",
"content": [
{"type": "tool_use", "id": tc.id, "name": tc.name, "input": tc.input}
for tc in tool_calls
],
}
class _BedrockConverseAdapter[LLMT: _RawAssistantClient](MessageAdapter[LLMT]):
def to_assistant_provider_message(self, response: AgentLLMResponse) -> ProviderMessage:
return self._llm.build_assistant_message(response.raw_content)
def to_synthetic_assistant_provider_message(
self, tool_calls: list[ToolCall]
) -> ProviderMessage:
from core.llm.transports.sdk.bedrock_converse import build_assistant_tool_use_message
result: dict[str, Any] = build_assistant_tool_use_message(tool_calls)
return result
def app_message_content(self, content: RuntimeContent) -> RuntimeContent:
return _to_converse_text_blocks(content)
class _OpenAIAdapter[LLMT: _OpenAIShapedClient](_GenericAdapter[LLMT]):
def to_tool_result_provider_messages(
self, tool_calls: list[ToolCall], results: list[Any]
) -> list[ProviderMessage]:
return list(self._llm.build_tool_result_messages(tool_calls, results))
def to_synthetic_assistant_provider_message(
self, tool_calls: list[ToolCall]
) -> ProviderMessage:
# Reuse the canonical OpenAI tool_calls shape, then restore the
# synthetic-turn convention of a null (not empty-string) content.
from core.llm.shared.openai_chat_completions import build_assistant_message
message = build_assistant_message("", tool_calls)
message["content"] = None
return message
class _CLIAdapter[LLMT: _ConstructAssistantClient](_GenericAdapter[LLMT]):
def to_synthetic_assistant_provider_message(
self, tool_calls: list[ToolCall]
) -> ProviderMessage:
return self._llm.build_assistant_message("", tool_calls)
def adapter_for(llm: Any) -> MessageAdapter[Any]:
"""Resolve the message adapter for a provider client (the one dispatch point)."""
from core.llm.transports.sdk.agent_clients import (
AnthropicAgentClient,
BedrockConverseAgentClient,
CLIBackedAgentClient,
OpenAIAgentClient,
)
if isinstance(llm, BedrockConverseAgentClient):
return _BedrockConverseAdapter(llm)
if isinstance(llm, AnthropicAgentClient):
return _AnthropicAdapter(llm)
if isinstance(llm, OpenAIAgentClient) or _is_litellm_agent_client(llm):
return _OpenAIAdapter(llm)
if isinstance(llm, CLIBackedAgentClient):
return _CLIAdapter(llm)
return _GenericAdapter(llm)
def _is_litellm_agent_client(llm: Any) -> bool:
cls = type(llm)
return (
cls.__module__ == "core.llm.transports.litellm.clients"
and cls.__name__ == "LiteLLMAgentClient"
)
def _to_converse_text_blocks(content: RuntimeContent) -> RuntimeContent:
if not isinstance(content, list):
return content
converted: list[dict[str, Any]] = []
for block in content:
if block.get("type") == "text" and "text" in block:
converted.append({"text": str(block["text"])})
else:
converted.append(dict(block))
return converted
__all__ = ["MessageAdapter", "adapter_for"]