mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Simplify Python hosting core (#6492)
Remove linking, multicast, durable delivery, and host push machinery from the v1 hosting core. Keep those scenarios in a proposed follow-up ADR and update channel packages, samples, docs, tests, and workspace metadata around the smaller host/channel contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
e5a6e35843
commit
36ce0950e4
+20
-46
@@ -9,21 +9,17 @@ get an answer back — no OpenAI-style envelope, no Responses item lattice.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator, Awaitable
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import Any, cast
|
||||
|
||||
from agent_framework_hosting import (
|
||||
ChannelContext,
|
||||
ChannelContribution,
|
||||
ChannelRequest,
|
||||
ChannelResponseContext,
|
||||
ChannelResponseHook,
|
||||
ChannelRunHook,
|
||||
ChannelSession,
|
||||
ChannelStreamTransformHook,
|
||||
HostedRunResult,
|
||||
apply_response_hook,
|
||||
apply_run_hook,
|
||||
ChannelStreamUpdateHook,
|
||||
logger,
|
||||
)
|
||||
from starlette.requests import Request
|
||||
@@ -47,7 +43,7 @@ class InvocationsChannel:
|
||||
path: str = "/invocations",
|
||||
run_hook: ChannelRunHook | None = None,
|
||||
response_hook: ChannelResponseHook | None = None,
|
||||
stream_transform_hook: ChannelStreamTransformHook | None = None,
|
||||
stream_update_hook: ChannelStreamUpdateHook | None = None,
|
||||
) -> None:
|
||||
"""Configure the invocations endpoint.
|
||||
|
||||
@@ -58,13 +54,13 @@ class InvocationsChannel:
|
||||
translate the wire payload into ``Message`` instances.
|
||||
``response_hook`` may rewrite the :class:`HostedRunResult` before
|
||||
the channel serializes it to JSON for the originating caller.
|
||||
``stream_transform_hook`` lets callers map or drop individual
|
||||
``stream_update_hook`` lets callers map or drop individual
|
||||
``AgentResponseUpdate`` chunks while streaming.
|
||||
"""
|
||||
self.path = path
|
||||
self._hook = run_hook
|
||||
self.response_hook = response_hook
|
||||
self._stream_transform_hook = stream_transform_hook
|
||||
self._stream_update_hook = stream_update_hook
|
||||
self._ctx: ChannelContext | None = None
|
||||
|
||||
def contribute(self, context: ChannelContext) -> ChannelContribution:
|
||||
@@ -115,43 +111,23 @@ class InvocationsChannel:
|
||||
attributes=attributes,
|
||||
)
|
||||
|
||||
if self._hook is not None:
|
||||
channel_request = await apply_run_hook(
|
||||
self._hook,
|
||||
channel_request,
|
||||
target=self._ctx.target,
|
||||
protocol_request=body_map,
|
||||
)
|
||||
|
||||
if channel_request.stream:
|
||||
return StreamingResponse(
|
||||
self._stream(channel_request),
|
||||
self._stream(channel_request, body_map),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
|
||||
result = await self._ctx.run(channel_request)
|
||||
result = await self._apply_response_hook(result, channel_request)
|
||||
result = await self._ctx.run(
|
||||
channel_request,
|
||||
run_hook=self._hook,
|
||||
protocol_request=body_map,
|
||||
response_hook=self.response_hook,
|
||||
channel_name=self.name,
|
||||
)
|
||||
return JSONResponse({"response": result.result.text, "session_id": session_id})
|
||||
|
||||
async def _apply_response_hook(
|
||||
self,
|
||||
result: HostedRunResult[Any],
|
||||
request: ChannelRequest,
|
||||
) -> HostedRunResult[Any]:
|
||||
"""Apply the channel-level response hook for an originating reply."""
|
||||
if self.response_hook is None:
|
||||
return result
|
||||
context = ChannelResponseContext(
|
||||
request=request,
|
||||
channel_name=self.name,
|
||||
destination_identity=None,
|
||||
originating=True,
|
||||
is_echo=False,
|
||||
)
|
||||
return await apply_response_hook(self.response_hook, result, context=context)
|
||||
|
||||
async def _stream(self, request: ChannelRequest) -> AsyncIterator[str]:
|
||||
async def _stream(self, request: ChannelRequest, protocol_request: dict[str, Any]) -> AsyncIterator[str]:
|
||||
r"""Yield bare ``data:`` SSE lines for each text chunk + a final ``[DONE]``.
|
||||
|
||||
SSE protocol notes:
|
||||
@@ -171,15 +147,13 @@ class InvocationsChannel:
|
||||
yield "event: error\ndata: channel not initialized\n\n"
|
||||
return
|
||||
try:
|
||||
stream = self._ctx.run_stream(request)
|
||||
stream = await self._ctx.run_stream(
|
||||
request,
|
||||
run_hook=self._hook,
|
||||
protocol_request=protocol_request,
|
||||
stream_update_hook=self._stream_update_hook,
|
||||
)
|
||||
async for update in stream:
|
||||
if self._stream_transform_hook is not None:
|
||||
transformed = self._stream_transform_hook(update)
|
||||
if isinstance(transformed, Awaitable):
|
||||
transformed = await transformed
|
||||
if transformed is None:
|
||||
continue
|
||||
update = transformed
|
||||
chunk = getattr(update, "text", None)
|
||||
if chunk:
|
||||
# Each text chunk is its own SSE event so curl-friendly
|
||||
|
||||
@@ -137,24 +137,22 @@ class TestInvocations:
|
||||
|
||||
async def hook(req: ChannelRequest, **_: Any) -> ChannelRequest:
|
||||
captured.append(req)
|
||||
# Force stream off even if requested.
|
||||
return replace(req, stream=False)
|
||||
return replace(req, input="rewritten")
|
||||
|
||||
agent = _FakeAgent(reply="ok")
|
||||
host = AgentFrameworkHost(target=agent, channels=[InvocationsChannel(run_hook=hook)])
|
||||
with TestClient(host.app) as client:
|
||||
r = client.post("/invocations", json={"message": "x", "stream": True})
|
||||
assert r.status_code == 200
|
||||
# Even though caller asked for stream=True, hook flipped it off — so
|
||||
# we get JSON back, not SSE.
|
||||
assert r.headers["content-type"].startswith("application/json")
|
||||
assert r.headers["content-type"].startswith("text/event-stream")
|
||||
assert captured and captured[0].channel == "invocations"
|
||||
assert agent.calls[0]["messages"].text == "rewritten"
|
||||
|
||||
def test_response_hook_can_rewrite_originating_reply(self) -> None:
|
||||
contexts: list[Any] = []
|
||||
seen_kwargs: list[dict[str, Any]] = []
|
||||
|
||||
def hook(result: HostedRunResult, **kwargs: Any) -> HostedRunResult:
|
||||
contexts.append(kwargs["context"])
|
||||
seen_kwargs.append(dict(kwargs))
|
||||
return HostedRunResult(_FakeAgentResponse(text=f"hooked:{result.result.text}"), session=result.session)
|
||||
|
||||
agent = _FakeAgent(reply="pong")
|
||||
@@ -165,12 +163,10 @@ class TestInvocations:
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json() == {"response": "hooked:pong", "session_id": None}
|
||||
assert contexts
|
||||
assert contexts[0].channel_name == "invocations"
|
||||
assert contexts[0].originating is True
|
||||
assert contexts[0].destination_identity is None
|
||||
assert seen_kwargs
|
||||
assert seen_kwargs[0]["channel_name"] == "invocations"
|
||||
|
||||
def test_stream_transform_hook_can_rewrite_chunks(self) -> None:
|
||||
def test_stream_update_hook_can_rewrite_chunks(self) -> None:
|
||||
agent = _FakeAgent(chunks=["foo", "bar"])
|
||||
|
||||
def transform(update: Any) -> Any:
|
||||
@@ -178,7 +174,7 @@ class TestInvocations:
|
||||
|
||||
host = AgentFrameworkHost(
|
||||
target=agent,
|
||||
channels=[InvocationsChannel(stream_transform_hook=transform)],
|
||||
channels=[InvocationsChannel(stream_update_hook=transform)],
|
||||
)
|
||||
with TestClient(host.app) as client:
|
||||
r = client.post("/invocations", json={"message": "x", "stream": True})
|
||||
@@ -188,7 +184,7 @@ class TestInvocations:
|
||||
assert "data: BAR" in body
|
||||
assert "data: foo" not in body
|
||||
|
||||
def test_stream_transform_hook_can_drop_chunks(self) -> None:
|
||||
def test_stream_update_hook_can_drop_chunks(self) -> None:
|
||||
agent = _FakeAgent(chunks=["keep", "drop", "keep2"])
|
||||
|
||||
def transform(update: Any) -> Any:
|
||||
@@ -196,7 +192,7 @@ class TestInvocations:
|
||||
|
||||
host = AgentFrameworkHost(
|
||||
target=agent,
|
||||
channels=[InvocationsChannel(stream_transform_hook=transform)],
|
||||
channels=[InvocationsChannel(stream_update_hook=transform)],
|
||||
)
|
||||
with TestClient(host.app) as client:
|
||||
r = client.post("/invocations", json={"message": "x", "stream": True})
|
||||
@@ -206,7 +202,7 @@ class TestInvocations:
|
||||
assert "data: keep2" in body
|
||||
assert "data: drop" not in body
|
||||
|
||||
def test_stream_transform_hook_supports_async(self) -> None:
|
||||
def test_stream_update_hook_supports_async(self) -> None:
|
||||
agent = _FakeAgent(chunks=["aa"])
|
||||
|
||||
async def transform(update: Any) -> Any:
|
||||
@@ -214,7 +210,7 @@ class TestInvocations:
|
||||
|
||||
host = AgentFrameworkHost(
|
||||
target=agent,
|
||||
channels=[InvocationsChannel(stream_transform_hook=transform)],
|
||||
channels=[InvocationsChannel(stream_update_hook=transform)],
|
||||
)
|
||||
with TestClient(host.app) as client:
|
||||
r = client.post("/invocations", json={"message": "x", "stream": True})
|
||||
|
||||
Reference in New Issue
Block a user