coze-coding-utils 0.2.4__tar.gz → 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/PKG-INFO +1 -1
  2. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/pyproject.toml +1 -1
  3. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/helper/agent_helper.py +93 -15
  4. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/openai/converter/request_converter.py +22 -0
  5. coze_coding_utils-0.2.4/src/coze_coding_utils/error/test_classifier.py +0 -0
  6. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/.gitignore +0 -0
  7. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/LICENSE +0 -0
  8. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/README.md +0 -0
  9. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/__init__.py +0 -0
  10. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/error/__init__.py +0 -0
  11. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/error/classifier.py +0 -0
  12. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/error/codes.py +0 -0
  13. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/error/exceptions.py +0 -0
  14. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/error/patterns.py +0 -0
  15. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/file/__init__.py +0 -0
  16. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/file/file.py +0 -0
  17. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/helper/__init__.py +0 -0
  18. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/helper/graph_helper.py +0 -0
  19. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/helper/stream_runner.py +0 -0
  20. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/log/__init__.py +0 -0
  21. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/log/common.py +0 -0
  22. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/log/config.py +0 -0
  23. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/log/err_trace.py +0 -0
  24. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/log/loop_trace.py +0 -0
  25. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/log/node_log.py +0 -0
  26. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/log/parser.py +0 -0
  27. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/log/write_log.py +0 -0
  28. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/messages/__init__.py +0 -0
  29. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/messages/client.py +0 -0
  30. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/messages/server.py +0 -0
  31. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/openai/__init__.py +0 -0
  32. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/openai/converter/__init__.py +0 -0
  33. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/openai/converter/response_converter.py +0 -0
  34. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/openai/handler.py +0 -0
  35. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/openai/types/__init__.py +0 -0
  36. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/openai/types/request.py +0 -0
  37. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/openai/types/response.py +0 -0
  38. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/runtime_ctx/__init__.py +0 -0
  39. {coze_coding_utils-0.2.4 → coze_coding_utils-0.2.5}/src/coze_coding_utils/runtime_ctx/context.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coze-coding-utils
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: Utilities for Coze coding client runtime context and helpers.
5
5
  Project-URL: Homepage, https://code.byted.org/stone/coze-coding-client
6
6
  Author: Bytedance Stone Team
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "coze-coding-utils"
7
- version = "0.2.4"
7
+ version = "0.2.5"
8
8
  description = "Utilities for Coze coding client runtime context and helpers."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,7 +1,8 @@
1
1
  import uuid
2
2
  import json
3
- import os
4
- from typing import Any, Dict, List, Tuple, Iterator
3
+ import requests
4
+ from urllib.parse import urlparse, urlencode, parse_qs, urljoin
5
+ from typing import Any, Dict, List, Tuple, Iterator, Optional
5
6
  import time
6
7
  from coze_coding_utils.file.file import File, FileOps, infer_file_category
7
8
  from coze_coding_utils.error import classify_error
@@ -31,6 +32,78 @@ from coze_coding_utils.messages.server import (
31
32
  )
32
33
 
33
34
 
35
+ _VIDEO_MIME_MAP: Dict[str, str] = {
36
+ "mp4": "video/mp4",
37
+ "avi": "video/x-msvideo",
38
+ "mov": "video/quicktime",
39
+ "mkv": "video/x-matroska",
40
+ "flv": "video/x-flv",
41
+ "wmv": "video/x-ms-wmv",
42
+ "webm": "video/webm",
43
+ "m4v": "video/x-m4v",
44
+ "3gp": "video/3gpp",
45
+ }
46
+
47
+ # 下游 LLM API 实际支持的视频 MIME 类型白名单
48
+ _SUPPORTED_VIDEO_MIMES = {
49
+ "video/mp4",
50
+ "video/quicktime",
51
+ "video/webm",
52
+ "video/3gpp",
53
+ "video/x-m4v",
54
+ }
55
+
56
+
57
+ def _probe_remote_mime(url: str) -> Optional[str]:
58
+ """HEAD 请求探测远程文件的真实 Content-Type,失败时返回 None"""
59
+ try:
60
+ resp = requests.head(url, timeout=5, allow_redirects=True)
61
+ ct = resp.headers.get("Content-Type", "")
62
+ return ct.split(";")[0].strip() or None
63
+ except Exception:
64
+ return None
65
+
66
+
67
+ def _ensure_video_content_type(url: str, mime_type: str) -> str:
68
+ """
69
+ 对象存储(TOS/S3)的 presigned URL 可能返回 application/octet-stream。
70
+ 通过追加 response-content-type 参数让服务端在响应中覆盖 Content-Type,
71
+ 从而使 LLM API fetch URL 时能拿到正确的 video MIME,而不会因 octet-stream 被拒。
72
+ 已有签名不受影响(TOS/S3 response-* 参数不计入签名哈希)。
73
+ """
74
+ parsed = urlparse(url)
75
+ # 已经有正确的 response-content-type,不重复添加
76
+ qs = parse_qs(parsed.query, keep_blank_values=True)
77
+ if qs.get("response-content-type") == [mime_type]:
78
+ return url
79
+ sep = "&" if parsed.query else "?"
80
+ return url + sep + "response-content-type=" + requests.utils.quote(mime_type, safe="")
81
+
82
+
83
+ def _build_video_parts(
84
+ file_data: File, file_info: Any, file_ext: str, mime_override: Optional[str] = None
85
+ ) -> List[Dict[str, Any]]:
86
+ """将视频文件转换为 content_parts,不支持的格式退化为文本描述。
87
+ mime_override: 外部已探测好的 MIME,传入时跳过内部探测,避免重复 HEAD 请求。
88
+ """
89
+ ext = file_ext.lstrip(".").lower()
90
+ mime_type = mime_override or _VIDEO_MIME_MAP.get(ext)
91
+ if not mime_type and file_data.is_remote:
92
+ mime_type = _probe_remote_mime(file_info.url)
93
+
94
+ if mime_type and mime_type in _SUPPORTED_VIDEO_MIMES:
95
+ # 追加 response-content-type 参数,确保对象存储返回正确的 Content-Type
96
+ # 防止 LLM API fetch URL 时因拿到 application/octet-stream 而报错
97
+ video_url = _ensure_video_content_type(file_info.url, mime_type) if file_data.is_remote else file_info.url
98
+ return [
99
+ {"type": "text", "text": f'{file_data.url}'},
100
+ {"type": "video_url", "video_url": {"url": video_url, "media_type": mime_type}},
101
+ ]
102
+ else:
103
+ fmt_hint = mime_type or (f".{ext}" if ext else "unknown")
104
+ return [{"type": "text", "text": f"video url: {file_info.url} (format: {fmt_hint})"}]
105
+
106
+
34
107
  def to_stream_input(msg: ClientMessage) -> Dict[str, Any]:
35
108
  content_parts = []
36
109
  if msg and msg.content and msg.content.query and msg.content.query.prompt:
@@ -43,7 +116,23 @@ def to_stream_input(msg: ClientMessage) -> Dict[str, Any]:
43
116
  and block.content.upload_file
44
117
  ):
45
118
  file_info = block.content.upload_file
46
- file_type, _ = infer_file_category(file_info.url)
119
+ # 第一轮:从 URL 后缀识别类型
120
+ file_type, file_ext = infer_file_category(file_info.url)
121
+ # 第二轮:URL 无后缀时用 file_name 兜底
122
+ if file_type == "default" and file_info.file_name:
123
+ file_type, file_ext = infer_file_category(file_info.file_name)
124
+ # 第三轮:两轮扩展名都无法识别时,HEAD 探测真实 Content-Type
125
+ probed_mime: Optional[str] = None
126
+ if file_type == "default" and file_info.url.startswith(("http://", "https://")):
127
+ probed_mime = _probe_remote_mime(file_info.url)
128
+ if probed_mime:
129
+ top = probed_mime.split("/")[0]
130
+ if top == "video":
131
+ file_type = "video"
132
+ elif top == "image":
133
+ file_type = "image"
134
+ elif top == "audio":
135
+ file_type = "audio"
47
136
  file_data = File(url=file_info.url, file_type=file_type)
48
137
  # check is image
49
138
  if file_data.file_type == "image":
@@ -61,18 +150,7 @@ def to_stream_input(msg: ClientMessage) -> Dict[str, Any]:
61
150
  )
62
151
  # check is video
63
152
  elif file_data.file_type == "video":
64
- content_parts.append(
65
- {
66
- "type": "text",
67
- "text": f'{file_data.url}'
68
- }
69
- )
70
- content_parts.append(
71
- {
72
- "type": "video_url",
73
- "video_url": {"url": file_info.url},
74
- }
75
- )
153
+ content_parts.extend(_build_video_parts(file_data, file_info, file_ext, probed_mime))
76
154
  # check is audio
77
155
  elif file_data.file_type == "audio":
78
156
  content_parts.append(
@@ -6,6 +6,10 @@ from coze_coding_utils.openai.types.request import (
6
6
  ChatMessage,
7
7
  )
8
8
  from coze_coding_utils.file.file import File, FileOps, infer_file_category
9
+ from coze_coding_utils.helper.agent_helper import (
10
+ _VIDEO_MIME_MAP,
11
+ _ensure_video_content_type,
12
+ )
9
13
 
10
14
 
11
15
  class RequestConverter:
@@ -110,6 +114,15 @@ class RequestConverter:
110
114
  video_url_data = part.get("video_url", {})
111
115
  url = video_url_data.get("url", "")
112
116
  if url:
117
+ _, file_ext = infer_file_category(url)
118
+ ext = file_ext.lstrip(".").lower()
119
+ mime_type = _VIDEO_MIME_MAP.get(ext)
120
+ if mime_type:
121
+ fixed_url = _ensure_video_content_type(url, mime_type)
122
+ return [
123
+ {"type": "text", "text": url},
124
+ {"type": "video_url", "video_url": {"url": fixed_url, "media_type": mime_type}},
125
+ ]
113
126
  return [
114
127
  {"type": "text", "text": url},
115
128
  {"type": "video_url", "video_url": {"url": url}},
@@ -147,6 +160,15 @@ class RequestConverter:
147
160
  {"type": "image_url", "image_url": {"url": url}},
148
161
  ]
149
162
  elif file_type == "video":
163
+ _, file_ext = infer_file_category(url)
164
+ ext = file_ext.lstrip(".").lower()
165
+ mime_type = _VIDEO_MIME_MAP.get(ext)
166
+ if mime_type:
167
+ fixed_url = _ensure_video_content_type(url, mime_type)
168
+ return [
169
+ {"type": "text", "text": url},
170
+ {"type": "video_url", "video_url": {"url": fixed_url, "media_type": mime_type}},
171
+ ]
150
172
  return [
151
173
  {"type": "text", "text": url},
152
174
  {"type": "video_url", "video_url": {"url": url}},