mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 23:47: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 .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 build_compacted_seed_messages as build_compacted_seed_messages
|
||||||
from .compact import summarize_messages as summarize_messages
|
from .compact import summarize_messages as summarize_messages
|
||||||
from .events import AgentEvent as AgentEvent
|
from .events import AgentEvent as AgentEvent
|
||||||
@@ -14,8 +13,6 @@ from .events import ToolResultEvent as ToolResultEvent
|
|||||||
from .events import UsageEvent as UsageEvent
|
from .events import UsageEvent as UsageEvent
|
||||||
from .loop import DEFAULT_MAX_ROUNDS as DEFAULT_MAX_ROUNDS
|
from .loop import DEFAULT_MAX_ROUNDS as DEFAULT_MAX_ROUNDS
|
||||||
from .loop import run_chat_loop as run_chat_loop
|
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 DangerClassifier as DangerClassifier
|
||||||
from .tools import ToolConfirmHook as ToolConfirmHook
|
from .tools import ToolConfirmHook as ToolConfirmHook
|
||||||
from .tools import ToolDefinition as ToolDefinition
|
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