Skip to content

Commit ebb3a2b

Browse files
authored
Merge branch 'main' into feature/old_doc_document_type_support
2 parents 3736d32 + d16f634 commit ebb3a2b

File tree

15 files changed

+647
-17
lines changed

15 files changed

+647
-17
lines changed

docs/agents.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ There are five ways to run an agent:
6565

6666
1. [`agent.run()`][pydantic_ai.agent.AbstractAgent.run] — an async function which returns a [`RunResult`][pydantic_ai.agent.AgentRunResult] containing a completed response.
6767
2. [`agent.run_sync()`][pydantic_ai.agent.AbstractAgent.run_sync] — a plain, synchronous function which returns a [`RunResult`][pydantic_ai.agent.AgentRunResult] containing a completed response (internally, this just calls `loop.run_until_complete(self.run())`).
68-
3. [`agent.run_stream()`][pydantic_ai.agent.AbstractAgent.run_stream] — an async context manager which returns a [`StreamedRunResult`][pydantic_ai.result.StreamedRunResult], which contains methods to stream text and structured output as an async iterable.
68+
3. [`agent.run_stream()`][pydantic_ai.agent.AbstractAgent.run_stream] — an async context manager which returns a [`StreamedRunResult`][pydantic_ai.result.StreamedRunResult], which contains methods to stream text and structured output as an async iterable. [`agent.run_stream_sync()`][pydantic_ai.agent.AbstractAgent.run_stream_sync] is a synchronous variation that returns a [`StreamedRunResultSync`][pydantic_ai.result.StreamedRunResultSync] with synchronous versions of the same methods.
6969
4. [`agent.run_stream_events()`][pydantic_ai.agent.AbstractAgent.run_stream_events] — a function which returns an async iterable of [`AgentStreamEvent`s][pydantic_ai.messages.AgentStreamEvent] and a [`AgentRunResultEvent`][pydantic_ai.run.AgentRunResultEvent] containing the final run result.
7070
5. [`agent.iter()`][pydantic_ai.Agent.iter] — a context manager which returns an [`AgentRun`][pydantic_ai.agent.AgentRun], an async iterable over the nodes of the agent's underlying [`Graph`][pydantic_graph.graph.Graph].
7171

docs/api/result.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
inherited_members: true
66
members:
77
- StreamedRunResult
8+
- StreamedRunResultSync

docs/examples/ag-ui.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Next run the AG-UI Dojo example frontend.
4545
2. Change into to the `ag-ui/typescript-sdk` directory
4646

4747
```shell
48-
cd ag-ui/typescript-sdk
48+
cd ag-ui/sdks/typescript
4949
```
5050

5151
3. Run the Dojo app following the [official instructions](https://github.com/ag-ui-protocol/ag-ui/tree/main/typescript-sdk/apps/dojo#development-setup)

pydantic_ai_slim/pydantic_ai/_run_context.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,19 @@
1616
from .models import Model
1717
from .result import RunUsage
1818

19+
# TODO (v2): Change the default for all typevars like this from `None` to `object`
1920
AgentDepsT = TypeVar('AgentDepsT', default=None, contravariant=True)
2021
"""Type variable for agent dependencies."""
2122

23+
RunContextAgentDepsT = TypeVar('RunContextAgentDepsT', default=None, covariant=True)
24+
"""Type variable for the agent dependencies in `RunContext`."""
25+
2226

2327
@dataclasses.dataclass(repr=False, kw_only=True)
24-
class RunContext(Generic[AgentDepsT]):
28+
class RunContext(Generic[RunContextAgentDepsT]):
2529
"""Information about the current call."""
2630

27-
deps: AgentDepsT
31+
deps: RunContextAgentDepsT
2832
"""Dependencies for the agent."""
2933
model: Model
3034
"""The model used in this run."""

pydantic_ai_slim/pydantic_ai/_utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,15 @@ def sync_anext(iterator: Iterator[T]) -> T:
234234
raise StopAsyncIteration() from e
235235

236236

237+
def sync_async_iterator(async_iter: AsyncIterator[T]) -> Iterator[T]:
238+
loop = get_event_loop()
239+
while True:
240+
try:
241+
yield loop.run_until_complete(anext(async_iter))
242+
except StopAsyncIteration:
243+
break
244+
245+
237246
def now_utc() -> datetime:
238247
return datetime.now(tz=timezone.utc)
239248

@@ -489,3 +498,12 @@ def get_union_args(tp: Any) -> tuple[Any, ...]:
489498
return tuple(_unwrap_annotated(arg) for arg in get_args(tp))
490499
else:
491500
return ()
501+
502+
503+
def get_event_loop():
504+
try:
505+
event_loop = asyncio.get_event_loop()
506+
except RuntimeError: # pragma: lax no cover
507+
event_loop = asyncio.new_event_loop()
508+
asyncio.set_event_loop(event_loop)
509+
return event_loop

pydantic_ai_slim/pydantic_ai/agent/abstract.py

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from typing_extensions import Self, TypeIs, TypeVar
1313

1414
from pydantic_graph import End
15-
from pydantic_graph._utils import get_event_loop
1615

1716
from .. import (
1817
_agent_graph,
@@ -335,7 +334,7 @@ def run_sync(
335334
if infer_name and self.name is None:
336335
self._infer_name(inspect.currentframe())
337336

338-
return get_event_loop().run_until_complete(
337+
return _utils.get_event_loop().run_until_complete(
339338
self.run(
340339
user_prompt,
341340
output_type=output_type,
@@ -581,6 +580,133 @@ async def on_complete() -> None:
581580
if not yielded:
582581
raise exceptions.AgentRunError('Agent run finished without producing a final result') # pragma: no cover
583582

583+
@overload
584+
def run_stream_sync(
585+
self,
586+
user_prompt: str | Sequence[_messages.UserContent] | None = None,
587+
*,
588+
output_type: None = None,
589+
message_history: Sequence[_messages.ModelMessage] | None = None,
590+
deferred_tool_results: DeferredToolResults | None = None,
591+
model: models.Model | models.KnownModelName | str | None = None,
592+
deps: AgentDepsT = None,
593+
model_settings: ModelSettings | None = None,
594+
usage_limits: _usage.UsageLimits | None = None,
595+
usage: _usage.RunUsage | None = None,
596+
infer_name: bool = True,
597+
toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
598+
builtin_tools: Sequence[AbstractBuiltinTool] | None = None,
599+
event_stream_handler: EventStreamHandler[AgentDepsT] | None = None,
600+
) -> result.StreamedRunResultSync[AgentDepsT, OutputDataT]: ...
601+
602+
@overload
603+
def run_stream_sync(
604+
self,
605+
user_prompt: str | Sequence[_messages.UserContent] | None = None,
606+
*,
607+
output_type: OutputSpec[RunOutputDataT],
608+
message_history: Sequence[_messages.ModelMessage] | None = None,
609+
deferred_tool_results: DeferredToolResults | None = None,
610+
model: models.Model | models.KnownModelName | str | None = None,
611+
deps: AgentDepsT = None,
612+
model_settings: ModelSettings | None = None,
613+
usage_limits: _usage.UsageLimits | None = None,
614+
usage: _usage.RunUsage | None = None,
615+
infer_name: bool = True,
616+
toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
617+
builtin_tools: Sequence[AbstractBuiltinTool] | None = None,
618+
event_stream_handler: EventStreamHandler[AgentDepsT] | None = None,
619+
) -> result.StreamedRunResultSync[AgentDepsT, RunOutputDataT]: ...
620+
621+
def run_stream_sync(
622+
self,
623+
user_prompt: str | Sequence[_messages.UserContent] | None = None,
624+
*,
625+
output_type: OutputSpec[RunOutputDataT] | None = None,
626+
message_history: Sequence[_messages.ModelMessage] | None = None,
627+
deferred_tool_results: DeferredToolResults | None = None,
628+
model: models.Model | models.KnownModelName | str | None = None,
629+
deps: AgentDepsT = None,
630+
model_settings: ModelSettings | None = None,
631+
usage_limits: _usage.UsageLimits | None = None,
632+
usage: _usage.RunUsage | None = None,
633+
infer_name: bool = True,
634+
toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
635+
builtin_tools: Sequence[AbstractBuiltinTool] | None = None,
636+
event_stream_handler: EventStreamHandler[AgentDepsT] | None = None,
637+
) -> result.StreamedRunResultSync[AgentDepsT, Any]:
638+
"""Run the agent with a user prompt in sync streaming mode.
639+
640+
This is a convenience method that wraps [`run_stream()`][pydantic_ai.agent.AbstractAgent.run_stream] with `loop.run_until_complete(...)`.
641+
You therefore can't use this method inside async code or if there's an active event loop.
642+
643+
This method builds an internal agent graph (using system prompts, tools and output schemas) and then
644+
runs the graph until the model produces output matching the `output_type`, for example text or structured data.
645+
At this point, a streaming run result object is yielded from which you can stream the output as it comes in,
646+
and -- once this output has completed streaming -- get the complete output, message history, and usage.
647+
648+
As this method will consider the first output matching the `output_type` to be the final output,
649+
it will stop running the agent graph and will not execute any tool calls made by the model after this "final" output.
650+
If you want to always run the agent graph to completion and stream events and output at the same time,
651+
use [`agent.run()`][pydantic_ai.agent.AbstractAgent.run] with an `event_stream_handler` or [`agent.iter()`][pydantic_ai.agent.AbstractAgent.iter] instead.
652+
653+
Example:
654+
```python
655+
from pydantic_ai import Agent
656+
657+
agent = Agent('openai:gpt-4o')
658+
659+
def main():
660+
response = agent.run_stream_sync('What is the capital of the UK?')
661+
print(response.get_output())
662+
#> The capital of the UK is London.
663+
```
664+
665+
Args:
666+
user_prompt: User input to start/continue the conversation.
667+
output_type: Custom output type to use for this run, `output_type` may only be used if the agent has no
668+
output validators since output validators would expect an argument that matches the agent's output type.
669+
message_history: History of the conversation so far.
670+
deferred_tool_results: Optional results for deferred tool calls in the message history.
671+
model: Optional model to use for this run, required if `model` was not set when creating the agent.
672+
deps: Optional dependencies to use for this run.
673+
model_settings: Optional settings to use for this model's request.
674+
usage_limits: Optional limits on model request count or token usage.
675+
usage: Optional usage to start with, useful for resuming a conversation or agents used in tools.
676+
infer_name: Whether to try to infer the agent name from the call frame if it's not set.
677+
toolsets: Optional additional toolsets for this run.
678+
builtin_tools: Optional additional builtin tools for this run.
679+
event_stream_handler: Optional handler for events from the model's streaming response and the agent's execution of tools to use for this run.
680+
It will receive all the events up until the final result is found, which you can then read or stream from inside the context manager.
681+
Note that it does _not_ receive any events after the final result is found.
682+
683+
Returns:
684+
The result of the run.
685+
"""
686+
if infer_name and self.name is None:
687+
self._infer_name(inspect.currentframe())
688+
689+
async def _consume_stream():
690+
async with self.run_stream(
691+
user_prompt,
692+
output_type=output_type,
693+
message_history=message_history,
694+
deferred_tool_results=deferred_tool_results,
695+
model=model,
696+
deps=deps,
697+
model_settings=model_settings,
698+
usage_limits=usage_limits,
699+
usage=usage,
700+
infer_name=infer_name,
701+
toolsets=toolsets,
702+
builtin_tools=builtin_tools,
703+
event_stream_handler=event_stream_handler,
704+
) as stream_result:
705+
yield stream_result
706+
707+
async_result = _utils.get_event_loop().run_until_complete(anext(_consume_stream()))
708+
return result.StreamedRunResultSync(async_result)
709+
584710
@overload
585711
def run_stream_events(
586712
self,
@@ -1217,6 +1343,6 @@ def to_cli_sync(
12171343
agent.to_cli_sync(prog_name='assistant')
12181344
```
12191345
"""
1220-
return get_event_loop().run_until_complete(
1346+
return _utils.get_event_loop().run_until_complete(
12211347
self.to_cli(deps=deps, prog_name=prog_name, message_history=message_history)
12221348
)

pydantic_ai_slim/pydantic_ai/durable_exec/temporal/_run_context.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33
from typing import Any
44

5+
from typing_extensions import TypeVar
6+
57
from pydantic_ai.exceptions import UserError
6-
from pydantic_ai.tools import AgentDepsT, RunContext
8+
from pydantic_ai.tools import RunContext
9+
10+
AgentDepsT = TypeVar('AgentDepsT', default=None, covariant=True)
11+
"""Type variable for the agent dependencies in `RunContext`."""
712

813

914
class TemporalRunContext(RunContext[AgentDepsT]):
@@ -47,6 +52,6 @@ def serialize_run_context(cls, ctx: RunContext[Any]) -> dict[str, Any]:
4752
}
4853

4954
@classmethod
50-
def deserialize_run_context(cls, ctx: dict[str, Any], deps: AgentDepsT) -> TemporalRunContext[AgentDepsT]:
55+
def deserialize_run_context(cls, ctx: dict[str, Any], deps: Any) -> TemporalRunContext[Any]:
5156
"""Deserialize the run context from a `dict[str, Any]`."""
5257
return cls(**ctx, deps=deps)

pydantic_ai_slim/pydantic_ai/models/openai.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,10 @@ def __init__(
948948

949949
super().__init__(settings=settings, profile=profile or provider.model_profile)
950950

951+
@property
952+
def base_url(self) -> str:
953+
return str(self.client.base_url)
954+
951955
@property
952956
def model_name(self) -> OpenAIModelName:
953957
"""The model name."""

pydantic_ai_slim/pydantic_ai/providers/openrouter.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ def __init__(self, *, api_key: str) -> None: ...
8181
@overload
8282
def __init__(self, *, api_key: str, http_client: httpx.AsyncClient) -> None: ...
8383

84+
@overload
85+
def __init__(self, *, http_client: httpx.AsyncClient) -> None: ...
86+
8487
@overload
8588
def __init__(self, *, openai_client: AsyncOpenAI | None = None) -> None: ...
8689

0 commit comments

Comments
 (0)