Skip to content

Commit 80accf4

Browse files
author
lmh
committed
feat: support local file upload
1 parent 5920db2 commit 80accf4

File tree

2 files changed

+110
-15
lines changed

2 files changed

+110
-15
lines changed

volcenginesdkarkruntime/resources/responses/responses.py

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# This modified file is released under the same license.
1111

1212
from __future__ import annotations
13-
13+
from pathlib import Path
1414
from typing import (
1515
Union,
1616
Iterable,
@@ -19,7 +19,7 @@
1919

2020
import httpx
2121
from typing_extensions import Literal
22-
22+
from urllib.parse import urlparse, unquote_plus
2323
from ..._types import Body, Query, Headers
2424
from ..._utils._utils import with_sts_token, async_with_sts_token
2525
from ..._base_client import make_request_options
@@ -36,7 +36,12 @@
3636
from ...types.responses.response_create_params import ToolChoice
3737
from ...types.responses.response import Response
3838
from ...types.responses.tool_param import ToolParam
39-
from ...types.responses.response_input_param import ResponseInputParam
39+
from ...types.responses.response_input_param import (
40+
ResponseInputParam,
41+
)
42+
from ...types.responses.response_input_message_content_list_param import (
43+
ResponseInputContentParam,
44+
)
4045
from ...types.responses.response_stream_event import ResponseStreamEvent
4146
from ...types.responses.response_text_config_param import ResponseTextConfigParam
4247
from ...types.responses.response_caching_param import ResponseCaching
@@ -45,6 +50,14 @@
4550

4651
__all__ = ["Responses", "AsyncResponses"]
4752

53+
RESPONSES_MULTIMODAL_CONTENT_DATA_KEYS = {
54+
"input_image": "image_url",
55+
"input_video": "video_url",
56+
"input_file": "file_data",
57+
}
58+
59+
FILE_PATH_SCHEME = "file"
60+
4861

4962
def _add_beta_headers(
5063
extra_headers: Headers | None = None, tools: Iterable[ToolParam] | None = ()
@@ -185,6 +198,51 @@ def delete(
185198
cast_to=type(None),
186199
)
187200

201+
def _prepare_responses_input(self, input: ResponseInputParam):
202+
for input_item in input: # type: ResponseInputItemParam
203+
if "content" not in input_item: # skip non-content message
204+
continue
205+
content_list = input_item["content"]
206+
207+
if not isinstance(content_list, list): # skip non-list content
208+
continue
209+
210+
for content in content_list: # type: ResponseInputContentParam
211+
self._prepare_responses_input_file(content=content)
212+
213+
def _prepare_responses_input_file(self, content: ResponseInputContentParam):
214+
if "type" not in content: # skip non-type content
215+
return
216+
content_type = content["type"]
217+
if (
218+
content_type not in RESPONSES_MULTIMODAL_CONTENT_DATA_KEYS.keys()
219+
): # skip non-multimodal content
220+
return
221+
content_data_key = RESPONSES_MULTIMODAL_CONTENT_DATA_KEYS[content_type]
222+
if content_data_key not in content: # skip non-url content
223+
return
224+
content_data: str = content[content_data_key]
225+
226+
parsed = urlparse(content_data)
227+
if parsed.scheme != FILE_PATH_SCHEME: # skip non-file-scheme content
228+
return
229+
230+
# Decode percent-encoded parts in the path
231+
decoded_path = unquote_plus(parsed.path)
232+
233+
if parsed.netloc:
234+
# Handle cases like file://hostname/share/path or Windows UNC
235+
# For simplicity, prefix double-slash for network path
236+
full_path = f"{parsed.netloc}{decoded_path}"
237+
else:
238+
full_path = decoded_path
239+
240+
file_path = Path(full_path)
241+
file = self._client.files.create(file=file_path, purpose="user_data")
242+
self._client.files.wait_for_processing(id=file.id)
243+
content[content_data_key] = None # replace with file id
244+
content["file_id"] = file.id
245+
188246

189247
class AsyncResponses(AsyncAPIResource):
190248
@cached_property
@@ -223,6 +281,8 @@ async def create(
223281
reasoning: Optional[Reasoning] | None = None,
224282
) -> Response | AsyncStream[ResponseStreamEvent]:
225283
extra_headers = _add_beta_headers(extra_headers, tools)
284+
await self._prepare_responses_input(input=input)
285+
226286
resp = await self._post(
227287
"/responses",
228288
body={
@@ -309,6 +369,51 @@ async def delete(
309369
cast_to=type(None),
310370
)
311371

372+
async def _prepare_responses_input(self, input: ResponseInputParam):
373+
for input_item in input: # type: ResponseInputItemParam
374+
if "content" not in input_item: # skip non-content message
375+
continue
376+
content_list = input_item["content"]
377+
378+
if not isinstance(content_list, list): # skip non-list content
379+
continue
380+
381+
for content in content_list: # type: ResponseInputContentParam
382+
await self._prepare_responses_input_file(content=content)
383+
384+
async def _prepare_responses_input_file(self, content: ResponseInputContentParam):
385+
if "type" not in content: # skip non-type content
386+
return
387+
content_type = content["type"]
388+
if (
389+
content_type not in RESPONSES_MULTIMODAL_CONTENT_DATA_KEYS.keys()
390+
): # skip non-multimodal content
391+
return
392+
content_data_key = RESPONSES_MULTIMODAL_CONTENT_DATA_KEYS[content_type]
393+
if content_data_key not in content: # skip non-url content
394+
return
395+
content_data: str = content[content_data_key]
396+
397+
parsed = urlparse(content_data)
398+
if parsed.scheme != FILE_PATH_SCHEME: # skip non-file-scheme content
399+
return
400+
401+
# Decode percent-encoded parts in the path
402+
decoded_path = unquote_plus(parsed.path)
403+
404+
if parsed.netloc:
405+
# Handle cases like file://hostname/share/path or Windows UNC
406+
# For simplicity, prefix double-slash for network path
407+
full_path = f"{parsed.netloc}{decoded_path}"
408+
else:
409+
full_path = decoded_path
410+
411+
file_path = Path(full_path)
412+
file = await self._client.files.create(file=file_path, purpose="user_data")
413+
await self._client.files.wait_for_processing(id=file.id)
414+
content[content_data_key] = None # replace with file id
415+
content["file_id"] = file.id
416+
312417

313418
class ResponsesWithRawResponse:
314419
def __init__(self, responses: Responses) -> None:

volcenginesdkexamples/volcenginesdkarkruntime/async_responses_create.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import asyncio
2-
32
from volcenginesdkarkruntime import AsyncArk
43
from volcenginesdkarkruntime.types.responses.response_completed_event import ResponseCompletedEvent
54
from volcenginesdkarkruntime.types.responses.response_output_item_done_event import ResponseOutputItemDoneEvent
@@ -19,15 +18,6 @@
1918

2019

2120
async def main():
22-
# upload image file
23-
print("Upload image file")
24-
file = await client.files.create(
25-
# replace with your local image path
26-
file=open("path/to/sample.jpg", "rb"),
27-
purpose="user_data"
28-
)
29-
print(f"File uploaded: {file.id}")
30-
3121
# ==========================================================
3222
# 示例 1:多轮对话,开启 caching
3323
# ==========================================================
@@ -41,8 +31,8 @@ async def main():
4131
{"role": "user", "content": [
4232
{
4333
"type": "input_image",
44-
# ref image file id
45-
"file_id": file.id
34+
# local image file path, will be automatically uploaded to file
35+
"image_url": f"file://{your_image_path}"
4636
},
4737
{
4838
"type": "input_text",

0 commit comments

Comments
 (0)