Skip to content

Commit 302b8bf

Browse files
committed
Merge main
2 parents c4f41c0 + 8bf5bee commit 302b8bf

20 files changed

+1476
-1501
lines changed

Diff for: .github/set_docs_preview_url.py

+37-27
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import json
21
import os
32
import re
43

@@ -7,8 +6,8 @@
76
DEPLOY_OUTPUT = os.environ['DEPLOY_OUTPUT']
87
GITHUB_TOKEN = os.environ['GITHUB_TOKEN']
98
REPOSITORY = os.environ['REPOSITORY']
9+
PULL_REQUEST_NUMBER = os.environ['PULL_REQUEST_NUMBER']
1010
REF = os.environ['REF']
11-
ENVIRONMENT = os.environ['ENVIRONMENT']
1211

1312
m = re.search(r'https://(\S+)\.workers\.dev', DEPLOY_OUTPUT)
1413
assert m, f'Could not find worker URL in {DEPLOY_OUTPUT!r}'
@@ -19,37 +18,48 @@
1918

2019
version_id = m.group(1)
2120
preview_url = f'https://{version_id}-{worker_name}.workers.dev'
21+
print('Docs preview URL:', preview_url)
2222

2323
gh_headers = {
2424
'Accept': 'application/vnd.github+json',
2525
'Authorization': f'Bearer {GITHUB_TOKEN}',
2626
'X-GitHub-Api-Version': '2022-11-28',
2727
}
2828

29-
deployment_url = f'https://api.github.com/repos/{REPOSITORY}/deployments'
30-
deployment_data = {
31-
'ref': REF,
32-
'task': 'docs preview',
33-
'environment': ENVIRONMENT,
34-
'auto_merge': False,
35-
'required_contexts': [],
36-
'payload': json.dumps({
37-
'preview_url': preview_url,
38-
'worker_name': worker_name,
39-
'version_id': version_id,
40-
})
41-
}
42-
r = httpx.post(deployment_url, headers=gh_headers, json=deployment_data)
43-
print(f'POST {deployment_url}: {r.status_code}') # {r.text}
44-
r.raise_for_status()
45-
deployment_id = r.json()['id']
29+
# now create or update a comment on the PR with the preview URL
4630

47-
status_url = f'https://api.github.com/repos/{REPOSITORY}/deployments/{deployment_id}/statuses'
48-
status_data = {
49-
'environment': ENVIRONMENT,
50-
'environment_url': preview_url,
51-
'state': 'success',
52-
}
53-
r = httpx.post(status_url, headers=gh_headers, json=status_data)
54-
print(f'POST {status_url}: {r.status_code}') # {r.text}
31+
comments_url = f'https://api.github.com/repos/{REPOSITORY}/issues/{PULL_REQUEST_NUMBER}/comments'
32+
r = httpx.get(comments_url, headers=gh_headers)
33+
print(f'GET {comments_url} {r.status_code}')
34+
comment_update_url = None
35+
36+
for comment in r.json():
37+
if comment['user']['login'] == 'github-actions[bot]' and comment['body'].startswith('## Docs Preview'):
38+
comment_update_url = comment['url']
39+
break
40+
41+
body = f"""\
42+
## Docs Preview
43+
44+
<table>
45+
<tr>
46+
<td><strong>commit:</strong></td>
47+
<td><code>{REF:.7}</code></td>
48+
</tr>
49+
<tr>
50+
<td><strong>Preview URL:</strong></td>
51+
<td><a href="{preview_url}">{preview_url}</a></td>
52+
</tr>
53+
</table>
54+
"""
55+
comment_data = {'body': body}
56+
57+
if comment_update_url:
58+
print('Updating existing comment...')
59+
r = httpx.patch(comment_update_url, headers=gh_headers, json=comment_data)
60+
else:
61+
print('Creating new comment...')
62+
r = httpx.post(comments_url, headers=gh_headers, json=comment_data)
63+
64+
print(f'{r.request.method} {comments_url} {r.status_code}')
5565
r.raise_for_status()

Diff for: .github/workflows/after-ci.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77

88
permissions:
99
statuses: write
10-
deployments: write
10+
pull-requests: write
1111

1212
jobs:
1313
smokeshow:
@@ -51,6 +51,7 @@ jobs:
5151

5252
deploy-docs-preview:
5353
runs-on: ubuntu-latest
54+
if: github.event.workflow_run.event == 'pull_request'
5455
environment:
5556
name: deploy-preview
5657

@@ -86,12 +87,11 @@ jobs:
8687
--var GIT_COMMIT_SHA:${{ github.event.workflow_run.head_sha }}
8788
--var GIT_BRANCH:${{ github.event.workflow_run.head_branch }}
8889
89-
# work around for https://github.com/cloudflare/wrangler-action/issues/349
9090
- name: Set preview URL
9191
run: uv run --with httpx .github/set_docs_preview_url.py
9292
env:
9393
DEPLOY_OUTPUT: ${{ steps.deploy.outputs.command-output }}
9494
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
9595
REPOSITORY: ${{ github.repository }}
96+
PULL_REQUEST_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
9697
REF: ${{ github.event.workflow_run.head_sha }}
97-
ENVIRONMENT: deploy-preview

Diff for: docs-site/wrangler.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ workers_dev = false
99
directory = "../site"
1010
binding = "ASSETS"
1111
# so we can rewrite the HTML to insert the version notice
12-
experimental_serve_directly = false
12+
run_worker_first = true
1313

1414
[[kv_namespaces]]
1515
binding = "VERSION_NOTICE_CACHE"

Diff for: docs/help.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ If you're on a [Logfire][logfire] Pro plan, you can also get a dedicated private
1212

1313
The [PydanticAI GitHub Issues][github-issues] are a great place to ask questions and give us feedback.
1414

15-
[slack]: https://join.slack.com/t/pydanticlogfire/shared_invite/zt-2war8jrjq-w_nWG6ZX7Zm~gnzY7cXSog
15+
[slack]: https://logfire.pydantic.dev/docs/join-slack/
1616
[github-issues]: https://github.com/pydantic/pydantic-ai/issues
1717
[logfire]: https://pydantic.dev/logfire

Diff for: examples/pydantic_ai_examples/flight_booking.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,7 @@ class Failed(BaseModel):
105105

106106

107107
# This agent is responsible for extracting the user's seat selection
108-
seat_preference_agent = Agent[
109-
None, SeatPreference | Failed
110-
](
108+
seat_preference_agent = Agent[None, SeatPreference | Failed](
111109
'openai:gpt-4o',
112110
result_type=SeatPreference | Failed, # type: ignore
113111
system_prompt=(

Diff for: examples/pydantic_ai_examples/rag.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ async def retrieve(context: RunContext[Deps], search_query: str) -> str:
6767
model='text-embedding-3-small',
6868
)
6969

70-
assert (
71-
len(embedding.data) == 1
72-
), f'Expected 1 embedding, got {len(embedding.data)}, doc query: {search_query!r}'
70+
assert len(embedding.data) == 1, (
71+
f'Expected 1 embedding, got {len(embedding.data)}, doc query: {search_query!r}'
72+
)
7373
embedding = embedding.data[0].embedding
7474
embedding_json = pydantic_core.to_json(embedding).decode()
7575
rows = await context.deps.pool.fetch(
@@ -149,9 +149,9 @@ async def insert_doc_section(
149149
input=section.embedding_content(),
150150
model='text-embedding-3-small',
151151
)
152-
assert (
153-
len(embedding.data) == 1
154-
), f'Expected 1 embedding, got {len(embedding.data)}, doc section: {section}'
152+
assert len(embedding.data) == 1, (
153+
f'Expected 1 embedding, got {len(embedding.data)}, doc section: {section}'
154+
)
155155
embedding = embedding.data[0].embedding
156156
embedding_json = pydantic_core.to_json(embedding).decode()
157157
await pool.execute(

Diff for: pydantic_ai_slim/pydantic_ai/models/__init__.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,6 @@ def infer_model(model: Model | KnownModelName) -> Model:
364364
raise UserError(f'Unknown model: {model}')
365365

366366

367-
@cache
368367
def cached_async_http_client(timeout: int = 600, connect: int = 5) -> httpx.AsyncClient:
369368
"""Cached HTTPX async client so multiple agents and calls can share the same client.
370369
@@ -375,6 +374,16 @@ def cached_async_http_client(timeout: int = 600, connect: int = 5) -> httpx.Asyn
375374
The default timeouts match those of OpenAI,
376375
see <https://github.com/openai/openai-python/blob/v1.54.4/src/openai/_constants.py#L9>.
377376
"""
377+
client = _cached_async_http_client(timeout=timeout, connect=connect)
378+
if client.is_closed:
379+
# This happens if the context manager is used, so we need to create a new client.
380+
_cached_async_http_client.cache_clear()
381+
client = _cached_async_http_client(timeout=timeout, connect=connect)
382+
return client
383+
384+
385+
@cache
386+
def _cached_async_http_client(timeout: int = 600, connect: int = 5) -> httpx.AsyncClient:
378387
return httpx.AsyncClient(
379388
timeout=httpx.Timeout(timeout=timeout, connect=connect),
380389
headers={'User-Agent': get_user_agent()},

Diff for: pydantic_ai_slim/pydantic_ai/models/anthropic.py

+41-49
Original file line numberDiff line numberDiff line change
@@ -272,64 +272,56 @@ def _map_message(self, messages: list[ModelMessage]) -> tuple[str, list[MessageP
272272
anthropic_messages: list[MessageParam] = []
273273
for m in messages:
274274
if isinstance(m, ModelRequest):
275-
for part in m.parts:
276-
if isinstance(part, SystemPromptPart):
277-
system_prompt += part.content
278-
elif isinstance(part, UserPromptPart):
279-
anthropic_messages.append(MessageParam(role='user', content=part.content))
280-
elif isinstance(part, ToolReturnPart):
281-
anthropic_messages.append(
282-
MessageParam(
283-
role='user',
284-
content=[
285-
ToolResultBlockParam(
286-
tool_use_id=_guard_tool_call_id(t=part, model_source='Anthropic'),
287-
type='tool_result',
288-
content=part.model_response_str(),
289-
is_error=False,
290-
)
291-
],
292-
)
275+
user_content_params: list[ToolResultBlockParam | TextBlockParam] = []
276+
for request_part in m.parts:
277+
if isinstance(request_part, SystemPromptPart):
278+
system_prompt += request_part.content
279+
elif isinstance(request_part, UserPromptPart):
280+
text_block_param = TextBlockParam(type='text', text=request_part.content)
281+
user_content_params.append(text_block_param)
282+
elif isinstance(request_part, ToolReturnPart):
283+
tool_result_block_param = ToolResultBlockParam(
284+
tool_use_id=_guard_tool_call_id(t=request_part, model_source='Anthropic'),
285+
type='tool_result',
286+
content=request_part.model_response_str(),
287+
is_error=False,
293288
)
294-
elif isinstance(part, RetryPromptPart):
295-
if part.tool_name is None:
296-
anthropic_messages.append(MessageParam(role='user', content=part.model_response()))
289+
user_content_params.append(tool_result_block_param)
290+
elif isinstance(request_part, RetryPromptPart):
291+
if request_part.tool_name is None:
292+
retry_param = TextBlockParam(type='text', text=request_part.model_response())
297293
else:
298-
anthropic_messages.append(
299-
MessageParam(
300-
role='user',
301-
content=[
302-
ToolResultBlockParam(
303-
tool_use_id=_guard_tool_call_id(t=part, model_source='Anthropic'),
304-
type='tool_result',
305-
content=part.model_response(),
306-
is_error=True,
307-
),
308-
],
309-
)
294+
retry_param = ToolResultBlockParam(
295+
tool_use_id=_guard_tool_call_id(t=request_part, model_source='Anthropic'),
296+
type='tool_result',
297+
content=request_part.model_response(),
298+
is_error=True,
310299
)
300+
user_content_params.append(retry_param)
301+
anthropic_messages.append(
302+
MessageParam(
303+
role='user',
304+
content=user_content_params,
305+
)
306+
)
311307
elif isinstance(m, ModelResponse):
312-
content: list[TextBlockParam | ToolUseBlockParam] = []
313-
for item in m.parts:
314-
if isinstance(item, TextPart):
315-
content.append(TextBlockParam(text=item.content, type='text'))
308+
assistant_content_params: list[TextBlockParam | ToolUseBlockParam] = []
309+
for response_part in m.parts:
310+
if isinstance(response_part, TextPart):
311+
assistant_content_params.append(TextBlockParam(text=response_part.content, type='text'))
316312
else:
317-
assert isinstance(item, ToolCallPart)
318-
content.append(self._map_tool_call(item))
319-
anthropic_messages.append(MessageParam(role='assistant', content=content))
313+
tool_use_block_param = ToolUseBlockParam(
314+
id=_guard_tool_call_id(t=response_part, model_source='Anthropic'),
315+
type='tool_use',
316+
name=response_part.tool_name,
317+
input=response_part.args_as_dict(),
318+
)
319+
assistant_content_params.append(tool_use_block_param)
320+
anthropic_messages.append(MessageParam(role='assistant', content=assistant_content_params))
320321
else:
321322
assert_never(m)
322323
return system_prompt, anthropic_messages
323324

324-
@staticmethod
325-
def _map_tool_call(t: ToolCallPart) -> ToolUseBlockParam:
326-
return ToolUseBlockParam(
327-
id=_guard_tool_call_id(t=t, model_source='Anthropic'),
328-
type='tool_use',
329-
name=t.tool_name,
330-
input=t.args_as_dict(),
331-
)
332-
333325
@staticmethod
334326
def _map_tool_definition(f: ToolDefinition) -> ToolParam:
335327
return {

Diff for: pydantic_ai_slim/pydantic_ai/models/cohere.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def __init__(
124124
assert api_key is None, 'Cannot provide both `cohere_client` and `api_key`'
125125
self.client = cohere_client
126126
else:
127-
self.client = AsyncClientV2(api_key=api_key, httpx_client=http_client) # type: ignore
127+
self.client = AsyncClientV2(api_key=api_key, httpx_client=http_client)
128128

129129
async def request(
130130
self,

Diff for: pydantic_ai_slim/pydantic_ai/models/function.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ async def request_stream(
109109
model_settings,
110110
)
111111

112-
assert (
113-
self.stream_function is not None
114-
), 'FunctionModel must receive a `stream_function` to support streamed requests'
112+
assert self.stream_function is not None, (
113+
'FunctionModel must receive a `stream_function` to support streamed requests'
114+
)
115115

116116
response_stream = PeekableAsyncStream(self.stream_function(messages, agent_info))
117117

0 commit comments

Comments
 (0)