lexsi-sdk 0.1.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lexsi_sdk/__init__.py +5 -0
- lexsi_sdk/client/__init__.py +0 -0
- lexsi_sdk/client/client.py +176 -0
- lexsi_sdk/common/__init__.py +0 -0
- lexsi_sdk/common/config/.env.prod +3 -0
- lexsi_sdk/common/constants.py +143 -0
- lexsi_sdk/common/enums.py +8 -0
- lexsi_sdk/common/environment.py +49 -0
- lexsi_sdk/common/monitoring.py +81 -0
- lexsi_sdk/common/trigger.py +75 -0
- lexsi_sdk/common/types.py +122 -0
- lexsi_sdk/common/utils.py +93 -0
- lexsi_sdk/common/validation.py +110 -0
- lexsi_sdk/common/xai_uris.py +197 -0
- lexsi_sdk/core/__init__.py +0 -0
- lexsi_sdk/core/agent.py +62 -0
- lexsi_sdk/core/alert.py +56 -0
- lexsi_sdk/core/case.py +618 -0
- lexsi_sdk/core/dashboard.py +131 -0
- lexsi_sdk/core/guardrails/__init__.py +0 -0
- lexsi_sdk/core/guardrails/guard_template.py +299 -0
- lexsi_sdk/core/guardrails/guardrail_autogen.py +554 -0
- lexsi_sdk/core/guardrails/guardrails_langgraph.py +525 -0
- lexsi_sdk/core/guardrails/guardrails_openai.py +541 -0
- lexsi_sdk/core/guardrails/openai_runner.py +1328 -0
- lexsi_sdk/core/model_summary.py +110 -0
- lexsi_sdk/core/organization.py +549 -0
- lexsi_sdk/core/project.py +5131 -0
- lexsi_sdk/core/synthetic.py +387 -0
- lexsi_sdk/core/text.py +595 -0
- lexsi_sdk/core/tracer.py +208 -0
- lexsi_sdk/core/utils.py +36 -0
- lexsi_sdk/core/workspace.py +325 -0
- lexsi_sdk/core/wrapper.py +766 -0
- lexsi_sdk/core/xai.py +306 -0
- lexsi_sdk/version.py +34 -0
- lexsi_sdk-0.1.16.dist-info/METADATA +100 -0
- lexsi_sdk-0.1.16.dist-info/RECORD +40 -0
- lexsi_sdk-0.1.16.dist-info/WHEEL +5 -0
- lexsi_sdk-0.1.16.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1328 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
from agents import (
|
|
4
|
+
Agent,
|
|
5
|
+
AgentOutputSchema,
|
|
6
|
+
AgentOutputSchemaBase,
|
|
7
|
+
AgentSpanData,
|
|
8
|
+
AgentsException,
|
|
9
|
+
Handoff,
|
|
10
|
+
Model,
|
|
11
|
+
ModelBehaviorError,
|
|
12
|
+
ModelResponse,
|
|
13
|
+
OutputGuardrail,
|
|
14
|
+
OutputGuardrailResult,
|
|
15
|
+
OutputGuardrailTripwireTriggered,
|
|
16
|
+
RunConfig,
|
|
17
|
+
RunContextWrapper,
|
|
18
|
+
RunErrorDetails,
|
|
19
|
+
RunHooks,
|
|
20
|
+
RunItem,
|
|
21
|
+
RunResult,
|
|
22
|
+
RunResultStreaming,
|
|
23
|
+
Session,
|
|
24
|
+
Span,
|
|
25
|
+
SpanError,
|
|
26
|
+
Tool,
|
|
27
|
+
Usage,
|
|
28
|
+
agent_span,
|
|
29
|
+
trace,
|
|
30
|
+
)
|
|
31
|
+
from openai.types.responses import ResponseCompletedEvent
|
|
32
|
+
from openai.types.responses.response_prompt_param import (
|
|
33
|
+
ResponsePromptParam,
|
|
34
|
+
)
|
|
35
|
+
from agents.exceptions import (
|
|
36
|
+
AgentsException,
|
|
37
|
+
InputGuardrailTripwireTriggered,
|
|
38
|
+
MaxTurnsExceeded,
|
|
39
|
+
ModelBehaviorError,
|
|
40
|
+
OutputGuardrailTripwireTriggered,
|
|
41
|
+
RunErrorDetails,
|
|
42
|
+
UserError,
|
|
43
|
+
)
|
|
44
|
+
from agents.guardrail import (
|
|
45
|
+
InputGuardrail,
|
|
46
|
+
InputGuardrailResult,
|
|
47
|
+
OutputGuardrail,
|
|
48
|
+
OutputGuardrailResult,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
from agents.handoffs import Handoff, HandoffInputFilter, handoff
|
|
52
|
+
|
|
53
|
+
from typing import Any, cast, Generic
|
|
54
|
+
from agents.run import (
|
|
55
|
+
TResponseInputItem,
|
|
56
|
+
TContext,
|
|
57
|
+
DEFAULT_MAX_TURNS,
|
|
58
|
+
RunOptions,
|
|
59
|
+
AgentToolUseTracker,
|
|
60
|
+
_error_tracing,
|
|
61
|
+
TraceCtxManager,
|
|
62
|
+
get_model_tracing_impl,
|
|
63
|
+
AgentToolUseTracker,
|
|
64
|
+
NextStepFinalOutput,
|
|
65
|
+
NextStepHandoff,
|
|
66
|
+
NextStepRunAgain,
|
|
67
|
+
QueueCompleteSentinel,
|
|
68
|
+
RunImpl,
|
|
69
|
+
SingleStepResult,
|
|
70
|
+
TraceCtxManager,
|
|
71
|
+
)
|
|
72
|
+
from agents.stream_events import AgentUpdatedStreamEvent
|
|
73
|
+
from agents.items import ItemHelpers, ModelResponse, RunItem, TResponseInputItem
|
|
74
|
+
from agents.lifecycle import RunHooks
|
|
75
|
+
from agents.logger import logger
|
|
76
|
+
from agents.memory import Session
|
|
77
|
+
from agents.model_settings import ModelSettings
|
|
78
|
+
from agents.models.interface import Model, ModelProvider
|
|
79
|
+
from agents.models.multi_provider import MultiProvider
|
|
80
|
+
from agents.result import RunResult, RunResultStreaming
|
|
81
|
+
from agents.run_context import RunContextWrapper, TContext
|
|
82
|
+
from agents.stream_events import AgentUpdatedStreamEvent, RawResponsesStreamEvent
|
|
83
|
+
from agents.tool import Tool
|
|
84
|
+
from agents.tracing import Span, SpanError, agent_span, get_current_trace, trace
|
|
85
|
+
from agents.tracing.span_data import AgentSpanData
|
|
86
|
+
from agents.usage import Usage
|
|
87
|
+
from agents.util import _coro, _error_tracing
|
|
88
|
+
from agents.util._types import MaybeAwaitable
|
|
89
|
+
|
|
90
|
+
from typing_extensions import Unpack
|
|
91
|
+
import asyncio
|
|
92
|
+
from agents.tracing import (
|
|
93
|
+
get_current_trace,
|
|
94
|
+
)
|
|
95
|
+
from dataclasses import dataclass, field
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class ModelInputData:
|
|
100
|
+
"""Container for the data that will be sent to the model."""
|
|
101
|
+
|
|
102
|
+
input: list[TResponseInputItem]
|
|
103
|
+
instructions: str | None
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class CallModelData(Generic[TContext]):
|
|
107
|
+
"""Data passed to `RunConfig.call_model_input_filter` prior to model call."""
|
|
108
|
+
|
|
109
|
+
model_data: ModelInputData
|
|
110
|
+
agent: Agent[TContext]
|
|
111
|
+
context: TContext | None
|
|
112
|
+
|
|
113
|
+
class Runner:
|
|
114
|
+
"""Orchestrates agent execution loops for OpenAI Agents."""
|
|
115
|
+
@classmethod
|
|
116
|
+
async def run(
|
|
117
|
+
cls,
|
|
118
|
+
starting_agent: Agent[TContext],
|
|
119
|
+
input: str | list[TResponseInputItem],
|
|
120
|
+
*,
|
|
121
|
+
context: TContext | None = None,
|
|
122
|
+
max_turns: int = DEFAULT_MAX_TURNS,
|
|
123
|
+
hooks: RunHooks[TContext] | None = None,
|
|
124
|
+
run_config: RunConfig | None = None,
|
|
125
|
+
previous_response_id: str | None = None,
|
|
126
|
+
session: Session | None = None,
|
|
127
|
+
) -> RunResult:
|
|
128
|
+
"""Run a workflow starting at the given agent. The agent will run in a loop until a final
|
|
129
|
+
output is generated. The loop runs like so:
|
|
130
|
+
1. The agent is invoked with the given input.
|
|
131
|
+
2. If there is a final output (i.e. the agent produces something of type
|
|
132
|
+
`agent.output_type`, the loop terminates.
|
|
133
|
+
3. If there's a handoff, we run the loop again, with the new agent.
|
|
134
|
+
4. Else, we run tool calls (if any), and re-run the loop.
|
|
135
|
+
In two cases, the agent may raise an exception:
|
|
136
|
+
1. If the max_turns is exceeded, a MaxTurnsExceeded exception is raised.
|
|
137
|
+
2. If a guardrail tripwire is triggered, a GuardrailTripwireTriggered exception is raised.
|
|
138
|
+
Note that only the first agent's input guardrails are run.
|
|
139
|
+
|
|
140
|
+
:param starting_agent: The starting agent to run.
|
|
141
|
+
:param input: The initial input to the agent; string user message or list of items.
|
|
142
|
+
:param context: Optional context to run the agent with.
|
|
143
|
+
:param max_turns: Maximum number of turns to run the agent.
|
|
144
|
+
:param hooks: Callbacks for lifecycle events.
|
|
145
|
+
:param run_config: Global settings for the entire agent run.
|
|
146
|
+
:param previous_response_id: Prior response id to continue Responses API runs.
|
|
147
|
+
:param session: Optional session used by the runner.
|
|
148
|
+
:return: Run result containing inputs, guardrail results, and final output.
|
|
149
|
+
"""
|
|
150
|
+
runner = DEFAULT_AGENT_RUNNER
|
|
151
|
+
return await runner.run(
|
|
152
|
+
starting_agent,
|
|
153
|
+
input,
|
|
154
|
+
context=context,
|
|
155
|
+
max_turns=max_turns,
|
|
156
|
+
hooks=hooks,
|
|
157
|
+
run_config=run_config,
|
|
158
|
+
previous_response_id=previous_response_id,
|
|
159
|
+
session=session,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def run_sync(
|
|
164
|
+
cls,
|
|
165
|
+
starting_agent: Agent[TContext],
|
|
166
|
+
input: str | list[TResponseInputItem],
|
|
167
|
+
*,
|
|
168
|
+
context: TContext | None = None,
|
|
169
|
+
max_turns: int = DEFAULT_MAX_TURNS,
|
|
170
|
+
hooks: RunHooks[TContext] | None = None,
|
|
171
|
+
run_config: RunConfig | None = None,
|
|
172
|
+
previous_response_id: str | None = None,
|
|
173
|
+
session: Session | None = None,
|
|
174
|
+
) -> RunResult:
|
|
175
|
+
"""Run a workflow synchronously, starting at the given agent. Note that this just wraps the
|
|
176
|
+
`run` method, so it will not work if there's already an event loop (e.g. inside an async
|
|
177
|
+
function, or in a Jupyter notebook or async context like FastAPI). For those cases, use
|
|
178
|
+
the `run` method instead.
|
|
179
|
+
The agent will run in a loop until a final output is generated. The loop runs like so:
|
|
180
|
+
1. The agent is invoked with the given input.
|
|
181
|
+
2. If there is a final output (i.e. the agent produces something of type
|
|
182
|
+
`agent.output_type`, the loop terminates.
|
|
183
|
+
3. If there's a handoff, we run the loop again, with the new agent.
|
|
184
|
+
4. Else, we run tool calls (if any), and re-run the loop.
|
|
185
|
+
In two cases, the agent may raise an exception:
|
|
186
|
+
1. If the max_turns is exceeded, a MaxTurnsExceeded exception is raised.
|
|
187
|
+
2. If a guardrail tripwire is triggered, a GuardrailTripwireTriggered exception is raised.
|
|
188
|
+
Note that only the first agent's input guardrails are run.
|
|
189
|
+
|
|
190
|
+
:param starting_agent: The starting agent to run.
|
|
191
|
+
:param input: The initial input to the agent; string user message or list of items.
|
|
192
|
+
:param context: Optional context to run the agent with.
|
|
193
|
+
:param max_turns: Maximum number of turns to run the agent.
|
|
194
|
+
:param hooks: Callbacks for lifecycle events.
|
|
195
|
+
:param run_config: Global settings for the entire agent run.
|
|
196
|
+
:param previous_response_id: Prior response id to continue Responses API runs.
|
|
197
|
+
:param session: Optional session used by the runner.
|
|
198
|
+
:return: Run result containing inputs, guardrail results, and final output.
|
|
199
|
+
"""
|
|
200
|
+
runner = DEFAULT_AGENT_RUNNER
|
|
201
|
+
return runner.run_sync(
|
|
202
|
+
starting_agent,
|
|
203
|
+
input,
|
|
204
|
+
context=context,
|
|
205
|
+
max_turns=max_turns,
|
|
206
|
+
hooks=hooks,
|
|
207
|
+
run_config=run_config,
|
|
208
|
+
previous_response_id=previous_response_id,
|
|
209
|
+
session=session,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
def run_streamed(
|
|
214
|
+
cls,
|
|
215
|
+
starting_agent: Agent[TContext],
|
|
216
|
+
input: str | list[TResponseInputItem],
|
|
217
|
+
context: TContext | None = None,
|
|
218
|
+
max_turns: int = DEFAULT_MAX_TURNS,
|
|
219
|
+
hooks: RunHooks[TContext] | None = None,
|
|
220
|
+
run_config: RunConfig | None = None,
|
|
221
|
+
previous_response_id: str | None = None,
|
|
222
|
+
session: Session | None = None,
|
|
223
|
+
) -> RunResultStreaming:
|
|
224
|
+
"""Run a workflow starting at the given agent in streaming mode. The returned result object
|
|
225
|
+
contains a method you can use to stream semantic events as they are generated.
|
|
226
|
+
The agent will run in a loop until a final output is generated. The loop runs like so:
|
|
227
|
+
1. The agent is invoked with the given input.
|
|
228
|
+
2. If there is a final output (i.e. the agent produces something of type
|
|
229
|
+
`agent.output_type`, the loop terminates.
|
|
230
|
+
3. If there's a handoff, we run the loop again, with the new agent.
|
|
231
|
+
4. Else, we run tool calls (if any), and re-run the loop.
|
|
232
|
+
In two cases, the agent may raise an exception:
|
|
233
|
+
1. If the max_turns is exceeded, a MaxTurnsExceeded exception is raised.
|
|
234
|
+
2. If a guardrail tripwire is triggered, a GuardrailTripwireTriggered exception is raised.
|
|
235
|
+
Note that only the first agent's input guardrails are run.
|
|
236
|
+
|
|
237
|
+
:param starting_agent: The starting agent to run.
|
|
238
|
+
:param input: Initial input; string user message or list of items.
|
|
239
|
+
:param context: Optional context to run the agent with.
|
|
240
|
+
:param max_turns: Maximum number of turns to run the agent.
|
|
241
|
+
:param hooks: Callbacks for lifecycle events.
|
|
242
|
+
:param run_config: Global settings for the entire agent run.
|
|
243
|
+
:param previous_response_id: Prior response id to continue Responses API runs.
|
|
244
|
+
:param session: Optional session used by the runner.
|
|
245
|
+
:return: Result object containing run data and streaming method.
|
|
246
|
+
"""
|
|
247
|
+
runner = DEFAULT_AGENT_RUNNER
|
|
248
|
+
return runner.run_streamed(
|
|
249
|
+
starting_agent,
|
|
250
|
+
input,
|
|
251
|
+
context=context,
|
|
252
|
+
max_turns=max_turns,
|
|
253
|
+
hooks=hooks,
|
|
254
|
+
run_config=run_config,
|
|
255
|
+
previous_response_id=previous_response_id,
|
|
256
|
+
session=session,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class AgentRunner:
|
|
261
|
+
"""Async runner that manages the OpenAI Agents control loop."""
|
|
262
|
+
async def run(
|
|
263
|
+
self,
|
|
264
|
+
starting_agent: Agent[TContext],
|
|
265
|
+
input: str | list[TResponseInputItem],
|
|
266
|
+
**kwargs: Unpack[RunOptions[TContext]],
|
|
267
|
+
) -> RunResult:
|
|
268
|
+
"""Run the agent workflow asynchronously until completion."""
|
|
269
|
+
context = kwargs.get("context")
|
|
270
|
+
max_turns = kwargs.get("max_turns", DEFAULT_MAX_TURNS)
|
|
271
|
+
hooks = kwargs.get("hooks")
|
|
272
|
+
run_config = kwargs.get("run_config")
|
|
273
|
+
previous_response_id = kwargs.get("previous_response_id")
|
|
274
|
+
session = kwargs.get("session")
|
|
275
|
+
if hooks is None:
|
|
276
|
+
hooks = RunHooks[Any]()
|
|
277
|
+
if run_config is None:
|
|
278
|
+
run_config = RunConfig()
|
|
279
|
+
|
|
280
|
+
# Prepare input with session if enabled
|
|
281
|
+
prepared_input = await self._prepare_input_with_session(input, session)
|
|
282
|
+
|
|
283
|
+
tool_use_tracker = AgentToolUseTracker()
|
|
284
|
+
|
|
285
|
+
with TraceCtxManager(
|
|
286
|
+
workflow_name=run_config.workflow_name,
|
|
287
|
+
trace_id=run_config.trace_id,
|
|
288
|
+
group_id=run_config.group_id,
|
|
289
|
+
metadata=run_config.trace_metadata,
|
|
290
|
+
disabled=run_config.tracing_disabled,
|
|
291
|
+
):
|
|
292
|
+
current_turn = 0
|
|
293
|
+
original_input: str | list[TResponseInputItem] = _copy_str_or_list(prepared_input)
|
|
294
|
+
generated_items: list[RunItem] = []
|
|
295
|
+
model_responses: list[ModelResponse] = []
|
|
296
|
+
|
|
297
|
+
context_wrapper: RunContextWrapper[TContext] = RunContextWrapper(
|
|
298
|
+
context=context # type: ignore
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
input_guardrail_results: list[InputGuardrailResult] = []
|
|
302
|
+
|
|
303
|
+
current_span: Span[AgentSpanData] | None = None
|
|
304
|
+
current_agent = starting_agent
|
|
305
|
+
should_run_agent_start_hooks = True
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
while True:
|
|
309
|
+
all_tools = await AgentRunner._get_all_tools(current_agent, context_wrapper)
|
|
310
|
+
|
|
311
|
+
# Start an agent span if we don't have one
|
|
312
|
+
if current_span is None:
|
|
313
|
+
handoff_names = [
|
|
314
|
+
h.agent_name
|
|
315
|
+
for h in await AgentRunner._get_handoffs(current_agent, context_wrapper)
|
|
316
|
+
]
|
|
317
|
+
if output_schema := AgentRunner._get_output_schema(current_agent):
|
|
318
|
+
output_type_name = output_schema.name()
|
|
319
|
+
else:
|
|
320
|
+
output_type_name = "str"
|
|
321
|
+
|
|
322
|
+
current_span = agent_span(
|
|
323
|
+
name=current_agent.name,
|
|
324
|
+
handoffs=handoff_names,
|
|
325
|
+
output_type=output_type_name,
|
|
326
|
+
)
|
|
327
|
+
current_span.start(mark_as_current=True)
|
|
328
|
+
current_span.span_data.tools = [t.name for t in all_tools]
|
|
329
|
+
|
|
330
|
+
current_turn += 1
|
|
331
|
+
if current_turn > max_turns:
|
|
332
|
+
_error_tracing.attach_error_to_span(
|
|
333
|
+
current_span,
|
|
334
|
+
SpanError(
|
|
335
|
+
message="Max turns exceeded",
|
|
336
|
+
data={"max_turns": max_turns},
|
|
337
|
+
),
|
|
338
|
+
)
|
|
339
|
+
raise MaxTurnsExceeded(f"Max turns ({max_turns}) exceeded")
|
|
340
|
+
|
|
341
|
+
# Run input guardrails for the first turn or after a handoff
|
|
342
|
+
if current_turn == 1 or isinstance(turn_result.next_step, NextStepHandoff):
|
|
343
|
+
input_guardrail_results.extend(
|
|
344
|
+
await self._run_input_guardrails(
|
|
345
|
+
current_agent,
|
|
346
|
+
current_agent.input_guardrails + (run_config.input_guardrails or []),
|
|
347
|
+
_copy_str_or_list(prepared_input if current_turn == 1 else original_input),
|
|
348
|
+
context_wrapper,
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
# Update original_input with sanitized content if any guardrail used 'retry'
|
|
352
|
+
for result in input_guardrail_results:
|
|
353
|
+
if hasattr(result.output, 'sanitized_content') and result.output.sanitized_content:
|
|
354
|
+
original_input = result.output.sanitized_content
|
|
355
|
+
|
|
356
|
+
turn_result = await self._run_single_turn(
|
|
357
|
+
agent=current_agent,
|
|
358
|
+
all_tools=all_tools,
|
|
359
|
+
original_input=original_input,
|
|
360
|
+
generated_items=generated_items,
|
|
361
|
+
hooks=hooks,
|
|
362
|
+
context_wrapper=context_wrapper,
|
|
363
|
+
run_config=run_config,
|
|
364
|
+
should_run_agent_start_hooks=should_run_agent_start_hooks,
|
|
365
|
+
tool_use_tracker=tool_use_tracker,
|
|
366
|
+
previous_response_id=previous_response_id,
|
|
367
|
+
)
|
|
368
|
+
should_run_agent_start_hooks = False
|
|
369
|
+
|
|
370
|
+
model_responses.append(turn_result.model_response)
|
|
371
|
+
original_input = turn_result.original_input
|
|
372
|
+
generated_items = turn_result.generated_items
|
|
373
|
+
|
|
374
|
+
if isinstance(turn_result.next_step, NextStepFinalOutput):
|
|
375
|
+
output_guardrail_results = await self._run_output_guardrails(
|
|
376
|
+
current_agent.output_guardrails + (run_config.output_guardrails or []),
|
|
377
|
+
current_agent,
|
|
378
|
+
turn_result.next_step.output,
|
|
379
|
+
context_wrapper,
|
|
380
|
+
)
|
|
381
|
+
# Use sanitized content as final output if available
|
|
382
|
+
final_output = turn_result.next_step.output
|
|
383
|
+
for result in output_guardrail_results:
|
|
384
|
+
if hasattr(result.output, 'sanitized_content') and result.output.sanitized_content:
|
|
385
|
+
final_output = result.output.sanitized_content
|
|
386
|
+
|
|
387
|
+
result = RunResult(
|
|
388
|
+
input=original_input,
|
|
389
|
+
new_items=generated_items,
|
|
390
|
+
raw_responses=model_responses,
|
|
391
|
+
final_output=final_output,
|
|
392
|
+
_last_agent=current_agent,
|
|
393
|
+
input_guardrail_results=input_guardrail_results,
|
|
394
|
+
output_guardrail_results=output_guardrail_results,
|
|
395
|
+
context_wrapper=context_wrapper,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Save the conversation to session if enabled
|
|
399
|
+
await self._save_result_to_session(session, input, result)
|
|
400
|
+
|
|
401
|
+
return result
|
|
402
|
+
elif isinstance(turn_result.next_step, NextStepHandoff):
|
|
403
|
+
current_agent = cast(Agent[TContext], turn_result.next_step.new_agent)
|
|
404
|
+
# Pass sanitized input to the handoff agent
|
|
405
|
+
for result in input_guardrail_results:
|
|
406
|
+
if hasattr(result.output, 'sanitized_content') and result.output.sanitized_content:
|
|
407
|
+
original_input = result.output.sanitized_content
|
|
408
|
+
current_span.finish(reset_current=True)
|
|
409
|
+
current_span = None
|
|
410
|
+
should_run_agent_start_hooks = True
|
|
411
|
+
elif isinstance(turn_result.next_step, NextStepRunAgain):
|
|
412
|
+
pass
|
|
413
|
+
else:
|
|
414
|
+
raise AgentsException(
|
|
415
|
+
f"Unknown next step type: {type(turn_result.next_step)}"
|
|
416
|
+
)
|
|
417
|
+
except AgentsException as exc:
|
|
418
|
+
exc.run_data = RunErrorDetails(
|
|
419
|
+
input=original_input,
|
|
420
|
+
new_items=generated_items,
|
|
421
|
+
raw_responses=model_responses,
|
|
422
|
+
last_agent=current_agent,
|
|
423
|
+
context_wrapper=context_wrapper,
|
|
424
|
+
input_guardrail_results=input_guardrail_results,
|
|
425
|
+
output_guardrail_results=[],
|
|
426
|
+
)
|
|
427
|
+
raise
|
|
428
|
+
finally:
|
|
429
|
+
if current_span:
|
|
430
|
+
current_span.finish(reset_current=True)
|
|
431
|
+
|
|
432
|
+
def run_sync(
|
|
433
|
+
self,
|
|
434
|
+
starting_agent: Agent[TContext],
|
|
435
|
+
input: str | list[TResponseInputItem],
|
|
436
|
+
**kwargs: Unpack[RunOptions[TContext]],
|
|
437
|
+
) -> RunResult:
|
|
438
|
+
"""Synchronous wrapper that runs the async agent loop to completion."""
|
|
439
|
+
context = kwargs.get("context")
|
|
440
|
+
max_turns = kwargs.get("max_turns", DEFAULT_MAX_TURNS)
|
|
441
|
+
hooks = kwargs.get("hooks")
|
|
442
|
+
run_config = kwargs.get("run_config")
|
|
443
|
+
previous_response_id = kwargs.get("previous_response_id")
|
|
444
|
+
session = kwargs.get("session")
|
|
445
|
+
|
|
446
|
+
return asyncio.get_event_loop().run_until_complete(
|
|
447
|
+
self.run(
|
|
448
|
+
starting_agent,
|
|
449
|
+
input,
|
|
450
|
+
session=session,
|
|
451
|
+
context=context,
|
|
452
|
+
max_turns=max_turns,
|
|
453
|
+
hooks=hooks,
|
|
454
|
+
run_config=run_config,
|
|
455
|
+
previous_response_id=previous_response_id,
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
def run_streamed(
|
|
460
|
+
self,
|
|
461
|
+
starting_agent: Agent[TContext],
|
|
462
|
+
input: str | list[TResponseInputItem],
|
|
463
|
+
**kwargs: Unpack[RunOptions[TContext]],
|
|
464
|
+
) -> RunResultStreaming:
|
|
465
|
+
"""Run the agent loop while streaming intermediate responses."""
|
|
466
|
+
context = kwargs.get("context")
|
|
467
|
+
max_turns = kwargs.get("max_turns", DEFAULT_MAX_TURNS)
|
|
468
|
+
hooks = kwargs.get("hooks")
|
|
469
|
+
run_config = kwargs.get("run_config")
|
|
470
|
+
previous_response_id = kwargs.get("previous_response_id")
|
|
471
|
+
session = kwargs.get("session")
|
|
472
|
+
|
|
473
|
+
if hooks is None:
|
|
474
|
+
hooks = RunHooks[Any]()
|
|
475
|
+
if run_config is None:
|
|
476
|
+
run_config = RunConfig()
|
|
477
|
+
|
|
478
|
+
new_trace = (
|
|
479
|
+
None
|
|
480
|
+
if get_current_trace()
|
|
481
|
+
else trace(
|
|
482
|
+
workflow_name=run_config.workflow_name,
|
|
483
|
+
trace_id=run_config.trace_id,
|
|
484
|
+
group_id=run_config.group_id,
|
|
485
|
+
metadata=run_config.trace_metadata,
|
|
486
|
+
disabled=run_config.tracing_disabled,
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
output_schema = AgentRunner._get_output_schema(starting_agent)
|
|
491
|
+
context_wrapper: RunContextWrapper[TContext] = RunContextWrapper(
|
|
492
|
+
context=context # type: ignore
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
streamed_result = RunResultStreaming(
|
|
496
|
+
input=_copy_str_or_list(input),
|
|
497
|
+
new_items=[],
|
|
498
|
+
current_agent=starting_agent,
|
|
499
|
+
raw_responses=[],
|
|
500
|
+
final_output=None,
|
|
501
|
+
is_complete=False,
|
|
502
|
+
current_turn=0,
|
|
503
|
+
max_turns=max_turns,
|
|
504
|
+
input_guardrail_results=[],
|
|
505
|
+
output_guardrail_results=[],
|
|
506
|
+
_current_agent_output_schema=output_schema,
|
|
507
|
+
trace=new_trace,
|
|
508
|
+
context_wrapper=context_wrapper,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
streamed_result._run_impl_task = asyncio.create_task(
|
|
512
|
+
self._start_streaming(
|
|
513
|
+
starting_input=input,
|
|
514
|
+
streamed_result=streamed_result,
|
|
515
|
+
starting_agent=starting_agent,
|
|
516
|
+
max_turns=max_turns,
|
|
517
|
+
hooks=hooks,
|
|
518
|
+
context_wrapper=context_wrapper,
|
|
519
|
+
run_config=run_config,
|
|
520
|
+
previous_response_id=previous_response_id,
|
|
521
|
+
session=session,
|
|
522
|
+
)
|
|
523
|
+
)
|
|
524
|
+
return streamed_result
|
|
525
|
+
|
|
526
|
+
@classmethod
|
|
527
|
+
async def _maybe_filter_model_input(
|
|
528
|
+
cls,
|
|
529
|
+
*,
|
|
530
|
+
agent: Agent[TContext],
|
|
531
|
+
run_config: RunConfig,
|
|
532
|
+
context_wrapper: RunContextWrapper[TContext],
|
|
533
|
+
input_items: list[TResponseInputItem],
|
|
534
|
+
system_instructions: str | None,
|
|
535
|
+
) -> ModelInputData:
|
|
536
|
+
"""Apply optional call_model_input_filter to modify model input.
|
|
537
|
+
|
|
538
|
+
Returns a `ModelInputData` that will be sent to the model.
|
|
539
|
+
"""
|
|
540
|
+
effective_instructions = system_instructions
|
|
541
|
+
effective_input: list[TResponseInputItem] = input_items
|
|
542
|
+
|
|
543
|
+
if run_config.call_model_input_filter is None:
|
|
544
|
+
return ModelInputData(input=effective_input, instructions=effective_instructions)
|
|
545
|
+
|
|
546
|
+
try:
|
|
547
|
+
model_input = ModelInputData(
|
|
548
|
+
input=effective_input.copy(),
|
|
549
|
+
instructions=effective_instructions,
|
|
550
|
+
)
|
|
551
|
+
filter_payload: CallModelData[TContext] = CallModelData(
|
|
552
|
+
model_data=model_input,
|
|
553
|
+
agent=agent,
|
|
554
|
+
context=context_wrapper.context,
|
|
555
|
+
)
|
|
556
|
+
maybe_updated = run_config.call_model_input_filter(filter_payload)
|
|
557
|
+
updated = await maybe_updated if inspect.isawaitable(maybe_updated) else maybe_updated
|
|
558
|
+
if not isinstance(updated, ModelInputData):
|
|
559
|
+
raise UserError("call_model_input_filter must return a ModelInputData instance")
|
|
560
|
+
return updated
|
|
561
|
+
except Exception as e:
|
|
562
|
+
_error_tracing.attach_error_to_current_span(
|
|
563
|
+
SpanError(message="Error in call_model_input_filter", data={"error": str(e)})
|
|
564
|
+
)
|
|
565
|
+
raise
|
|
566
|
+
|
|
567
|
+
@classmethod
|
|
568
|
+
async def _run_input_guardrails_with_queue(
|
|
569
|
+
cls,
|
|
570
|
+
agent: Agent[Any],
|
|
571
|
+
guardrails: list[InputGuardrail[TContext]],
|
|
572
|
+
input: str | list[TResponseInputItem],
|
|
573
|
+
context: RunContextWrapper[TContext],
|
|
574
|
+
streamed_result: RunResultStreaming,
|
|
575
|
+
parent_span: Span[Any],
|
|
576
|
+
):
|
|
577
|
+
"""Run input guardrails concurrently and enqueue results during streaming."""
|
|
578
|
+
queue = streamed_result._input_guardrail_queue
|
|
579
|
+
|
|
580
|
+
# We'll run the guardrails and push them onto the queue as they complete
|
|
581
|
+
guardrail_tasks = [
|
|
582
|
+
asyncio.create_task(
|
|
583
|
+
RunImpl.run_single_input_guardrail(agent, guardrail, input, context)
|
|
584
|
+
)
|
|
585
|
+
for guardrail in guardrails
|
|
586
|
+
]
|
|
587
|
+
guardrail_results = []
|
|
588
|
+
try:
|
|
589
|
+
for done in asyncio.as_completed(guardrail_tasks):
|
|
590
|
+
result = await done
|
|
591
|
+
if result.output.tripwire_triggered:
|
|
592
|
+
_error_tracing.attach_error_to_span(
|
|
593
|
+
parent_span,
|
|
594
|
+
SpanError(
|
|
595
|
+
message="Guardrail tripwire triggered",
|
|
596
|
+
data={
|
|
597
|
+
"guardrail": result.guardrail.get_name(),
|
|
598
|
+
"type": "input_guardrail",
|
|
599
|
+
},
|
|
600
|
+
),
|
|
601
|
+
)
|
|
602
|
+
queue.put_nowait(result)
|
|
603
|
+
guardrail_results.append(result)
|
|
604
|
+
except Exception:
|
|
605
|
+
for t in guardrail_tasks:
|
|
606
|
+
t.cancel()
|
|
607
|
+
raise
|
|
608
|
+
|
|
609
|
+
streamed_result.input_guardrail_results = guardrail_results
|
|
610
|
+
# Update streamed_result.input with sanitized content if available
|
|
611
|
+
for result in guardrail_results:
|
|
612
|
+
if hasattr(result.output, 'sanitized_content') and result.output.sanitized_content:
|
|
613
|
+
streamed_result.input = result.output.sanitized_content
|
|
614
|
+
|
|
615
|
+
@classmethod
|
|
616
|
+
async def _start_streaming(
|
|
617
|
+
cls,
|
|
618
|
+
starting_input: str | list[TResponseInputItem],
|
|
619
|
+
streamed_result: RunResultStreaming,
|
|
620
|
+
starting_agent: Agent[TContext],
|
|
621
|
+
max_turns: int,
|
|
622
|
+
hooks: RunHooks[TContext],
|
|
623
|
+
context_wrapper: RunContextWrapper[TContext],
|
|
624
|
+
run_config: RunConfig,
|
|
625
|
+
previous_response_id: str | None,
|
|
626
|
+
session: Session | None,
|
|
627
|
+
):
|
|
628
|
+
"""Primary loop for streaming agent executions."""
|
|
629
|
+
if streamed_result.trace:
|
|
630
|
+
streamed_result.trace.start(mark_as_current=True)
|
|
631
|
+
|
|
632
|
+
current_span: Span[AgentSpanData] | None = None
|
|
633
|
+
current_agent = starting_agent
|
|
634
|
+
current_turn = 0
|
|
635
|
+
should_run_agent_start_hooks = True
|
|
636
|
+
tool_use_tracker = AgentToolUseTracker()
|
|
637
|
+
|
|
638
|
+
streamed_result._event_queue.put_nowait(AgentUpdatedStreamEvent(new_agent=current_agent))
|
|
639
|
+
|
|
640
|
+
try:
|
|
641
|
+
# Prepare input with session if enabled
|
|
642
|
+
prepared_input = await AgentRunner._prepare_input_with_session(starting_input, session)
|
|
643
|
+
|
|
644
|
+
# Update the streamed result with the prepared input
|
|
645
|
+
streamed_result.input = prepared_input
|
|
646
|
+
|
|
647
|
+
while True:
|
|
648
|
+
if streamed_result.is_complete:
|
|
649
|
+
break
|
|
650
|
+
|
|
651
|
+
all_tools = await cls._get_all_tools(current_agent, context_wrapper)
|
|
652
|
+
|
|
653
|
+
# Start an agent span if we don't have one
|
|
654
|
+
if current_span is None:
|
|
655
|
+
handoff_names = [
|
|
656
|
+
h.agent_name
|
|
657
|
+
for h in await cls._get_handoffs(current_agent, context_wrapper)
|
|
658
|
+
]
|
|
659
|
+
if output_schema := cls._get_output_schema(current_agent):
|
|
660
|
+
output_type_name = output_schema.name()
|
|
661
|
+
else:
|
|
662
|
+
output_type_name = "str"
|
|
663
|
+
|
|
664
|
+
current_span = agent_span(
|
|
665
|
+
name=current_agent.name,
|
|
666
|
+
handoffs=handoff_names,
|
|
667
|
+
output_type=output_type_name,
|
|
668
|
+
)
|
|
669
|
+
current_span.start(mark_as_current=True)
|
|
670
|
+
tool_names = [t.name for t in all_tools]
|
|
671
|
+
current_span.span_data.tools = tool_names
|
|
672
|
+
current_turn += 1
|
|
673
|
+
streamed_result.current_turn = current_turn
|
|
674
|
+
|
|
675
|
+
if current_turn > max_turns:
|
|
676
|
+
_error_tracing.attach_error_to_span(
|
|
677
|
+
current_span,
|
|
678
|
+
SpanError(
|
|
679
|
+
message="Max turns exceeded",
|
|
680
|
+
data={"max_turns": max_turns},
|
|
681
|
+
),
|
|
682
|
+
)
|
|
683
|
+
streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
|
|
684
|
+
break
|
|
685
|
+
|
|
686
|
+
if current_turn == 1 or isinstance(turn_result.next_step, NextStepHandoff):
|
|
687
|
+
# Run input guardrails in the background and put the results on the queue
|
|
688
|
+
streamed_result._input_guardrails_task = asyncio.create_task(
|
|
689
|
+
cls._run_input_guardrails_with_queue(
|
|
690
|
+
current_agent,
|
|
691
|
+
current_agent.input_guardrails + (run_config.input_guardrails or []),
|
|
692
|
+
ItemHelpers.input_to_new_input_list(prepared_input if current_turn == 1 else streamed_result.input),
|
|
693
|
+
context_wrapper,
|
|
694
|
+
streamed_result,
|
|
695
|
+
current_span,
|
|
696
|
+
)
|
|
697
|
+
)
|
|
698
|
+
try:
|
|
699
|
+
turn_result = await cls._run_single_turn_streamed(
|
|
700
|
+
streamed_result,
|
|
701
|
+
current_agent,
|
|
702
|
+
hooks,
|
|
703
|
+
context_wrapper,
|
|
704
|
+
run_config,
|
|
705
|
+
should_run_agent_start_hooks,
|
|
706
|
+
tool_use_tracker,
|
|
707
|
+
all_tools,
|
|
708
|
+
previous_response_id,
|
|
709
|
+
)
|
|
710
|
+
should_run_agent_start_hooks = False
|
|
711
|
+
|
|
712
|
+
streamed_result.raw_responses = streamed_result.raw_responses + [
|
|
713
|
+
turn_result.model_response
|
|
714
|
+
]
|
|
715
|
+
streamed_result.input = turn_result.original_input
|
|
716
|
+
streamed_result.new_items = turn_result.generated_items
|
|
717
|
+
|
|
718
|
+
if isinstance(turn_result.next_step, NextStepHandoff):
|
|
719
|
+
current_agent = turn_result.next_step.new_agent
|
|
720
|
+
# Update input with sanitized content from input guardrails for handoff
|
|
721
|
+
for result in streamed_result.input_guardrail_results:
|
|
722
|
+
if hasattr(result.output, 'sanitized_content') and result.output.sanitized_content:
|
|
723
|
+
streamed_result.input = result.output.sanitized_content
|
|
724
|
+
current_span.finish(reset_current=True)
|
|
725
|
+
current_span = None
|
|
726
|
+
should_run_agent_start_hooks = True
|
|
727
|
+
streamed_result._event_queue.put_nowait(
|
|
728
|
+
AgentUpdatedStreamEvent(new_agent=current_agent)
|
|
729
|
+
)
|
|
730
|
+
elif isinstance(turn_result.next_step, NextStepFinalOutput):
|
|
731
|
+
streamed_result._output_guardrails_task = asyncio.create_task(
|
|
732
|
+
cls._run_output_guardrails(
|
|
733
|
+
current_agent.output_guardrails
|
|
734
|
+
+ (run_config.output_guardrails or []),
|
|
735
|
+
current_agent,
|
|
736
|
+
turn_result.next_step.output,
|
|
737
|
+
context_wrapper,
|
|
738
|
+
)
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
try:
|
|
742
|
+
output_guardrail_results = await streamed_result._output_guardrails_task
|
|
743
|
+
except Exception:
|
|
744
|
+
output_guardrail_results = []
|
|
745
|
+
|
|
746
|
+
streamed_result.output_guardrail_results = output_guardrail_results
|
|
747
|
+
# Use sanitized content as final output if available
|
|
748
|
+
final_output = turn_result.next_step.output
|
|
749
|
+
for result in output_guardrail_results:
|
|
750
|
+
if hasattr(result.output, 'sanitized_content') and result.output.sanitized_content:
|
|
751
|
+
final_output = result.output.sanitized_content
|
|
752
|
+
streamed_result.final_output = final_output
|
|
753
|
+
streamed_result.is_complete = True
|
|
754
|
+
|
|
755
|
+
# Save the conversation to session if enabled
|
|
756
|
+
temp_result = RunResult(
|
|
757
|
+
input=streamed_result.input,
|
|
758
|
+
new_items=streamed_result.new_items,
|
|
759
|
+
raw_responses=streamed_result.raw_responses,
|
|
760
|
+
final_output=streamed_result.final_output,
|
|
761
|
+
_last_agent=current_agent,
|
|
762
|
+
input_guardrail_results=streamed_result.input_guardrail_results,
|
|
763
|
+
output_guardrail_results=streamed_result.output_guardrail_results,
|
|
764
|
+
context_wrapper=context_wrapper,
|
|
765
|
+
)
|
|
766
|
+
await AgentRunner._save_result_to_session(
|
|
767
|
+
session, starting_input, temp_result
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
|
|
771
|
+
elif isinstance(turn_result.next_step, NextStepRunAgain):
|
|
772
|
+
pass
|
|
773
|
+
except AgentsException as exc:
|
|
774
|
+
streamed_result.is_complete = True
|
|
775
|
+
streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
|
|
776
|
+
exc.run_data = RunErrorDetails(
|
|
777
|
+
input=streamed_result.input,
|
|
778
|
+
new_items=streamed_result.new_items,
|
|
779
|
+
raw_responses=streamed_result.raw_responses,
|
|
780
|
+
last_agent=current_agent,
|
|
781
|
+
context_wrapper=context_wrapper,
|
|
782
|
+
input_guardrail_results=streamed_result.input_guardrail_results,
|
|
783
|
+
output_guardrail_results=streamed_result.output_guardrail_results,
|
|
784
|
+
)
|
|
785
|
+
raise
|
|
786
|
+
except Exception as e:
|
|
787
|
+
if current_span:
|
|
788
|
+
_error_tracing.attach_error_to_span(
|
|
789
|
+
current_span,
|
|
790
|
+
SpanError(
|
|
791
|
+
message="Error in agent run",
|
|
792
|
+
data={"error": str(e)},
|
|
793
|
+
),
|
|
794
|
+
)
|
|
795
|
+
streamed_result.is_complete = True
|
|
796
|
+
streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
|
|
797
|
+
raise
|
|
798
|
+
|
|
799
|
+
streamed_result.is_complete = True
|
|
800
|
+
finally:
|
|
801
|
+
if current_span:
|
|
802
|
+
current_span.finish(reset_current=True)
|
|
803
|
+
if streamed_result.trace:
|
|
804
|
+
streamed_result.trace.finish(reset_current=True)
|
|
805
|
+
|
|
806
|
+
@classmethod
|
|
807
|
+
async def _run_single_turn_streamed(
|
|
808
|
+
cls,
|
|
809
|
+
streamed_result: RunResultStreaming,
|
|
810
|
+
agent: Agent[TContext],
|
|
811
|
+
hooks: RunHooks[TContext],
|
|
812
|
+
context_wrapper: RunContextWrapper[TContext],
|
|
813
|
+
run_config: RunConfig,
|
|
814
|
+
should_run_agent_start_hooks: bool,
|
|
815
|
+
tool_use_tracker: AgentToolUseTracker,
|
|
816
|
+
all_tools: list[Tool],
|
|
817
|
+
previous_response_id: str | None,
|
|
818
|
+
) -> SingleStepResult:
|
|
819
|
+
"""Execute a single streamed agent turn including tools and guardrails."""
|
|
820
|
+
if should_run_agent_start_hooks:
|
|
821
|
+
await asyncio.gather(
|
|
822
|
+
hooks.on_agent_start(context_wrapper, agent),
|
|
823
|
+
(
|
|
824
|
+
agent.hooks.on_start(context_wrapper, agent)
|
|
825
|
+
if agent.hooks
|
|
826
|
+
else _coro.noop_coroutine()
|
|
827
|
+
),
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
output_schema = cls._get_output_schema(agent)
|
|
831
|
+
|
|
832
|
+
streamed_result.current_agent = agent
|
|
833
|
+
streamed_result._current_agent_output_schema = output_schema
|
|
834
|
+
|
|
835
|
+
system_prompt, prompt_config = await asyncio.gather(
|
|
836
|
+
agent.get_system_prompt(context_wrapper),
|
|
837
|
+
agent.get_prompt(context_wrapper),
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
handoffs = await cls._get_handoffs(agent, context_wrapper)
|
|
841
|
+
model = cls._get_model(agent, run_config)
|
|
842
|
+
model_settings = agent.model_settings.resolve(run_config.model_settings)
|
|
843
|
+
model_settings = RunImpl.maybe_reset_tool_choice(agent, tool_use_tracker, model_settings)
|
|
844
|
+
|
|
845
|
+
final_response: ModelResponse | None = None
|
|
846
|
+
|
|
847
|
+
input = ItemHelpers.input_to_new_input_list(streamed_result.input)
|
|
848
|
+
input.extend([item.to_input_item() for item in streamed_result.new_items])
|
|
849
|
+
|
|
850
|
+
filtered = await cls._maybe_filter_model_input(
|
|
851
|
+
agent=agent,
|
|
852
|
+
run_config=run_config,
|
|
853
|
+
context_wrapper=context_wrapper,
|
|
854
|
+
input_items=input,
|
|
855
|
+
system_instructions=system_prompt,
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
# Call hook just before the model is invoked, with the correct system_prompt.
|
|
859
|
+
if agent.hooks:
|
|
860
|
+
await agent.hooks.on_llm_start(
|
|
861
|
+
context_wrapper, agent, filtered.instructions, filtered.input
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
# 1. Stream the output events
|
|
865
|
+
async for event in model.stream_response(
|
|
866
|
+
filtered.instructions,
|
|
867
|
+
filtered.input,
|
|
868
|
+
model_settings,
|
|
869
|
+
all_tools,
|
|
870
|
+
output_schema,
|
|
871
|
+
handoffs,
|
|
872
|
+
get_model_tracing_impl(
|
|
873
|
+
run_config.tracing_disabled, run_config.trace_include_sensitive_data
|
|
874
|
+
),
|
|
875
|
+
previous_response_id=previous_response_id,
|
|
876
|
+
prompt=prompt_config,
|
|
877
|
+
):
|
|
878
|
+
if isinstance(event, ResponseCompletedEvent):
|
|
879
|
+
usage = (
|
|
880
|
+
Usage(
|
|
881
|
+
requests=1,
|
|
882
|
+
input_tokens=event.response.usage.input_tokens,
|
|
883
|
+
output_tokens=event.response.usage.output_tokens,
|
|
884
|
+
total_tokens=event.response.usage.total_tokens,
|
|
885
|
+
input_tokens_details=event.response.usage.input_tokens_details,
|
|
886
|
+
output_tokens_details=event.response.usage.output_tokens_details,
|
|
887
|
+
)
|
|
888
|
+
if event.response.usage
|
|
889
|
+
else Usage()
|
|
890
|
+
)
|
|
891
|
+
final_response = ModelResponse(
|
|
892
|
+
output=event.response.output,
|
|
893
|
+
usage=usage,
|
|
894
|
+
response_id=event.response.id,
|
|
895
|
+
)
|
|
896
|
+
context_wrapper.usage.add(usage)
|
|
897
|
+
|
|
898
|
+
streamed_result._event_queue.put_nowait(RawResponsesStreamEvent(data=event))
|
|
899
|
+
|
|
900
|
+
# Call hook just after the model response is finalized.
|
|
901
|
+
if agent.hooks and final_response is not None:
|
|
902
|
+
await agent.hooks.on_llm_end(context_wrapper, agent, final_response)
|
|
903
|
+
|
|
904
|
+
# 2. At this point, the streaming is complete for this turn of the agent loop.
|
|
905
|
+
if not final_response:
|
|
906
|
+
raise ModelBehaviorError("Model did not produce a final response!")
|
|
907
|
+
|
|
908
|
+
# 3. Now, we can process the turn as we do in the non-streaming case
|
|
909
|
+
return await cls._get_single_step_result_from_streamed_response(
|
|
910
|
+
agent=agent,
|
|
911
|
+
streamed_result=streamed_result,
|
|
912
|
+
new_response=final_response,
|
|
913
|
+
output_schema=output_schema,
|
|
914
|
+
all_tools=all_tools,
|
|
915
|
+
handoffs=handoffs,
|
|
916
|
+
hooks=hooks,
|
|
917
|
+
context_wrapper=context_wrapper,
|
|
918
|
+
run_config=run_config,
|
|
919
|
+
tool_use_tracker=tool_use_tracker,
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
@classmethod
|
|
923
|
+
async def _run_single_turn(
|
|
924
|
+
cls,
|
|
925
|
+
*,
|
|
926
|
+
agent: Agent[TContext],
|
|
927
|
+
all_tools: list[Tool],
|
|
928
|
+
original_input: str | list[TResponseInputItem],
|
|
929
|
+
generated_items: list[RunItem],
|
|
930
|
+
hooks: RunHooks[TContext],
|
|
931
|
+
context_wrapper: RunContextWrapper[TContext],
|
|
932
|
+
run_config: RunConfig,
|
|
933
|
+
should_run_agent_start_hooks: bool,
|
|
934
|
+
tool_use_tracker: AgentToolUseTracker,
|
|
935
|
+
previous_response_id: str | None,
|
|
936
|
+
) -> SingleStepResult:
|
|
937
|
+
"""Run one non-streaming agent turn including guardrails and tools."""
|
|
938
|
+
# Ensure we run the hooks before anything else
|
|
939
|
+
if should_run_agent_start_hooks:
|
|
940
|
+
await asyncio.gather(
|
|
941
|
+
hooks.on_agent_start(context_wrapper, agent),
|
|
942
|
+
(
|
|
943
|
+
agent.hooks.on_start(context_wrapper, agent)
|
|
944
|
+
if agent.hooks
|
|
945
|
+
else _coro.noop_coroutine()
|
|
946
|
+
),
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
system_prompt, prompt_config = await asyncio.gather(
|
|
950
|
+
agent.get_system_prompt(context_wrapper),
|
|
951
|
+
agent.get_prompt(context_wrapper),
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
output_schema = cls._get_output_schema(agent)
|
|
955
|
+
handoffs = await cls._get_handoffs(agent, context_wrapper)
|
|
956
|
+
input = ItemHelpers.input_to_new_input_list(original_input)
|
|
957
|
+
input.extend([generated_item.to_input_item() for generated_item in generated_items])
|
|
958
|
+
|
|
959
|
+
new_response = await cls._get_new_response(
|
|
960
|
+
agent,
|
|
961
|
+
system_prompt,
|
|
962
|
+
input,
|
|
963
|
+
output_schema,
|
|
964
|
+
all_tools,
|
|
965
|
+
handoffs,
|
|
966
|
+
context_wrapper,
|
|
967
|
+
run_config,
|
|
968
|
+
tool_use_tracker,
|
|
969
|
+
previous_response_id,
|
|
970
|
+
prompt_config,
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
return await cls._get_single_step_result_from_response(
|
|
974
|
+
agent=agent,
|
|
975
|
+
original_input=original_input,
|
|
976
|
+
pre_step_items=generated_items,
|
|
977
|
+
new_response=new_response,
|
|
978
|
+
output_schema=output_schema,
|
|
979
|
+
all_tools=all_tools,
|
|
980
|
+
handoffs=handoffs,
|
|
981
|
+
hooks=hooks,
|
|
982
|
+
context_wrapper=context_wrapper,
|
|
983
|
+
run_config=run_config,
|
|
984
|
+
tool_use_tracker=tool_use_tracker,
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
@classmethod
|
|
988
|
+
async def _get_single_step_result_from_response(
|
|
989
|
+
cls,
|
|
990
|
+
*,
|
|
991
|
+
agent: Agent[TContext],
|
|
992
|
+
all_tools: list[Tool],
|
|
993
|
+
original_input: str | list[TResponseInputItem],
|
|
994
|
+
pre_step_items: list[RunItem],
|
|
995
|
+
new_response: ModelResponse,
|
|
996
|
+
output_schema: AgentOutputSchemaBase | None,
|
|
997
|
+
handoffs: list[Handoff],
|
|
998
|
+
hooks: RunHooks[TContext],
|
|
999
|
+
context_wrapper: RunContextWrapper[TContext],
|
|
1000
|
+
run_config: RunConfig,
|
|
1001
|
+
tool_use_tracker: AgentToolUseTracker,
|
|
1002
|
+
) -> SingleStepResult:
|
|
1003
|
+
"""Process a model response and execute any resulting tool calls."""
|
|
1004
|
+
processed_response = RunImpl.process_model_response(
|
|
1005
|
+
agent=agent,
|
|
1006
|
+
all_tools=all_tools,
|
|
1007
|
+
response=new_response,
|
|
1008
|
+
output_schema=output_schema,
|
|
1009
|
+
handoffs=handoffs,
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
tool_use_tracker.add_tool_use(agent, processed_response.tools_used)
|
|
1013
|
+
|
|
1014
|
+
return await RunImpl.execute_tools_and_side_effects(
|
|
1015
|
+
agent=agent,
|
|
1016
|
+
original_input=original_input,
|
|
1017
|
+
pre_step_items=pre_step_items,
|
|
1018
|
+
new_response=new_response,
|
|
1019
|
+
processed_response=processed_response,
|
|
1020
|
+
output_schema=output_schema,
|
|
1021
|
+
hooks=hooks,
|
|
1022
|
+
context_wrapper=context_wrapper,
|
|
1023
|
+
run_config=run_config,
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
@classmethod
|
|
1027
|
+
async def _get_single_step_result_from_streamed_response(
|
|
1028
|
+
cls,
|
|
1029
|
+
*,
|
|
1030
|
+
agent: Agent[TContext],
|
|
1031
|
+
all_tools: list[Tool],
|
|
1032
|
+
streamed_result: RunResultStreaming,
|
|
1033
|
+
new_response: ModelResponse,
|
|
1034
|
+
output_schema: AgentOutputSchemaBase | None,
|
|
1035
|
+
handoffs: list[Handoff],
|
|
1036
|
+
hooks: RunHooks[TContext],
|
|
1037
|
+
context_wrapper: RunContextWrapper[TContext],
|
|
1038
|
+
run_config: RunConfig,
|
|
1039
|
+
tool_use_tracker: AgentToolUseTracker,
|
|
1040
|
+
) -> SingleStepResult:
|
|
1041
|
+
"""Process a streamed model response and enqueue resulting events."""
|
|
1042
|
+
original_input = streamed_result.input
|
|
1043
|
+
pre_step_items = streamed_result.new_items
|
|
1044
|
+
event_queue = streamed_result._event_queue
|
|
1045
|
+
|
|
1046
|
+
processed_response = RunImpl.process_model_response(
|
|
1047
|
+
agent=agent,
|
|
1048
|
+
all_tools=all_tools,
|
|
1049
|
+
response=new_response,
|
|
1050
|
+
output_schema=output_schema,
|
|
1051
|
+
handoffs=handoffs,
|
|
1052
|
+
)
|
|
1053
|
+
new_items_processed_response = processed_response.new_items
|
|
1054
|
+
tool_use_tracker.add_tool_use(agent, processed_response.tools_used)
|
|
1055
|
+
RunImpl.stream_step_items_to_queue(new_items_processed_response, event_queue)
|
|
1056
|
+
|
|
1057
|
+
single_step_result = await RunImpl.execute_tools_and_side_effects(
|
|
1058
|
+
agent=agent,
|
|
1059
|
+
original_input=original_input,
|
|
1060
|
+
pre_step_items=pre_step_items,
|
|
1061
|
+
new_response=new_response,
|
|
1062
|
+
processed_response=processed_response,
|
|
1063
|
+
output_schema=output_schema,
|
|
1064
|
+
hooks=hooks,
|
|
1065
|
+
context_wrapper=context_wrapper,
|
|
1066
|
+
run_config=run_config,
|
|
1067
|
+
)
|
|
1068
|
+
new_step_items = [
|
|
1069
|
+
item
|
|
1070
|
+
for item in single_step_result.new_step_items
|
|
1071
|
+
if item not in new_items_processed_response
|
|
1072
|
+
]
|
|
1073
|
+
RunImpl.stream_step_items_to_queue(new_step_items, event_queue)
|
|
1074
|
+
|
|
1075
|
+
return single_step_result
|
|
1076
|
+
|
|
1077
|
+
@classmethod
|
|
1078
|
+
async def _run_input_guardrails(
|
|
1079
|
+
cls,
|
|
1080
|
+
agent: Agent[Any],
|
|
1081
|
+
guardrails: list[InputGuardrail[TContext]],
|
|
1082
|
+
input: str | list[TResponseInputItem],
|
|
1083
|
+
context: RunContextWrapper[TContext],
|
|
1084
|
+
) -> list[InputGuardrailResult]:
|
|
1085
|
+
"""Run configured input guardrails for a given agent."""
|
|
1086
|
+
if not guardrails:
|
|
1087
|
+
return []
|
|
1088
|
+
|
|
1089
|
+
guardrail_tasks = [
|
|
1090
|
+
asyncio.create_task(
|
|
1091
|
+
RunImpl.run_single_input_guardrail(agent, guardrail, input, context)
|
|
1092
|
+
)
|
|
1093
|
+
for guardrail in guardrails
|
|
1094
|
+
]
|
|
1095
|
+
|
|
1096
|
+
guardrail_results = []
|
|
1097
|
+
|
|
1098
|
+
for done in asyncio.as_completed(guardrail_tasks):
|
|
1099
|
+
result = await done
|
|
1100
|
+
if result.output.tripwire_triggered:
|
|
1101
|
+
# Cancel all guardrail tasks if a tripwire is triggered.
|
|
1102
|
+
for t in guardrail_tasks:
|
|
1103
|
+
t.cancel()
|
|
1104
|
+
_error_tracing.attach_error_to_current_span(
|
|
1105
|
+
SpanError(
|
|
1106
|
+
message="Guardrail tripwire triggered",
|
|
1107
|
+
data={"guardrail": result.guardrail.get_name()},
|
|
1108
|
+
)
|
|
1109
|
+
)
|
|
1110
|
+
raise InputGuardrailTripwireTriggered(result)
|
|
1111
|
+
else:
|
|
1112
|
+
guardrail_results.append(result)
|
|
1113
|
+
|
|
1114
|
+
return guardrail_results
|
|
1115
|
+
|
|
1116
|
+
@classmethod
|
|
1117
|
+
async def _run_output_guardrails(
|
|
1118
|
+
cls,
|
|
1119
|
+
guardrails: list[OutputGuardrail[TContext]],
|
|
1120
|
+
agent: Agent[TContext],
|
|
1121
|
+
agent_output: Any,
|
|
1122
|
+
context: RunContextWrapper[TContext],
|
|
1123
|
+
) -> list[OutputGuardrailResult]:
|
|
1124
|
+
"""Run configured output guardrails for a given agent output."""
|
|
1125
|
+
if not guardrails:
|
|
1126
|
+
return []
|
|
1127
|
+
|
|
1128
|
+
guardrail_tasks = [
|
|
1129
|
+
asyncio.create_task(
|
|
1130
|
+
RunImpl.run_single_output_guardrail(guardrail, agent, agent_output, context)
|
|
1131
|
+
)
|
|
1132
|
+
for guardrail in guardrails
|
|
1133
|
+
]
|
|
1134
|
+
|
|
1135
|
+
guardrail_results = []
|
|
1136
|
+
|
|
1137
|
+
for done in asyncio.as_completed(guardrail_tasks):
|
|
1138
|
+
result = await done
|
|
1139
|
+
if result.output.tripwire_triggered:
|
|
1140
|
+
# Cancel all guardrail tasks if a tripwire is triggered.
|
|
1141
|
+
for t in guardrail_tasks:
|
|
1142
|
+
t.cancel()
|
|
1143
|
+
_error_tracing.attach_error_to_current_span(
|
|
1144
|
+
SpanError(
|
|
1145
|
+
message="Guardrail tripwire triggered",
|
|
1146
|
+
data={"guardrail": result.guardrail.get_name()},
|
|
1147
|
+
)
|
|
1148
|
+
)
|
|
1149
|
+
raise OutputGuardrailTripwireTriggered(result)
|
|
1150
|
+
else:
|
|
1151
|
+
guardrail_results.append(result)
|
|
1152
|
+
|
|
1153
|
+
return guardrail_results
|
|
1154
|
+
|
|
1155
|
+
@classmethod
|
|
1156
|
+
async def _get_new_response(
|
|
1157
|
+
cls,
|
|
1158
|
+
agent: Agent[TContext],
|
|
1159
|
+
system_prompt: str | None,
|
|
1160
|
+
input: list[TResponseInputItem],
|
|
1161
|
+
output_schema: AgentOutputSchemaBase | None,
|
|
1162
|
+
all_tools: list[Tool],
|
|
1163
|
+
handoffs: list[Handoff],
|
|
1164
|
+
context_wrapper: RunContextWrapper[TContext],
|
|
1165
|
+
run_config: RunConfig,
|
|
1166
|
+
tool_use_tracker: AgentToolUseTracker,
|
|
1167
|
+
previous_response_id: str | None,
|
|
1168
|
+
prompt_config: ResponsePromptParam | None,
|
|
1169
|
+
) -> ModelResponse:
|
|
1170
|
+
"""Call the model provider to obtain a new response for this turn."""
|
|
1171
|
+
# Allow user to modify model input right before the call, if configured
|
|
1172
|
+
filtered = await cls._maybe_filter_model_input(
|
|
1173
|
+
agent=agent,
|
|
1174
|
+
run_config=run_config,
|
|
1175
|
+
context_wrapper=context_wrapper,
|
|
1176
|
+
input_items=input,
|
|
1177
|
+
system_instructions=system_prompt,
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
model = cls._get_model(agent, run_config)
|
|
1181
|
+
model_settings = agent.model_settings.resolve(run_config.model_settings)
|
|
1182
|
+
model_settings = RunImpl.maybe_reset_tool_choice(agent, tool_use_tracker, model_settings)
|
|
1183
|
+
# If the agent has hooks, we need to call them before and after the LLM call
|
|
1184
|
+
if agent.hooks:
|
|
1185
|
+
await agent.hooks.on_llm_start(
|
|
1186
|
+
context_wrapper,
|
|
1187
|
+
agent,
|
|
1188
|
+
filtered.instructions, # Use filtered instructions
|
|
1189
|
+
filtered.input, # Use filtered input
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
new_response = await model.get_response(
|
|
1193
|
+
system_instructions=filtered.instructions,
|
|
1194
|
+
input=filtered.input,
|
|
1195
|
+
model_settings=model_settings,
|
|
1196
|
+
tools=all_tools,
|
|
1197
|
+
output_schema=output_schema,
|
|
1198
|
+
handoffs=handoffs,
|
|
1199
|
+
tracing=get_model_tracing_impl(
|
|
1200
|
+
run_config.tracing_disabled, run_config.trace_include_sensitive_data
|
|
1201
|
+
),
|
|
1202
|
+
previous_response_id=previous_response_id,
|
|
1203
|
+
prompt=prompt_config,
|
|
1204
|
+
)
|
|
1205
|
+
# If the agent has hooks, we need to call them after the LLM call
|
|
1206
|
+
if agent.hooks:
|
|
1207
|
+
await agent.hooks.on_llm_end(context_wrapper, agent, new_response)
|
|
1208
|
+
|
|
1209
|
+
context_wrapper.usage.add(new_response.usage)
|
|
1210
|
+
|
|
1211
|
+
return new_response
|
|
1212
|
+
|
|
1213
|
+
@classmethod
|
|
1214
|
+
def _get_output_schema(cls, agent: Agent[Any]) -> AgentOutputSchemaBase | None:
|
|
1215
|
+
"""Resolve the output schema for the provided agent."""
|
|
1216
|
+
if agent.output_type is None or agent.output_type is str:
|
|
1217
|
+
return None
|
|
1218
|
+
elif isinstance(agent.output_type, AgentOutputSchemaBase):
|
|
1219
|
+
return agent.output_type
|
|
1220
|
+
|
|
1221
|
+
return AgentOutputSchema(agent.output_type)
|
|
1222
|
+
|
|
1223
|
+
@classmethod
|
|
1224
|
+
async def _get_handoffs(
|
|
1225
|
+
cls, agent: Agent[Any], context_wrapper: RunContextWrapper[Any]
|
|
1226
|
+
) -> list[Handoff]:
|
|
1227
|
+
"""Collect enabled handoffs for the given agent."""
|
|
1228
|
+
handoffs = []
|
|
1229
|
+
for handoff_item in agent.handoffs:
|
|
1230
|
+
if isinstance(handoff_item, Handoff):
|
|
1231
|
+
handoffs.append(handoff_item)
|
|
1232
|
+
elif isinstance(handoff_item, Agent):
|
|
1233
|
+
handoffs.append(handoff(handoff_item))
|
|
1234
|
+
|
|
1235
|
+
async def _check_handoff_enabled(handoff_obj: Handoff) -> bool:
|
|
1236
|
+
"""Determine whether a handoff should run for the current context."""
|
|
1237
|
+
attr = handoff_obj.is_enabled
|
|
1238
|
+
if isinstance(attr, bool):
|
|
1239
|
+
return attr
|
|
1240
|
+
res = attr(context_wrapper, agent)
|
|
1241
|
+
if inspect.isawaitable(res):
|
|
1242
|
+
return bool(await res)
|
|
1243
|
+
return bool(res)
|
|
1244
|
+
|
|
1245
|
+
results = await asyncio.gather(*(_check_handoff_enabled(h) for h in handoffs))
|
|
1246
|
+
enabled: list[Handoff] = [h for h, ok in zip(handoffs, results) if ok]
|
|
1247
|
+
return enabled
|
|
1248
|
+
|
|
1249
|
+
@classmethod
|
|
1250
|
+
async def _get_all_tools(
|
|
1251
|
+
cls, agent: Agent[Any], context_wrapper: RunContextWrapper[Any]
|
|
1252
|
+
) -> list[Tool]:
|
|
1253
|
+
"""Gather all tools available to the agent from hooks and config."""
|
|
1254
|
+
return await agent.get_all_tools(context_wrapper)
|
|
1255
|
+
|
|
1256
|
+
@classmethod
|
|
1257
|
+
def _get_model(cls, agent: Agent[Any], run_config: RunConfig) -> Model:
|
|
1258
|
+
"""Resolve which model to call for this agent run."""
|
|
1259
|
+
if isinstance(run_config.model, Model):
|
|
1260
|
+
return run_config.model
|
|
1261
|
+
elif isinstance(run_config.model, str):
|
|
1262
|
+
return run_config.model_provider.get_model(run_config.model)
|
|
1263
|
+
elif isinstance(agent.model, Model):
|
|
1264
|
+
return agent.model
|
|
1265
|
+
|
|
1266
|
+
return run_config.model_provider.get_model(agent.model)
|
|
1267
|
+
|
|
1268
|
+
@classmethod
|
|
1269
|
+
async def _prepare_input_with_session(
|
|
1270
|
+
cls,
|
|
1271
|
+
input: str | list[TResponseInputItem],
|
|
1272
|
+
session: Session | None,
|
|
1273
|
+
) -> str | list[TResponseInputItem]:
|
|
1274
|
+
"""Prepare input by combining it with session history if enabled."""
|
|
1275
|
+
if session is None:
|
|
1276
|
+
return input
|
|
1277
|
+
|
|
1278
|
+
# Validate that we don't have both a session and a list input, as this creates
|
|
1279
|
+
# ambiguity about whether the list should append to or replace existing session history
|
|
1280
|
+
if isinstance(input, list):
|
|
1281
|
+
raise UserError(
|
|
1282
|
+
"Cannot provide both a session and a list of input items. "
|
|
1283
|
+
"When using session memory, provide only a string input to append to the "
|
|
1284
|
+
"conversation, or use session=None and provide a list to manually manage "
|
|
1285
|
+
"conversation history."
|
|
1286
|
+
)
|
|
1287
|
+
|
|
1288
|
+
# Get previous conversation history
|
|
1289
|
+
history = await session.get_items()
|
|
1290
|
+
|
|
1291
|
+
# Convert input to list format
|
|
1292
|
+
new_input_list = ItemHelpers.input_to_new_input_list(input)
|
|
1293
|
+
|
|
1294
|
+
# Combine history with new input
|
|
1295
|
+
combined_input = history + new_input_list
|
|
1296
|
+
|
|
1297
|
+
return combined_input
|
|
1298
|
+
|
|
1299
|
+
@classmethod
|
|
1300
|
+
async def _save_result_to_session(
|
|
1301
|
+
cls,
|
|
1302
|
+
session: Session | None,
|
|
1303
|
+
original_input: str | list[TResponseInputItem],
|
|
1304
|
+
result: RunResult,
|
|
1305
|
+
) -> None:
|
|
1306
|
+
"""Save the conversation turn to session."""
|
|
1307
|
+
if session is None:
|
|
1308
|
+
return
|
|
1309
|
+
|
|
1310
|
+
# Convert original input to list format if needed
|
|
1311
|
+
input_list = ItemHelpers.input_to_new_input_list(original_input)
|
|
1312
|
+
|
|
1313
|
+
# Convert new items to input format
|
|
1314
|
+
new_items_as_input = [item.to_input_item() for item in result.new_items]
|
|
1315
|
+
|
|
1316
|
+
# Save all items from this turn
|
|
1317
|
+
items_to_save = input_list + new_items_as_input
|
|
1318
|
+
await session.add_items(items_to_save)
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
DEFAULT_AGENT_RUNNER = AgentRunner()
|
|
1322
|
+
|
|
1323
|
+
|
|
1324
|
+
def _copy_str_or_list(input: str | list[TResponseInputItem]) -> str | list[TResponseInputItem]:
|
|
1325
|
+
"""Return a shallow copy of list inputs while leaving strings untouched."""
|
|
1326
|
+
if isinstance(input, str):
|
|
1327
|
+
return input
|
|
1328
|
+
return input.copy()
|