Skip to content

Commit da512fd

Browse files
committed
refactor(ai): route Agent.prompt()/stream() through a Transport strategy
The fake, record, inline-fake, and real branches each duplicated the _log_call() + _apply_schema() tail. Resolve the source once via a Transport (live / stand-in / inline fake) selected in _transport(), so each public method has a single call path and logs/applies its schema in one place. Pure internal refactor: the public prompt/stream/fake/record API and the fake/record mechanism are unchanged.
1 parent 6b2aba8 commit da512fd

2 files changed

Lines changed: 161 additions & 41 deletions

File tree

fastapi_startkit/src/fastapi_startkit/ai/agent.py

Lines changed: 19 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .document import Document
77
from .response import AgentResponse, AgentSnapshot
88
from .testing import AgentBinding
9+
from .transport import InlineFakeTransport, LiveTransport, StandInTransport, Transport
910

1011
if TYPE_CHECKING:
1112
from langchain_core.tools import BaseTool
@@ -49,31 +50,12 @@ async def prompt(
4950
attachments: list[Document] | None = None,
5051
provider_options: dict | None = None,
5152
) -> AgentResponse:
52-
stand_in = self._faked()
53-
if stand_in is not None:
54-
response = await stand_in.prompt(message, attachments=attachments)
55-
self._log_call("prompt", message)
56-
return self._apply_schema(response)
57-
58-
_run_kwargs = dict(
53+
response = await self._transport(message).prompt(
54+
message,
5955
model=model,
6056
attachments=attachments,
6157
provider_options=provider_options,
6258
)
63-
64-
match = self._match_fake(message)
65-
if match is not None:
66-
if isinstance(match, AgentSnapshot):
67-
response = await match.resolve(self, message, **_run_kwargs)
68-
else:
69-
response = match
70-
self._log_call("prompt", message)
71-
return self._apply_schema(response)
72-
73-
messages = self._build_messages(message, attachments)
74-
chat_model = self._build_model(model, provider_options)
75-
76-
response = await self._run_pipeline(chat_model, messages)
7759
self._log_call("prompt", message)
7860
return self._apply_schema(response)
7961

@@ -85,26 +67,11 @@ async def stream(
8567
provider_options: dict | None = None,
8668
) -> AsyncIterator[str]:
8769
self._log_call("stream", message)
88-
89-
swapped = self._faked()
90-
if swapped is not None:
91-
if hasattr(swapped, "stream"):
92-
async for chunk in swapped.stream(message):
93-
yield chunk
94-
else:
95-
response = await swapped.prompt(message)
96-
yield response.content
97-
return
98-
99-
fake = self._match_fake(message)
100-
if fake is not None:
101-
if isinstance(fake, AgentSnapshot):
102-
response = await fake.resolve(self, message)
103-
else:
104-
response = fake
105-
yield response.content
106-
return
107-
async for chunk in self._stream(message, model=model, provider_options=provider_options):
70+
async for chunk in self._transport(message).stream(
71+
message,
72+
model=model,
73+
provider_options=provider_options,
74+
):
10875
yield chunk
10976

11077
@classmethod
@@ -135,6 +102,17 @@ def _faked(self) -> Any:
135102
binding = type(self)._binding()
136103
return binding if binding is not self else None
137104

105+
def _transport(self, message: str) -> Transport:
106+
stand_in = self._faked()
107+
if stand_in is not None:
108+
return StandInTransport(self, stand_in)
109+
110+
match = self._match_fake(message)
111+
if match is not None:
112+
return InlineFakeTransport(self, match)
113+
114+
return LiveTransport(self)
115+
138116
def assert_prompted(self, times: int | None = None) -> None:
139117
calls = [c for c in self._call_log if c["method"] in ("prompt", "stream")]
140118
if times is not None:
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""Transport strategies for Agent.prompt()/stream().
2+
3+
A single call resolves exactly one transport — the real model pipeline, a
4+
stand-in bound via ``Agent.fake()``/``Agent.record()``, or an inline fake matched
5+
from ``Agent._fakes``. Concentrating the branch here lets the public methods keep
6+
one call path and log / apply their schema in one place instead of once per branch.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from typing import TYPE_CHECKING, Any, AsyncIterator
12+
13+
from .response import AgentResponse, AgentSnapshot
14+
15+
if TYPE_CHECKING:
16+
from .agent import Agent
17+
from .document import Document
18+
19+
20+
class Transport:
21+
def __init__(self, agent: Agent) -> None:
22+
self._agent = agent
23+
24+
async def prompt(
25+
self,
26+
message: str,
27+
*,
28+
model: str | None,
29+
attachments: list[Document] | None,
30+
provider_options: dict | None,
31+
) -> AgentResponse:
32+
raise NotImplementedError
33+
34+
def stream(
35+
self,
36+
message: str,
37+
*,
38+
model: str | None,
39+
provider_options: dict | None,
40+
) -> AsyncIterator[str]:
41+
raise NotImplementedError
42+
43+
44+
class LiveTransport(Transport):
45+
"""Runs the real model pipeline."""
46+
47+
async def prompt(
48+
self,
49+
message: str,
50+
*,
51+
model: str | None,
52+
attachments: list[Document] | None,
53+
provider_options: dict | None,
54+
) -> AgentResponse:
55+
agent = self._agent
56+
messages = agent._build_messages(message, attachments)
57+
chat_model = agent._build_model(model, provider_options)
58+
return await agent._run_pipeline(chat_model, messages)
59+
60+
async def stream(
61+
self,
62+
message: str,
63+
*,
64+
model: str | None,
65+
provider_options: dict | None,
66+
) -> AsyncIterator[str]:
67+
async for chunk in self._agent._stream(message, model=model, provider_options=provider_options):
68+
yield chunk
69+
70+
71+
class StandInTransport(Transport):
72+
"""Delegates to a stand-in bound via Agent.fake()/record()."""
73+
74+
def __init__(self, agent: Agent, stand_in: Any) -> None:
75+
super().__init__(agent)
76+
self._stand_in = stand_in
77+
78+
async def prompt(
79+
self,
80+
message: str,
81+
*,
82+
model: str | None,
83+
attachments: list[Document] | None,
84+
provider_options: dict | None,
85+
) -> AgentResponse:
86+
return await self._stand_in.prompt(message, attachments=attachments)
87+
88+
async def stream(
89+
self,
90+
message: str,
91+
*,
92+
model: str | None,
93+
provider_options: dict | None,
94+
) -> AsyncIterator[str]:
95+
stand_in = self._stand_in
96+
if hasattr(stand_in, "stream"):
97+
async for chunk in stand_in.stream(message):
98+
yield chunk
99+
else:
100+
response = await stand_in.prompt(message)
101+
yield response.content
102+
103+
104+
class InlineFakeTransport(Transport):
105+
"""Serves a canned response matched from Agent._fakes."""
106+
107+
def __init__(self, agent: Agent, match: AgentResponse | AgentSnapshot) -> None:
108+
super().__init__(agent)
109+
self._match = match
110+
111+
async def prompt(
112+
self,
113+
message: str,
114+
*,
115+
model: str | None,
116+
attachments: list[Document] | None,
117+
provider_options: dict | None,
118+
) -> AgentResponse:
119+
match = self._match
120+
if isinstance(match, AgentSnapshot):
121+
return await match.resolve(
122+
self._agent,
123+
message,
124+
model=model,
125+
attachments=attachments,
126+
provider_options=provider_options,
127+
)
128+
return match
129+
130+
async def stream(
131+
self,
132+
message: str,
133+
*,
134+
model: str | None,
135+
provider_options: dict | None,
136+
) -> AsyncIterator[str]:
137+
match = self._match
138+
if isinstance(match, AgentSnapshot):
139+
response = await match.resolve(self._agent, message)
140+
else:
141+
response = match
142+
yield response.content

0 commit comments

Comments
 (0)