diff --git a/src/plyngent/agent/__init__.py b/src/plyngent/agent/__init__.py index 00f1349..078f672 100644 --- a/src/plyngent/agent/__init__.py +++ b/src/plyngent/agent/__init__.py @@ -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 diff --git a/src/plyngent/agent/client.py b/src/plyngent/agent/client.py deleted file mode 100644 index d217024..0000000 --- a/src/plyngent/agent/client.py +++ /dev/null @@ -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]: ... diff --git a/src/plyngent/agent/responses_client.py b/src/plyngent/agent/responses_client.py deleted file mode 100644 index 324eded..0000000 --- a/src/plyngent/agent/responses_client.py +++ /dev/null @@ -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)