mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 07:17:54 +08:00
core/agent: remove ChatClient protocol and ResponsesChatClient wrapper
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
from .chat import ChatAgent as ChatAgent
|
||||
from .client import ChatClient as ChatClient
|
||||
from .compact import build_compacted_seed_messages as build_compacted_seed_messages
|
||||
from .compact import summarize_messages as summarize_messages
|
||||
from .events import AgentEvent as AgentEvent
|
||||
@@ -14,8 +13,6 @@ from .events import ToolResultEvent as ToolResultEvent
|
||||
from .events import UsageEvent as UsageEvent
|
||||
from .loop import DEFAULT_MAX_ROUNDS as DEFAULT_MAX_ROUNDS
|
||||
from .loop import run_chat_loop as run_chat_loop
|
||||
from .responses_client import ResponsesChatClient as ResponsesChatClient
|
||||
from .responses_client import wrap_openai_for_agent as wrap_openai_for_agent
|
||||
from .tools import DangerClassifier as DangerClassifier
|
||||
from .tools import ToolConfirmHook as ToolConfirmHook
|
||||
from .tools import ToolDefinition as ToolDefinition
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, Protocol, overload
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from plyngent.lmproto.openai_compatible.model import (
|
||||
ChatCompletionChunk,
|
||||
ChatCompletionResponse,
|
||||
ChatCompletionsParam,
|
||||
)
|
||||
|
||||
|
||||
class ChatClient(Protocol):
|
||||
"""Structural protocol for OpenAI-compatible chat completion clients.
|
||||
|
||||
When ``stream=True``, implementations may return an async iterator from an
|
||||
async method (``await client.chat_completions(..., stream=True)`` then
|
||||
``async for chunk in stream``). That library shape is intentional.
|
||||
"""
|
||||
|
||||
@overload
|
||||
async def chat_completions(
|
||||
self, param: ChatCompletionsParam, *, stream: Literal[False] = False
|
||||
) -> ChatCompletionResponse: ...
|
||||
|
||||
@overload
|
||||
async def chat_completions(
|
||||
self, param: ChatCompletionsParam, *, stream: Literal[True]
|
||||
) -> AsyncIterator[ChatCompletionChunk]: ...
|
||||
|
||||
async def chat_completions(
|
||||
self, param: ChatCompletionsParam, *, stream: bool = False
|
||||
) -> ChatCompletionResponse | AsyncIterator[ChatCompletionChunk]: ...
|
||||
@@ -1,137 +0,0 @@
|
||||
"""ChatClient adapter: agent chat loop over OpenAI Responses API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Literal, overload
|
||||
|
||||
from msgspec import UNSET
|
||||
|
||||
from plyngent.agent.responses_bridge import (
|
||||
chat_param_to_responses_kwargs,
|
||||
finish_reason_chunk,
|
||||
reasoning_delta_chunk,
|
||||
response_to_assistant_message,
|
||||
response_to_chat_completion,
|
||||
responses_status_to_finish_reason,
|
||||
text_delta_chunk,
|
||||
tool_call_chunks_from_response,
|
||||
usage_chunk_from_response,
|
||||
)
|
||||
from plyngent.lmproto.openai.model import ResponsesCreateParam
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator, Sequence
|
||||
|
||||
from plyngent.lmproto.openai.client import OpenAIClient
|
||||
from plyngent.lmproto.openai.model import Response
|
||||
from plyngent.lmproto.openai_compatible.model import (
|
||||
ChatCompletionChunk,
|
||||
ChatCompletionResponse,
|
||||
ChatCompletionsParam,
|
||||
)
|
||||
|
||||
|
||||
class ResponsesChatClient:
|
||||
"""Present OpenAI Responses as :class:`~plyngent.agent.client.ChatClient`.
|
||||
|
||||
History and tool results remain chat-completions-shaped; only the HTTP call
|
||||
uses ``POST /responses``. Optional *provider_tools* are hosted tools merged
|
||||
into the request (not local registry handlers).
|
||||
"""
|
||||
|
||||
_client: OpenAIClient
|
||||
_provider_tools: list[dict[str, Any]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: OpenAIClient,
|
||||
*,
|
||||
provider_tools: Sequence[dict[str, Any]] | None = None,
|
||||
) -> None:
|
||||
self._client = client
|
||||
self._provider_tools = [dict(t) for t in provider_tools] if provider_tools else []
|
||||
|
||||
async def models(self) -> list[str]:
|
||||
return await self._client.models()
|
||||
|
||||
@overload
|
||||
async def chat_completions(
|
||||
self, param: ChatCompletionsParam, *, stream: Literal[False] = False
|
||||
) -> ChatCompletionResponse: ...
|
||||
|
||||
@overload
|
||||
async def chat_completions(
|
||||
self, param: ChatCompletionsParam, *, stream: Literal[True]
|
||||
) -> AsyncIterator[ChatCompletionChunk]: ...
|
||||
|
||||
async def chat_completions(
|
||||
self, param: ChatCompletionsParam, *, stream: bool = False
|
||||
) -> ChatCompletionResponse | AsyncIterator[ChatCompletionChunk]:
|
||||
kwargs = chat_param_to_responses_kwargs(
|
||||
param,
|
||||
provider_tools=self._provider_tools or None,
|
||||
)
|
||||
create = ResponsesCreateParam(**kwargs)
|
||||
if stream:
|
||||
return self._stream_as_chat_chunks(create, model=param.model)
|
||||
response = await self._client.responses(create, stream=False)
|
||||
return response_to_chat_completion(response)
|
||||
|
||||
async def _stream_as_chat_chunks(
|
||||
self,
|
||||
create: ResponsesCreateParam,
|
||||
*,
|
||||
model: str,
|
||||
) -> AsyncIterator[ChatCompletionChunk]:
|
||||
stream = await self._client.responses(create, stream=True)
|
||||
final: Response | None = None
|
||||
async for event in stream:
|
||||
etype = event.type
|
||||
if etype == "response.output_text.delta" and isinstance(event.delta, str) and event.delta:
|
||||
yield text_delta_chunk(model=model, content=event.delta)
|
||||
continue
|
||||
if (
|
||||
etype
|
||||
in {
|
||||
"response.reasoning_summary_text.delta",
|
||||
"response.reasoning_text.delta",
|
||||
}
|
||||
and isinstance(event.delta, str)
|
||||
and event.delta
|
||||
):
|
||||
yield reasoning_delta_chunk(model=model, content=event.delta)
|
||||
continue
|
||||
if etype == "response.completed" and event.response is not UNSET:
|
||||
# Decode full response for tools + usage (field is dict | Unset).
|
||||
import msgspec
|
||||
|
||||
from plyngent.lmproto.openai.model import Response as ResponseModel
|
||||
|
||||
try:
|
||||
final = msgspec.convert(event.response, ResponseModel)
|
||||
except TypeError, ValueError, msgspec.ValidationError:
|
||||
final = None
|
||||
|
||||
if final is None:
|
||||
# Stream ended without response.completed — signal missing terminal.
|
||||
# Loop will treat empty + no finish_reason as a glitch.
|
||||
return
|
||||
|
||||
assistant = response_to_assistant_message(final)
|
||||
has_tools = assistant.tool_calls is not UNSET and bool(assistant.tool_calls)
|
||||
finish = responses_status_to_finish_reason(final, has_tool_calls=has_tools)
|
||||
for chunk in tool_call_chunks_from_response(final, model=model):
|
||||
yield chunk
|
||||
yield finish_reason_chunk(model=model, finish_reason=finish)
|
||||
usage = usage_chunk_from_response(final, model=model)
|
||||
if usage is not None:
|
||||
yield usage
|
||||
|
||||
|
||||
def wrap_openai_for_agent(
|
||||
client: OpenAIClient,
|
||||
*,
|
||||
provider_tools: Sequence[dict[str, Any]] | None = None,
|
||||
) -> ResponsesChatClient:
|
||||
"""Wrap a platform OpenAI client so the agent uses Responses by default."""
|
||||
return ResponsesChatClient(client, provider_tools=provider_tools)
|
||||
Reference in New Issue
Block a user