Skip to content

Commit 2835107

Browse files
authored
feat: introduced content parts to the type of the request user message content (#164)
1 parent 3f019b3 commit 2835107

File tree

11 files changed

+82
-23
lines changed

11 files changed

+82
-23
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class EchoApplication(ChatCompletion):
3737
# Generate response with a single choice
3838
with response.create_single_choice() as choice:
3939
# Fill the content of the response with the last user's content
40-
choice.append_content(last_user_message.content or "")
40+
choice.append_content(last_user_message.text())
4141

4242

4343
# DIALApp extends FastAPI to provide a user-friendly interface for routing requests to your applications

aidial_sdk/chat_completion/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
FunctionCall,
1010
FunctionChoice,
1111
Message,
12+
MessageContentImagePart,
13+
MessageContentPart,
14+
MessageContentTextPart,
1215
Request,
1316
ResponseFormat,
1417
Role,

aidial_sdk/chat_completion/request.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from enum import Enum
22
from typing import Any, Dict, List, Literal, Mapping, Optional, Union
33

4+
from typing_extensions import assert_never
5+
46
from aidial_sdk.chat_completion.enums import Status
57
from aidial_sdk.deployment.from_request_mixin import FromRequestDeploymentMixin
8+
from aidial_sdk.exceptions import InvalidRequestError
69
from aidial_sdk.pydantic_v1 import (
710
ConstrainedFloat,
811
ConstrainedInt,
@@ -58,15 +61,51 @@ class Role(str, Enum):
5861
TOOL = "tool"
5962

6063

64+
class ImageURL(ExtraForbidModel):
65+
url: StrictStr
66+
detail: Optional[Literal["auto", "low", "high"]] = None
67+
68+
69+
class MessageContentImagePart(ExtraForbidModel):
70+
type: Literal["image_url"]
71+
image_url: ImageURL
72+
73+
74+
class MessageContentTextPart(ExtraForbidModel):
75+
type: Literal["text"]
76+
text: StrictStr
77+
78+
79+
MessageContentPart = Union[MessageContentTextPart, MessageContentImagePart]
80+
81+
6182
class Message(ExtraForbidModel):
6283
role: Role
63-
content: Optional[StrictStr] = None
84+
content: Optional[Union[StrictStr, List[MessageContentPart]]] = None
6485
custom_content: Optional[CustomContent] = None
6586
name: Optional[StrictStr] = None
6687
tool_calls: Optional[List[ToolCall]] = None
6788
tool_call_id: Optional[StrictStr] = None
6889
function_call: Optional[FunctionCall] = None
6990

91+
def text(self) -> str:
92+
"""
93+
Returns content of the message only if it's present as a string.
94+
Otherwise, throws an invalid request exception.
95+
"""
96+
97+
def _error_message(actual: str) -> str:
98+
return f"Unable to retrieve text content of the message: the actual content is {actual}."
99+
100+
if self.content is None:
101+
raise InvalidRequestError(_error_message("null or missing"))
102+
elif isinstance(self.content, str):
103+
return self.content
104+
elif isinstance(self.content, list):
105+
raise InvalidRequestError(_error_message("a list of content parts"))
106+
else:
107+
assert_never(self.content)
108+
70109

71110
class Addon(ExtraForbidModel):
72111
name: Optional[StrictStr] = None

examples/echo/app.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,15 @@ async def chat_completion(
1515
self, request: Request, response: Response
1616
) -> None:
1717
# Get last message (the newest) from the history
18-
last_user_message = request.messages[-1]
18+
last_message = request.messages[-1]
1919

2020
# Generate response with a single choice
2121
with response.create_single_choice() as choice:
2222
# Fill the content of the response with the last user's content
23-
choice.append_content(last_user_message.content or "")
23+
choice.append_content(last_message.text())
2424

25-
if last_user_message.custom_content is not None:
26-
for attachment in (
27-
last_user_message.custom_content.attachments or []
28-
):
25+
if last_message.custom_content is not None:
26+
for attachment in last_message.custom_content.attachments or []:
2927
# Add the same attachment to the response
3028
choice.add_attachment(**attachment.dict())
3129

examples/langchain_rag/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ async def chat_completion(
6161

6262
with response.create_single_choice() as choice:
6363
message = request.messages[-1]
64-
user_query = message.content or ""
64+
user_query = message.text()
6565

6666
file_url = get_last_attachment_url(request.messages)
6767
file_abs_url = urljoin(f"{DIAL_URL}/v1/", file_url)

examples/render_text/app/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ async def chat_completion(self, request: Request, response: Response):
2323
# Create a single choice
2424
with response.create_single_choice() as choice:
2525
# Get the last message content
26-
content = request.messages[-1].content or ""
26+
content = request.messages[-1].text()
2727

2828
# The image may be returned either as base64 string or as URL
2929
# The content specifies the mode of return: 'base64' or 'url'

tests/applications/broken_immediately.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ class BrokenApplication(ChatCompletion):
2727
async def chat_completion(
2828
self, request: Request, response: Response
2929
) -> None:
30-
raise_exception(request.messages[0].content or "")
30+
raise_exception(request.messages[0].text())

tests/applications/broken_in_runtime.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ async def chat_completion(
1717
choice.append_content("Test content")
1818
await response.aflush()
1919

20-
raise_exception(request.messages[0].content or "")
20+
raise_exception(request.messages[0].text())

tests/applications/echo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ async def chat_completion(
2727
response.set_response_id("test_id")
2828
response.set_created(0)
2929

30-
content = request.messages[-1].content or ""
30+
content = request.messages[-1].text()
3131

3232
with response.create_single_choice() as choice:
3333
choice.append_content(content)

tests/test_errors.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,28 @@
5151
}
5252
},
5353
),
54+
(
55+
None,
56+
400,
57+
{
58+
"error": {
59+
"message": "Unable to retrieve text content of the message: the actual content is null or missing.",
60+
"type": "invalid_request_error",
61+
"code": "400",
62+
}
63+
},
64+
),
65+
(
66+
[{"type": "text", "text": "hello"}],
67+
400,
68+
{
69+
"error": {
70+
"message": "Unable to retrieve text content of the message: the actual content is a list of content parts.",
71+
"type": "invalid_request_error",
72+
"code": "400",
73+
}
74+
},
75+
),
5476
]
5577

5678

@@ -72,10 +94,8 @@ def test_error(type, response_status_code, response_content):
7294
headers={"Api-Key": "TEST_API_KEY"},
7395
)
7496

75-
assert (
76-
response.status_code == response_status_code
77-
and response.json() == response_content
78-
)
97+
assert response.status_code == response_status_code
98+
assert response.json() == response_content
7999

80100

81101
@pytest.mark.parametrize(
@@ -96,10 +116,8 @@ def test_streaming_error(type, response_status_code, response_content):
96116
headers={"Api-Key": "TEST_API_KEY"},
97117
)
98118

99-
assert (
100-
response.status_code == response_status_code
101-
and response.json() == response_content
102-
)
119+
assert response.status_code == response_status_code
120+
assert response.json() == response_content
103121

104122

105123
@pytest.mark.parametrize(
@@ -184,4 +202,5 @@ def test_no_api_key():
184202
},
185203
)
186204

187-
assert response.status_code == 400 and response.json() == API_KEY_IS_MISSING
205+
assert response.status_code == 400
206+
assert response.json() == API_KEY_IS_MISSING

0 commit comments

Comments
 (0)