workbuddy2api 2.0.0__py3-none-any.whl

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.
@@ -0,0 +1,410 @@
1
+ """
2
+ 结构化投影元数据支持
3
+
4
+ 为响应截断提供机器可读的元数据,使客户端能够:
5
+ - 准确定位被截断的内容位置
6
+ - 了解原始内容的完整长度
7
+ - 获取压缩统计信息
8
+
9
+ ## 启用方式
10
+
11
+ ### 请求头(推荐)
12
+ ```
13
+ x-projection-metadata: structured
14
+ ```
15
+
16
+ ### 请求体参数(备选)
17
+ ```json
18
+ {"x_projection_metadata": "structured"}
19
+ ```
20
+
21
+ ## 元数据结构
22
+
23
+ 响应中新增 `_meta.projection` 字段:
24
+
25
+ ```json
26
+ {
27
+ "choices": [...],
28
+ "_meta": {
29
+ "projection": {
30
+ "enabled": true,
31
+ "version": "1.0",
32
+ "truncations": [
33
+ {
34
+ "id": "trunc-msg2-tool-output-1",
35
+ "type": "tool_output_lines",
36
+ "location": {
37
+ "message_index": 2,
38
+ "role": "tool",
39
+ "tool_call_id": "call_abc123",
40
+ "field": "content"
41
+ },
42
+ "original_size": {"lines": 156, "chars": 8942},
43
+ "kept": {
44
+ "head": {"lines": 10, "chars": 453},
45
+ "tail": {"lines": 6, "chars": 289}
46
+ },
47
+ "omitted": {
48
+ "lines": 140,
49
+ "chars": 8200,
50
+ "position": {"start_line": 11, "end_line": 150}
51
+ },
52
+ "marker": "... [omitted 140 lines] ..."
53
+ }
54
+ ],
55
+ "stats": {
56
+ "total_truncations": 1,
57
+ "original_size_chars": 8942,
58
+ "projected_size_chars": 742,
59
+ "compression_ratio": 0.083
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## 截断类型
67
+
68
+ - `tool_output_lines`: 工具输出行截断(超过 24 行)
69
+ - `free_text_chars`: 自由文本字符截断
70
+ - `hard_truncate`: 硬截断(尾部)
71
+ - `json_depth`: JSON 深度限制(超过 4 层)
72
+ - `json_keys`: JSON 键数量限制(超过 12 个)
73
+
74
+ ## 向后兼容性
75
+
76
+ - 不携带启用参数时,响应中**不包含** `_meta` 字段(默认行为)
77
+ - 现有客户端无影响
78
+ """
79
+
80
+ from __future__ import annotations
81
+
82
+ from dataclasses import asdict, dataclass, field
83
+ from typing import Any
84
+
85
+
86
+ @dataclass
87
+ class TruncationLocation:
88
+ """截断位置信息"""
89
+
90
+ message_index: int # 消息在 messages 数组中的索引
91
+ role: str # assistant | user | tool | system
92
+ field: str # content | arguments | ...
93
+ tool_call_id: str | None = None # 工具调用 ID(role=tool 时)
94
+ tool_call_index: int | None = None # 工具调用索引(role=assistant 时)
95
+
96
+ def to_dict(self) -> dict[str, Any]:
97
+ """序列化为字典(移除 None 值)"""
98
+ return {k: v for k, v in asdict(self).items() if v is not None}
99
+
100
+
101
+ @dataclass
102
+ class TruncationMetadata:
103
+ """单个截断的元数据"""
104
+
105
+ id: str # 唯一标识(格式: trunc-msg{idx}-{type}-{seq})
106
+ type: str # tool_output_lines | free_text_chars | hard_truncate | json_depth | json_keys
107
+ location: TruncationLocation # 截断位置
108
+ original_size: dict[str, int] # 原始大小(lines/chars/keys/depth)
109
+ kept: dict[str, Any] # 保留部分信息
110
+ omitted: dict[str, Any] # 省略部分信息
111
+ marker: str | dict # 人类可读标记(文本或结构化字段)
112
+
113
+ def to_dict(self) -> dict[str, Any]:
114
+ """序列化为字典"""
115
+ return {
116
+ "id": self.id,
117
+ "type": self.type,
118
+ "location": self.location.to_dict(),
119
+ "original_size": self.original_size,
120
+ "kept": self.kept,
121
+ "omitted": self.omitted,
122
+ "marker": self.marker,
123
+ }
124
+
125
+
126
+ class MetadataCollector:
127
+ """元数据收集器(在投影过程中累积截断信息)"""
128
+
129
+ VERSION = "1.0"
130
+
131
+ def __init__(self):
132
+ self.truncations: list[TruncationMetadata] = []
133
+ self._id_counters: dict[str, int] = {} # 每种类型的序列号计数器
134
+ self._original_chars_total: int = 0 # 原始总字符数
135
+ self._projected_chars_total: int = 0 # 投影后总字符数
136
+
137
+ def record_tool_output_truncation(
138
+ self,
139
+ location: TruncationLocation,
140
+ original_lines: int,
141
+ original_chars: int,
142
+ kept_head_lines: int,
143
+ kept_head_chars: int,
144
+ kept_tail_lines: int,
145
+ kept_tail_chars: int,
146
+ omitted_lines: int,
147
+ omitted_chars: int,
148
+ start_line: int,
149
+ end_line: int,
150
+ marker: str,
151
+ ) -> str:
152
+ """
153
+ 记录工具输出行截断
154
+
155
+ Args:
156
+ location: 截断位置
157
+ original_lines: 原始总行数
158
+ original_chars: 原始总 kept_head_lines: 保留的头部行数
159
+ kept_head_chars: 保留的头部字符数
160
+ kept_tail_lines: 保留的尾部行数
161
+ kept_tail_chars: 保留的尾部字符数
162
+ omitted_lines: 省略的行数
163
+ omitted_chars: 省略的字符数
164
+ start_line: 省略部分起始行号(1-indexed)
165
+ end_line: 省略部分结束行号(1-indexed)
166
+ marker: 人类可读标记
167
+
168
+ Returns:
169
+ 截断 ID
170
+ """
171
+ trunc_id = self._generate_id(location.message_index, "tool-output")
172
+
173
+ metadata = TruncationMetadata(
174
+ id=trunc_id,
175
+ type="tool_output_lines",
176
+ location=location,
177
+ original_size={"lines": original_lines, "chars": original_chars},
178
+ kept={
179
+ "head": {"lines": kept_head_lines, "chars": kept_head_chars},
180
+ "tail": {"lines": kept_tail_lines, "chars": kept_tail_chars},
181
+ },
182
+ omitted={
183
+ "lines": omitted_lines,
184
+ "chars": omitted_chars,
185
+ "position": {"start_line": start_line, "end_line": end_line},
186
+ },
187
+ marker=marker,
188
+ )
189
+
190
+ self.truncations.append(metadata)
191
+ self._update_stats(original_chars, kept_head_chars + kept_tail_chars)
192
+ return trunc_id
193
+
194
+ def record_free_text_truncation(
195
+ self,
196
+ location: TruncationLocation,
197
+ original_chars: int,
198
+ kept_head_chars: int,
199
+ kept_tail_chars: int,
200
+ omitted_chars: int,
201
+ start_char: int,
202
+ end_char: int,
203
+ marker: str,
204
+ ) -> str:
205
+ """
206
+ 记录自由文本字符截断
207
+
208
+ Args:
209
+ location: 截断位置
210
+ original_chars: 原始总字符数
211
+ kept_head_chars: 保留的头部字符数
212
+ kept_tail_chars: 保留的尾部字符数
213
+ start_char: 省略部分起始字符位置(0-indexed)
214
+ start_char: 省略部分起始字符位置(0-indexed)
215
+ end_char: 省略部分结束字符位置(0-indexed)
216
+ marker: 人类可读标记
217
+
218
+ Returns:
219
+ 截断 ID
220
+ """
221
+ trunc_id = self._generate_id(location.message_index, "free-text")
222
+
223
+ metadata = TruncationMetadata(
224
+ id=trunc_id,
225
+ type="free_text_chars",
226
+ location=location,
227
+ original_size={"chars": original_chars},
228
+ kept={
229
+ "head": {"chars": kept_head_chars},
230
+ "tail": {"chars": kept_tail_chars},
231
+ },
232
+ omitted={
233
+ "chars": omitted_chars,
234
+ "position": {"start_char": start_char, "end_char": end_char},
235
+ },
236
+ marker=marker,
237
+ )
238
+
239
+ self.truncations.append(metadata)
240
+ self._update_stats(original_chars, kept_head_chars + kept_tail_chars)
241
+ return trunc_id
242
+
243
+ def record_hard_truncation(
244
+ self,
245
+ location: TruncationLocation,
246
+ original_chars: int,
247
+ kept_chars: int,
248
+ truncated_chars: int,
249
+ marker: str,
250
+ ) -> str:
251
+ """
252
+ 记录硬截断(尾部)
253
+
254
+ Args:
255
+ location: 截断位置
256
+ original_chars: 原始总字符数
257
+ kept_chars: 保留的字符数
258
+ truncated_chars: 截断的字符数
259
+ marker: 人类可读标记
260
+
261
+ Returns:
262
+ 截断 ID
263
+ """
264
+ trunc_id = self._generate_id(location.message_index, "hard-truncate")
265
+
266
+ metadata = TruncationMetadata(
267
+ id=trunc_id,
268
+ type="hard_truncate",
269
+ location=location,
270
+ original_size={"chars": original_chars},
271
+ kept={"chars": kept_chars},
272
+ omitted={"chars": truncated_chars},
273
+ marker=marker,
274
+ )
275
+
276
+ self.truncations.append(metadata)
277
+ self._update_stats(original_chars, kept_chars)
278
+ return trunc_id
279
+
280
+ def record_json_keys_truncation(
281
+ self,
282
+ location: TruncationLocation,
283
+ original_keys: int,
284
+ kept_keys: int,
285
+ kept_key_names: list[str],
286
+ omitted_keys: int,
287
+ omitted_key_names: list[str],
288
+ marker: dict,
289
+ ) -> str:
290
+ """
291
+ 记录 JSON 键数量截断
292
+
293
+ Args:
294
+ location: 截断位置
295
+ original_keys: 原始键数量
296
+ kept_keys: 保留的键数量
297
+ kept_key_names: 保留的键名列表
298
+ omitted_keys: 省略的键数量
299
+ omitted_key_names: 省略的键名列表
300
+ marker: 结构化标记(如 {"_omitted_keys": 5})
301
+
302
+ Returns:
303
+ 截断 ID
304
+ """
305
+ trunc_id = self._generate_id(location.message_index, "json-keys")
306
+
307
+ metadata = TruncationMetadata(
308
+ id=trunc_id,
309
+ type="json_keys",
310
+ location=location,
311
+ original_size={"keys": original_keys},
312
+ kept={"keys": kept_keys, "names": kept_key_names},
313
+ omitted={"keys": omitted_keys, "names": omitted_key_names},
314
+ marker=marker,
315
+ )
316
+
317
+ self.truncations.append(metadata)
318
+ # JSON 键截断不计入字符统计(因为难以准确测量)
319
+ return trunc_id
320
+
321
+ def record_json_depth_truncation(
322
+ self,
323
+ location: TruncationLocation,
324
+ original_depth: int,
325
+ max_depth: int,
326
+ json_path: str,
327
+ marker: str,
328
+ ) -> str:
329
+ """
330
+ 记录 JSON 深度截断
331
+
332
+ Args:
333
+ location: 截断位置
334
+ original_depth: 原始嵌套深度
335
+ max_depth: 最大允许深度
336
+ json_path: 被截断的 JSON 路径(如 "data.items[0].metadata")
337
+ marker: 人类可读标记(如 "<omitted>")
338
+
339
+ Returns:
340
+ 截断 ID
341
+ """
342
+ trunc_id = self._generate_id(location.message_index, "json-depth")
343
+
344
+ metadata = TruncationMetadata(
345
+ id=trunc_id,
346
+ type="json_depth",
347
+ location=location,
348
+ original_size={"depth": original_depth},
349
+ kept={"depth": max_depth},
350
+ omitted={"depth": original_depth - max_depth, "path": json_path},
351
+ marker=marker,
352
+ )
353
+
354
+ self.truncations.append(metadata)
355
+ return trunc_id
356
+
357
+ def to_dict(self) -> dict[str, Any]:
358
+ """
359
+ 序列化为响应中的 _meta.projection 字段
360
+
361
+ Returns:
362
+ 字典格式的元数据(包含所有截断记录 + 统计信息)
363
+ """
364
+ compression_ratio = 0.0
365
+ if self._original_chars_total > 0:
366
+ compression_ratio = round(
367
+ self._projected_chars_total / self._original_chars_total, 3
368
+ )
369
+
370
+ return {
371
+ "enabled": True,
372
+ "version": self.VERSION,
373
+ "truncations": [t.to_dict() for t in self.truncations],
374
+ "stats": {
375
+ "total_truncations": len(self.truncations),
376
+ "original_size_chars": self._original_chars_total,
377
+ "projected_size_chars": self._projected_chars_total,
378
+ "compression_ratio": compression_ratio,
379
+ },
380
+ }
381
+
382
+ def _generate_id(self, message_index: int, type_slug: str) -> str:
383
+ """
384
+ 生成截断唯一 ID
385
+
386
+ 格式: trunc-msg{idx}-{type}-{seq}
387
+ 示例: trunc-msg2-tool-output-1
388
+
389
+ Args:
390
+ message_index: 消息索引
391
+ type_slug: 类型标识(tool-output | free-text | hard-truncate | json-keys | json-depth)
392
+
393
+ Returns:
394
+ 唯一 ID
395
+ """
396
+ key = f"msg{message_index}-{type_slug}"
397
+ seq = self._id_counters.get(key, 0) + 1
398
+ self._id_counters[key] = seq
399
+ return f"trunc-{key}-{seq}"
400
+
401
+ def _update_stats(self, original_chars: int, projected_chars: int) -> None:
402
+ """
403
+ 更新统计信息
404
+
405
+ Args:
406
+ original_chars: 原始字符数
407
+ projected_chars: 投影后字符数
408
+ """
409
+ self._original_chars_total += original_chars
410
+ self._projected_chars_total += projected_chars