Skip to content

Commit 04f8c46

Browse files
Get chat app working with new messages format (#251)
Co-authored-by: Samuel Colvin <[email protected]>
1 parent 7bcc723 commit 04f8c46

File tree

3 files changed

+49
-10
lines changed

3 files changed

+49
-10
lines changed

pydantic_ai_examples/chat_app.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ <h1>Chat App</h1>
5353
</div>
5454
</form>
5555
<div id="error" class="d-none text-danger">
56-
Error occurred, check the console for more information.
56+
Error occurred, check the browser developer console for more information.
5757
</div>
5858
</main>
5959
</body>

pydantic_ai_examples/chat_app.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,31 @@
88
from __future__ import annotations as _annotations
99

1010
import asyncio
11+
import json
1112
import sqlite3
1213
from collections.abc import AsyncIterator
1314
from concurrent.futures.thread import ThreadPoolExecutor
1415
from contextlib import asynccontextmanager
1516
from dataclasses import dataclass
17+
from datetime import datetime, timezone
1618
from functools import partial
1719
from pathlib import Path
18-
from typing import Annotated, Any, Callable, TypeVar
20+
from typing import Annotated, Any, Callable, Literal, TypeVar
1921

2022
import fastapi
2123
import logfire
2224
from fastapi import Depends, Request
2325
from fastapi.responses import HTMLResponse, Response, StreamingResponse
2426
from pydantic import Field, TypeAdapter
25-
from typing_extensions import LiteralString, ParamSpec
27+
from typing_extensions import LiteralString, ParamSpec, TypedDict
2628

2729
from pydantic_ai import Agent
30+
from pydantic_ai.exceptions import UnexpectedModelBehavior
2831
from pydantic_ai.messages import (
2932
Message,
3033
MessagesTypeAdapter,
3134
ModelResponse,
35+
TextPart,
3236
UserPrompt,
3337
)
3438

@@ -68,19 +72,54 @@ async def get_db(request: Request) -> Database:
6872
async def get_chat(database: Database = Depends(get_db)) -> Response:
6973
msgs = await database.get_messages()
7074
return Response(
71-
b'\n'.join(MessageTypeAdapter.dump_json(m) for m in msgs),
75+
b'\n'.join(json.dumps(to_chat_message(m)).encode('utf-8') for m in msgs),
7276
media_type='text/plain',
7377
)
7478

7579

80+
class ChatMessage(TypedDict):
81+
"""Format of messages sent to the browser."""
82+
83+
role: Literal['user', 'model']
84+
timestamp: str
85+
content: str
86+
87+
88+
def to_chat_message(m: Message) -> ChatMessage:
89+
if isinstance(m, UserPrompt):
90+
return {
91+
'role': 'user',
92+
'timestamp': m.timestamp.isoformat(),
93+
'content': m.content,
94+
}
95+
elif isinstance(m, ModelResponse):
96+
first_part = m.parts[0]
97+
if isinstance(first_part, TextPart):
98+
return {
99+
'role': 'model',
100+
'timestamp': m.timestamp.isoformat(),
101+
'content': first_part.content,
102+
}
103+
raise UnexpectedModelBehavior(f'Unexpected message type for chat app: {m}')
104+
105+
76106
@app.post('/chat/')
77107
async def post_chat(
78108
prompt: Annotated[str, fastapi.Form()], database: Database = Depends(get_db)
79109
) -> StreamingResponse:
80110
async def stream_messages():
81111
"""Streams new line delimited JSON `Message`s to the client."""
82112
# stream the user prompt so that can be displayed straight away
83-
yield MessageTypeAdapter.dump_json(UserPrompt(content=prompt)) + b'\n'
113+
yield (
114+
json.dumps(
115+
{
116+
'role': 'user',
117+
'timestamp': datetime.now(tz=timezone.utc).isoformat(),
118+
'content': prompt,
119+
}
120+
).encode('utf-8')
121+
+ b'\n'
122+
)
84123
# get the chat history so far to pass as context to the agent
85124
messages = await database.get_messages()
86125
# run the agent with the user prompt and the chat history
@@ -89,7 +128,7 @@ async def stream_messages():
89128
# text here is a `str` and the frontend wants
90129
# JSON encoded ModelResponse, so we create one
91130
m = ModelResponse.from_text(content=text, timestamp=result.timestamp())
92-
yield MessageTypeAdapter.dump_json(m) + b'\n'
131+
yield json.dumps(to_chat_message(m)).encode('utf-8') + b'\n'
93132

94133
# add new messages (e.g. the user prompt and the agent response in this case) to the database
95134
await database.add_messages(result.new_messages_json())

pydantic_ai_examples/chat_app.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async function onFetchResponse(response: Response): Promise<void> {
3636
// The format of messages, this matches pydantic-ai both for brevity and understanding
3737
// in production, you might not want to keep this format all the way to the frontend
3838
interface Message {
39-
message_kind: string
39+
role: string
4040
content: string
4141
timestamp: string
4242
}
@@ -50,14 +50,14 @@ function addMessages(responseText: string) {
5050
const messages: Message[] = lines.filter(line => line.length > 1).map(j => JSON.parse(j))
5151
for (const message of messages) {
5252
// we use the timestamp as a crude element id
53-
const {timestamp, message_kind, content} = message
53+
const {timestamp, role, content} = message
5454
const id = `msg-${timestamp}`
5555
let msgDiv = document.getElementById(id)
5656
if (!msgDiv) {
5757
msgDiv = document.createElement('div')
5858
msgDiv.id = id
59-
msgDiv.title = `${message_kind} at ${timestamp}`
60-
msgDiv.classList.add('border-top', 'pt-2', message_kind)
59+
msgDiv.title = `${role} at ${timestamp}`
60+
msgDiv.classList.add('border-top', 'pt-2', role)
6161
convElement.appendChild(msgDiv)
6262
}
6363
msgDiv.innerHTML = marked.parse(content)

0 commit comments

Comments
 (0)