@@ -220,6 +220,181 @@ Once the run finishes, `agent_run.final_result` becomes a [`AgentRunResult`][pyd
220
220
221
221
---
222
222
223
+ ### Streaming
224
+
225
+ Here is an example of streaming in combination with ` async for ` :
226
+
227
+ ``` python {title="streaming.py"}
228
+ import asyncio
229
+ from dataclasses import dataclass
230
+ from datetime import date
231
+
232
+ from pydantic_ai import (
233
+ Agent,
234
+ capture_run_messages,
235
+ )
236
+ from pydantic_ai.agent import is_handle_response_node, is_model_request_node
237
+ from pydantic_ai.messages import (
238
+ PartStartEvent,
239
+ PartDeltaEvent,
240
+ FunctionToolCallEvent,
241
+ FunctionToolResultEvent,
242
+ FinalResultEvent, TextPartDelta, ToolCallPartDelta,
243
+ )
244
+ from pydantic_ai.tools import RunContext
245
+ from pydantic_graph import End
246
+
247
+
248
+ @dataclass
249
+ class WeatherService :
250
+ async def get_forecast (self , location : str , forecast_date : date) -> str :
251
+ # In real code: call weather API, DB queries, etc.
252
+ return f " The forecast in { location} on { forecast_date} is 24°C and sunny. "
253
+
254
+ async def get_historic_weather (self , location : str , forecast_date : date) -> str :
255
+ # In real code: call a historical weather API or DB
256
+ return f " The weather in { location} on { forecast_date} was 18°C and partly cloudy. "
257
+
258
+
259
+ weather_agent = Agent[WeatherService, str ](
260
+ " openai:gpt-4o" ,
261
+ deps_type = WeatherService,
262
+ result_type = str , # We'll produce a final answer as plain text
263
+ system_prompt = " Providing a weather forecast at the locations the user provides." ,
264
+ )
265
+
266
+
267
+ @weather_agent.tool
268
+ async def weather_forecast (
269
+ ctx : RunContext[WeatherService],
270
+ location : str ,
271
+ forecast_date : date,
272
+ ) -> str :
273
+ if forecast_date >= date.today():
274
+ return await ctx.deps.get_forecast(location, forecast_date)
275
+ else :
276
+ return await ctx.deps.get_historic_weather(location, forecast_date)
277
+
278
+
279
+ async def main ():
280
+ # The user asks for tomorrow's weather in Paris
281
+ user_prompt = " What will the weather be like in Paris tomorrow?"
282
+
283
+ # We'll capture raw messages for debugging
284
+ with capture_run_messages() as messages:
285
+ # Provide a WeatherService instance as the agent's dependencies
286
+ deps = WeatherService()
287
+
288
+ # Begin a node-by-node, streaming iteration
289
+ with weather_agent.iter(user_prompt, deps = deps) as run:
290
+ node = run.next_node # The first node to run
291
+ while not isinstance (node, End):
292
+ if is_model_request_node(node):
293
+ # A model request node => We can stream tokens from the model's request
294
+ print (" === ModelRequestNode: streaming partial request tokens ===" )
295
+ async with node.stream(run.ctx) as request_stream:
296
+ async for event in request_stream:
297
+ if isinstance (event, PartStartEvent):
298
+ print (f " [Request] Starting part { event.index} : { event.part!r } " )
299
+ elif isinstance (event, PartDeltaEvent):
300
+ if isinstance (event.delta, TextPartDelta):
301
+ print (f " [Request] Part { event.index} text delta: { event.delta.content_delta!r } " )
302
+ elif isinstance (event.delta, ToolCallPartDelta):
303
+ print (f " [Request] Part { event.index} args_delta= { event.delta.args_delta} " )
304
+ elif isinstance (event, FinalResultEvent):
305
+ print (f " [Result] The model produced a final result (tool_name= { event.tool_name} ) " )
306
+
307
+ elif is_handle_response_node(node):
308
+ # A handle-response node => The model returned some data, potentially calls a tool
309
+ print (" === HandleResponseNode: streaming partial response & tool usage ===" )
310
+ async with node.stream(run.ctx) as handle_stream:
311
+ async for event in handle_stream:
312
+ if isinstance (event, FunctionToolCallEvent):
313
+ print (f " [Tools] The LLM calls tool= { event.part.tool_name!r } with args= { event.part.args} (tool_call_id= { event.part.tool_call_id!r } ) " )
314
+ elif isinstance (event, FunctionToolResultEvent):
315
+ print (f " [Tools] Tool call { event.tool_call_id!r } returned => { event.result.content} " )
316
+
317
+ node = await run.next(node)
318
+
319
+ # Once an End node is reached, the agent run is complete
320
+ assert run.result is not None
321
+ print (" \n === Final Agent Output ===" )
322
+ print (" Forecast:" , run.result.data)
323
+
324
+ # Show the raw messages exchanged
325
+ print (" \n === Raw Messages Captured ===" )
326
+ for m in messages:
327
+ print (" -" , m)
328
+
329
+
330
+ if __name__ == " __main__" :
331
+ asyncio.run(main())
332
+
333
+ """
334
+ === ModelRequestNode: streaming partial request tokens ===
335
+ [Request] Starting part 0: ToolCallPart(tool_name='weather_forecast', args='', tool_call_id='call_Q0QqiZfIhHyNViiLG7jT0G9R', part_kind='tool-call')
336
+ [Request] Part 0 args_delta={"
337
+ [Request] Part 0 args_delta=location
338
+ [Request] Part 0 args_delta=":"
339
+ [Request] Part 0 args_delta=Paris
340
+ [Request] Part 0 args_delta=","
341
+ [Request] Part 0 args_delta=forecast
342
+ [Request] Part 0 args_delta=_date
343
+ [Request] Part 0 args_delta=":"
344
+ [Request] Part 0 args_delta=202
345
+ [Request] Part 0 args_delta=3
346
+ [Request] Part 0 args_delta=-
347
+ [Request] Part 0 args_delta=11
348
+ [Request] Part 0 args_delta=-
349
+ [Request] Part 0 args_delta=02
350
+ [Request] Part 0 args_delta="}
351
+ === HandleResponseNode: streaming partial response & tool usage ===
352
+ [Tools] The LLM calls tool='weather_forecast' with args={"location":"Paris","forecast_date":"2023-11-02"} (tool_call_id='call_Q0QqiZfIhHyNViiLG7jT0G9R')
353
+ [Tools] Tool call 'call_Q0QqiZfIhHyNViiLG7jT0G9R' returned => The weather in Paris on 2023-11-02 was 18°C and partly cloudy.
354
+ === ModelRequestNode: streaming partial request tokens ===
355
+ [Request] Starting part 0: TextPart(content='', part_kind='text')
356
+ [Result] The model produced a final result (tool_name=None)
357
+ [Request] Part 0 text delta: 'The'
358
+ [Request] Part 0 text delta: ' weather'
359
+ [Request] Part 0 text delta: ' forecast'
360
+ [Request] Part 0 text delta: ' for'
361
+ [Request] Part 0 text delta: ' Paris'
362
+ [Request] Part 0 text delta: ' tomorrow'
363
+ [Request] Part 0 text delta: ','
364
+ [Request] Part 0 text delta: ' November'
365
+ [Request] Part 0 text delta: ' '
366
+ [Request] Part 0 text delta: '2'
367
+ [Request] Part 0 text delta: ','
368
+ [Request] Part 0 text delta: ' '
369
+ [Request] Part 0 text delta: '202'
370
+ [Request] Part 0 text delta: '3'
371
+ [Request] Part 0 text delta: ','
372
+ [Request] Part 0 text delta: ' is'
373
+ [Request] Part 0 text delta: ' expected'
374
+ [Request] Part 0 text delta: ' to'
375
+ [Request] Part 0 text delta: ' be'
376
+ [Request] Part 0 text delta: ' '
377
+ [Request] Part 0 text delta: '18'
378
+ [Request] Part 0 text delta: '°C'
379
+ [Request] Part 0 text delta: ' and'
380
+ [Request] Part 0 text delta: ' partly'
381
+ [Request] Part 0 text delta: ' cloudy'
382
+ [Request] Part 0 text delta: '.'
383
+ === HandleResponseNode: streaming partial response & tool usage ===
384
+
385
+ === Final Agent Output ===
386
+ Forecast: The weather forecast for Paris tomorrow, November 2, 2023, is expected to be 18°C and partly cloudy.
387
+
388
+ === Raw Messages Captured ===
389
+ - ModelRequest(parts=[SystemPromptPart(content='Providing a weather forecast at the locations the user provides.', dynamic_ref=None, part_kind='system-prompt'), UserPromptPart(content='What will the weather be like in Paris tomorrow?', timestamp=datetime.datetime(2025, 2, 25, 7, 16, 4, 867863, tzinfo=datetime.timezone.utc), part_kind='user-prompt')], kind='request')
390
+ - ModelResponse(parts=[ToolCallPart(tool_name='weather_forecast', args='{"location":"Paris","forecast_date":"2023-11-02"}', tool_call_id='call_Q0QqiZfIhHyNViiLG7jT0G9R', part_kind='tool-call')], model_name='gpt-4o', timestamp=datetime.datetime(2025, 2, 25, 7, 16, 8, tzinfo=datetime.timezone.utc), kind='response')
391
+ - ModelRequest(parts=[ToolReturnPart(tool_name='weather_forecast', content='The weather in Paris on 2023-11-02 was 18°C and partly cloudy.', tool_call_id='call_Q0QqiZfIhHyNViiLG7jT0G9R', timestamp=datetime.datetime(2025, 2, 25, 7, 16, 9, 150432, tzinfo=datetime.timezone.utc), part_kind='tool-return')], kind='request')
392
+ - ModelResponse(parts=[TextPart(content='The weather forecast for Paris tomorrow, November 2, 2023, is expected to be 18°C and partly cloudy.', part_kind='text')], model_name='gpt-4o', timestamp=datetime.datetime(2025, 2, 25, 7, 16, 9, tzinfo=datetime.timezone.utc), kind='response')
393
+ """
394
+ ```
395
+
396
+ ---
397
+
223
398
### Additional Configuration
224
399
225
400
#### Usage Limits
0 commit comments