deepwrap 0.1.3__tar.gz → 0.2.1__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.
- {deepwrap-0.1.3 → deepwrap-0.2.1}/PKG-INFO +76 -2
- {deepwrap-0.1.3 → deepwrap-0.2.1}/README.md +75 -1
- deepwrap-0.2.1/deepwrap/__init__.py +4 -0
- deepwrap-0.2.1/deepwrap/api/__init__.py +10 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/api/chat_session.py +89 -4
- deepwrap-0.2.1/deepwrap/api/files.py +136 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/client.py +2 -1
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/config.py +1 -1
- deepwrap-0.2.1/deepwrap/function_calling.py +104 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/interfaces/api.py +61 -8
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/interfaces/cli.py +103 -128
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap.egg-info/PKG-INFO +76 -2
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap.egg-info/SOURCES.txt +4 -1
- {deepwrap-0.1.3 → deepwrap-0.2.1}/pyproject.toml +1 -1
- deepwrap-0.2.1/tests/test_chat_session.py +182 -0
- deepwrap-0.1.3/deepwrap/__init__.py +0 -3
- deepwrap-0.1.3/deepwrap/api/__init__.py +0 -7
- {deepwrap-0.1.3 → deepwrap-0.2.1}/LICENSE +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/__main__.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/api/base.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/api/chats.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/api/pow.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/core/__init__.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/core/auth.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/core/session_manager.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/interfaces/__init__.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/modules/__init__.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/modules/pow_asm.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/py.typed +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/utils/bearer_token_extractor.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/utils/browser_finder.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/utils/browser_process.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/utils/cdp_client.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/utils/config_store.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/utils/dev_tools_http.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap/utils/port_finder.py +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap.egg-info/dependency_links.txt +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap.egg-info/entry_points.txt +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap.egg-info/requires.txt +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/deepwrap.egg-info/top_level.txt +0 -0
- {deepwrap-0.1.3 → deepwrap-0.2.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepwrap
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Python SDK, CLI, and local FastAPI wrapper for DeepSeek Chat.
|
|
5
5
|
Author-email: Nika Kudukhashvili <nikakuduxashvili0@gmail.com>
|
|
6
6
|
Maintainer-email: Nika Kudukhashvili <nikakuduxashvili0@gmail.com>
|
|
@@ -327,6 +327,66 @@ The `ChatSession` keeps track of the latest message ID internally, so follow-up
|
|
|
327
327
|
|
|
328
328
|
---
|
|
329
329
|
|
|
330
|
+
### Vision file uploads
|
|
331
|
+
|
|
332
|
+
Files can be attached only to a `vision` session. DeepWrap uploads the file,
|
|
333
|
+
waits for DeepSeek to process it, and forwards its file ID with the prompt.
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
chat = client.chats.create_session(model="vision")
|
|
337
|
+
|
|
338
|
+
response = chat.respond(
|
|
339
|
+
"Describe this image.",
|
|
340
|
+
files=["./photo.png"],
|
|
341
|
+
stream=False,
|
|
342
|
+
)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
You can also upload once and reuse the returned ID:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
uploaded = chat.upload_file("./photo.png")
|
|
349
|
+
response = chat.respond("What is visible?", file_ids=[uploaded.id], stream=False)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Pseudo function calling
|
|
353
|
+
|
|
354
|
+
DeepWrap supplies tool definitions through a synthetic system protocol. The model
|
|
355
|
+
either answers normally or returns a strict `<deepwrap_tool_call>` JSON envelope.
|
|
356
|
+
Passing `functions` enables the automatic tool/result loop; omitting it returns the
|
|
357
|
+
parsed call for execution by the application.
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
from deepwrap import Tool
|
|
361
|
+
|
|
362
|
+
add = Tool(
|
|
363
|
+
name="add",
|
|
364
|
+
description="Add two integers.",
|
|
365
|
+
parameters={
|
|
366
|
+
"type": "object",
|
|
367
|
+
"properties": {
|
|
368
|
+
"a": {"type": "integer"},
|
|
369
|
+
"b": {"type": "integer"},
|
|
370
|
+
},
|
|
371
|
+
"required": ["a", "b"],
|
|
372
|
+
},
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
result = chat.respond_with_tools(
|
|
376
|
+
"What is 20 + 22?",
|
|
377
|
+
[add],
|
|
378
|
+
functions={"add": lambda a, b: a + b},
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
print(result.content)
|
|
382
|
+
print(result.tool_calls)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Tool calling is non-streaming because the complete envelope must be validated
|
|
386
|
+
before a function can be selected or executed.
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
330
390
|
## Supported Models
|
|
331
391
|
|
|
332
392
|
DeepWrap currently supports:
|
|
@@ -337,6 +397,9 @@ default
|
|
|
337
397
|
vision
|
|
338
398
|
```
|
|
339
399
|
|
|
400
|
+
Web search is supported only by `default` (Instant). DeepWrap always sends
|
|
401
|
+
`search_enabled=false` for `expert` and `vision`, even if `search=True` is passed.
|
|
402
|
+
|
|
340
403
|
Example:
|
|
341
404
|
|
|
342
405
|
```python
|
|
@@ -385,6 +448,17 @@ Run the interactive terminal interface:
|
|
|
385
448
|
deepwrap
|
|
386
449
|
```
|
|
387
450
|
|
|
451
|
+
To attach an image in interactive mode:
|
|
452
|
+
|
|
453
|
+
```text
|
|
454
|
+
/model vision
|
|
455
|
+
/attach ./photo.png
|
|
456
|
+
Describe this image.
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Use `/attachments` to inspect pending files and `/detach` to clear them. Pending
|
|
460
|
+
attachments are consumed after the next successful response.
|
|
461
|
+
|
|
388
462
|
Send a single message from the terminal:
|
|
389
463
|
|
|
390
464
|
```bash
|
|
@@ -504,7 +578,7 @@ Example response:
|
|
|
504
578
|
{
|
|
505
579
|
"ok": true,
|
|
506
580
|
"app": "deepwrap",
|
|
507
|
-
"version": "0.1
|
|
581
|
+
"version": "0.2.1",
|
|
508
582
|
"token_configured": true,
|
|
509
583
|
"cached_clients": 1,
|
|
510
584
|
"active_sessions": 0
|
|
@@ -272,6 +272,66 @@ The `ChatSession` keeps track of the latest message ID internally, so follow-up
|
|
|
272
272
|
|
|
273
273
|
---
|
|
274
274
|
|
|
275
|
+
### Vision file uploads
|
|
276
|
+
|
|
277
|
+
Files can be attached only to a `vision` session. DeepWrap uploads the file,
|
|
278
|
+
waits for DeepSeek to process it, and forwards its file ID with the prompt.
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
chat = client.chats.create_session(model="vision")
|
|
282
|
+
|
|
283
|
+
response = chat.respond(
|
|
284
|
+
"Describe this image.",
|
|
285
|
+
files=["./photo.png"],
|
|
286
|
+
stream=False,
|
|
287
|
+
)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
You can also upload once and reuse the returned ID:
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
uploaded = chat.upload_file("./photo.png")
|
|
294
|
+
response = chat.respond("What is visible?", file_ids=[uploaded.id], stream=False)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Pseudo function calling
|
|
298
|
+
|
|
299
|
+
DeepWrap supplies tool definitions through a synthetic system protocol. The model
|
|
300
|
+
either answers normally or returns a strict `<deepwrap_tool_call>` JSON envelope.
|
|
301
|
+
Passing `functions` enables the automatic tool/result loop; omitting it returns the
|
|
302
|
+
parsed call for execution by the application.
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
from deepwrap import Tool
|
|
306
|
+
|
|
307
|
+
add = Tool(
|
|
308
|
+
name="add",
|
|
309
|
+
description="Add two integers.",
|
|
310
|
+
parameters={
|
|
311
|
+
"type": "object",
|
|
312
|
+
"properties": {
|
|
313
|
+
"a": {"type": "integer"},
|
|
314
|
+
"b": {"type": "integer"},
|
|
315
|
+
},
|
|
316
|
+
"required": ["a", "b"],
|
|
317
|
+
},
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
result = chat.respond_with_tools(
|
|
321
|
+
"What is 20 + 22?",
|
|
322
|
+
[add],
|
|
323
|
+
functions={"add": lambda a, b: a + b},
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
print(result.content)
|
|
327
|
+
print(result.tool_calls)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Tool calling is non-streaming because the complete envelope must be validated
|
|
331
|
+
before a function can be selected or executed.
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
275
335
|
## Supported Models
|
|
276
336
|
|
|
277
337
|
DeepWrap currently supports:
|
|
@@ -282,6 +342,9 @@ default
|
|
|
282
342
|
vision
|
|
283
343
|
```
|
|
284
344
|
|
|
345
|
+
Web search is supported only by `default` (Instant). DeepWrap always sends
|
|
346
|
+
`search_enabled=false` for `expert` and `vision`, even if `search=True` is passed.
|
|
347
|
+
|
|
285
348
|
Example:
|
|
286
349
|
|
|
287
350
|
```python
|
|
@@ -330,6 +393,17 @@ Run the interactive terminal interface:
|
|
|
330
393
|
deepwrap
|
|
331
394
|
```
|
|
332
395
|
|
|
396
|
+
To attach an image in interactive mode:
|
|
397
|
+
|
|
398
|
+
```text
|
|
399
|
+
/model vision
|
|
400
|
+
/attach ./photo.png
|
|
401
|
+
Describe this image.
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Use `/attachments` to inspect pending files and `/detach` to clear them. Pending
|
|
405
|
+
attachments are consumed after the next successful response.
|
|
406
|
+
|
|
333
407
|
Send a single message from the terminal:
|
|
334
408
|
|
|
335
409
|
```bash
|
|
@@ -449,7 +523,7 @@ Example response:
|
|
|
449
523
|
{
|
|
450
524
|
"ok": true,
|
|
451
525
|
"app": "deepwrap",
|
|
452
|
-
"version": "0.1
|
|
526
|
+
"version": "0.2.1",
|
|
453
527
|
"token_configured": true,
|
|
454
528
|
"cached_clients": 1,
|
|
455
529
|
"active_sessions": 0
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Callable, Generator, Literal, Mapping, Optional, Sequence
|
|
4
5
|
|
|
5
6
|
from .base import BaseAPI
|
|
6
7
|
from deepwrap.config import Config
|
|
8
|
+
from deepwrap.function_calling import (
|
|
9
|
+
Tool,
|
|
10
|
+
ToolCall,
|
|
11
|
+
ToolResponse,
|
|
12
|
+
build_tool_prompt,
|
|
13
|
+
build_tool_result,
|
|
14
|
+
execute_tool_call,
|
|
15
|
+
parse_tool_calls,
|
|
16
|
+
)
|
|
7
17
|
|
|
8
18
|
|
|
9
19
|
ChunkKind = Literal["think", "response"]
|
|
@@ -77,6 +87,7 @@ class ChatSession(BaseAPI):
|
|
|
77
87
|
prompt: str,
|
|
78
88
|
thinking: bool = True,
|
|
79
89
|
search: bool = True,
|
|
90
|
+
ref_file_ids: Optional[Sequence[str]] = None,
|
|
80
91
|
) -> Generator[tuple[ChunkKind, str], None, None]:
|
|
81
92
|
"""
|
|
82
93
|
Send a prompt and stream structured response chunks.
|
|
@@ -104,14 +115,17 @@ class ChatSession(BaseAPI):
|
|
|
104
115
|
prompt = "\n".join(Config.god_mode).format(prompt)
|
|
105
116
|
self._is_god_mode_triggered = True
|
|
106
117
|
|
|
118
|
+
if ref_file_ids and self.model_type != "vision":
|
|
119
|
+
raise ValueError("File attachments are supported only by the vision model.")
|
|
120
|
+
|
|
107
121
|
body = {
|
|
108
122
|
"chat_session_id": self.session_id,
|
|
109
123
|
"parent_message_id": self._last_message_id,
|
|
110
124
|
"model_type": self.model_type if self._last_message_id is None else None,
|
|
111
125
|
"prompt": prompt,
|
|
112
|
-
"ref_file_ids":
|
|
126
|
+
"ref_file_ids": list(ref_file_ids or ()),
|
|
113
127
|
"thinking_enabled": thinking,
|
|
114
|
-
"search_enabled": search,
|
|
128
|
+
"search_enabled": search if self.model_type == "default" else False,
|
|
115
129
|
"action": None,
|
|
116
130
|
"preempt": False,
|
|
117
131
|
}
|
|
@@ -215,6 +229,7 @@ class ChatSession(BaseAPI):
|
|
|
215
229
|
prompt: str,
|
|
216
230
|
thinking: bool = True,
|
|
217
231
|
search: bool = True,
|
|
232
|
+
ref_file_ids: Optional[Sequence[str]] = None,
|
|
218
233
|
) -> Generator[str, None, None]:
|
|
219
234
|
"""
|
|
220
235
|
Internal flat streaming generator.
|
|
@@ -242,6 +257,7 @@ class ChatSession(BaseAPI):
|
|
|
242
257
|
prompt = prompt,
|
|
243
258
|
thinking = thinking,
|
|
244
259
|
search = search,
|
|
260
|
+
ref_file_ids = ref_file_ids,
|
|
245
261
|
):
|
|
246
262
|
if kind != active_kind:
|
|
247
263
|
if active_kind == "think":
|
|
@@ -263,6 +279,8 @@ class ChatSession(BaseAPI):
|
|
|
263
279
|
thinking: bool = True,
|
|
264
280
|
search: bool = True,
|
|
265
281
|
stream: bool = True,
|
|
282
|
+
files: Optional[Sequence[str | Path]] = None,
|
|
283
|
+
file_ids: Optional[Sequence[str]] = None,
|
|
266
284
|
) -> str | Generator[str, None, None]:
|
|
267
285
|
"""
|
|
268
286
|
Send a prompt and return either a stream or the full response.
|
|
@@ -287,11 +305,18 @@ class ChatSession(BaseAPI):
|
|
|
287
305
|
- a `str` when `stream=False`
|
|
288
306
|
"""
|
|
289
307
|
|
|
308
|
+
resolved_file_ids = list(file_ids or ())
|
|
309
|
+
if files:
|
|
310
|
+
if self.model_type != "vision":
|
|
311
|
+
raise ValueError("File attachments are supported only by the vision model.")
|
|
312
|
+
resolved_file_ids.extend(self.upload_file(path).id for path in files)
|
|
313
|
+
|
|
290
314
|
if stream:
|
|
291
315
|
return self._respond_stream(
|
|
292
316
|
prompt = prompt,
|
|
293
317
|
thinking = thinking,
|
|
294
318
|
search = search,
|
|
319
|
+
ref_file_ids = resolved_file_ids,
|
|
295
320
|
)
|
|
296
321
|
|
|
297
322
|
return "".join(
|
|
@@ -299,5 +324,65 @@ class ChatSession(BaseAPI):
|
|
|
299
324
|
prompt = prompt,
|
|
300
325
|
thinking = thinking,
|
|
301
326
|
search = search,
|
|
327
|
+
ref_file_ids = resolved_file_ids,
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def upload_file(self, path: str | Path, *, timeout: float = 60.0):
|
|
332
|
+
"""Upload and process a file for use by this vision session."""
|
|
333
|
+
|
|
334
|
+
if self.model_type != "vision":
|
|
335
|
+
raise ValueError("File attachments are supported only by the vision model.")
|
|
336
|
+
return self._client.files.upload_and_wait(
|
|
337
|
+
path,
|
|
338
|
+
model=self.model_type,
|
|
339
|
+
timeout=timeout,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def respond_with_tools(
|
|
343
|
+
self,
|
|
344
|
+
prompt: str,
|
|
345
|
+
tools: Sequence[Tool],
|
|
346
|
+
*,
|
|
347
|
+
functions: Optional[Mapping[str, Callable[..., object]]] = None,
|
|
348
|
+
thinking: bool = True,
|
|
349
|
+
max_tool_rounds: int = 4,
|
|
350
|
+
) -> ToolResponse:
|
|
351
|
+
"""Ask for tool calls and optionally execute them until a final answer."""
|
|
352
|
+
|
|
353
|
+
if not tools:
|
|
354
|
+
raise ValueError("At least one tool definition is required.")
|
|
355
|
+
if max_tool_rounds < 1:
|
|
356
|
+
raise ValueError("max_tool_rounds must be at least 1.")
|
|
357
|
+
|
|
358
|
+
known_names = {tool.name for tool in tools}
|
|
359
|
+
message = build_tool_prompt(prompt, tools)
|
|
360
|
+
call_history: list[ToolCall] = []
|
|
361
|
+
|
|
362
|
+
for _ in range(max_tool_rounds):
|
|
363
|
+
response = "".join(
|
|
364
|
+
chunk
|
|
365
|
+
for kind, chunk in self.respond_structured(
|
|
366
|
+
message,
|
|
367
|
+
thinking=thinking,
|
|
368
|
+
search=False,
|
|
369
|
+
)
|
|
370
|
+
if kind == "response"
|
|
302
371
|
)
|
|
303
|
-
|
|
372
|
+
calls = parse_tool_calls(response)
|
|
373
|
+
if not calls:
|
|
374
|
+
return ToolResponse(content=response, tool_calls=tuple(call_history))
|
|
375
|
+
if len(calls) != 1:
|
|
376
|
+
raise ValueError("The model must return exactly one tool call per turn.")
|
|
377
|
+
|
|
378
|
+
call = calls[0]
|
|
379
|
+
if call.name not in known_names:
|
|
380
|
+
raise ValueError(f"Model requested an unknown tool: {call.name}")
|
|
381
|
+
call_history.append(call)
|
|
382
|
+
if functions is None:
|
|
383
|
+
return ToolResponse(content="", tool_calls=tuple(call_history))
|
|
384
|
+
|
|
385
|
+
result = execute_tool_call(call, functions)
|
|
386
|
+
message = build_tool_result(call, result)
|
|
387
|
+
|
|
388
|
+
raise RuntimeError(f"Tool-call loop exceeded {max_tool_rounds} rounds.")
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import mimetypes
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Iterable
|
|
9
|
+
|
|
10
|
+
from .base import BaseAPI
|
|
11
|
+
from deepwrap.config import Config
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from deepwrap.client import Client
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class UploadedFile:
|
|
19
|
+
"""A file accepted and processed by DeepSeek."""
|
|
20
|
+
|
|
21
|
+
id: str
|
|
22
|
+
name: str
|
|
23
|
+
size: int
|
|
24
|
+
status: str
|
|
25
|
+
is_image: bool
|
|
26
|
+
model_kind: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FilesAPI(BaseAPI):
|
|
30
|
+
"""Upload files and wait for DeepSeek's asynchronous processing."""
|
|
31
|
+
|
|
32
|
+
UPLOAD_PATH = "/api/v0/file/upload_file"
|
|
33
|
+
UPLOAD_ENDPOINT = f"{Config.base_url}{UPLOAD_PATH}"
|
|
34
|
+
FETCH_ENDPOINT = f"{Config.base_url}{Config.api_prefix}/file/fetch_files"
|
|
35
|
+
FAILED_STATUSES = {
|
|
36
|
+
"FAILED",
|
|
37
|
+
"ERROR",
|
|
38
|
+
"REJECTED",
|
|
39
|
+
"CANCELLED",
|
|
40
|
+
"EXPIRED",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _unwrap(payload: dict[str, Any]) -> dict[str, Any]:
|
|
45
|
+
if payload.get("code") != 0:
|
|
46
|
+
raise RuntimeError(f"file request failed: {payload}")
|
|
47
|
+
data = payload.get("data", {})
|
|
48
|
+
if data.get("biz_code", 0) != 0:
|
|
49
|
+
raise RuntimeError(f"file request failed: {payload}")
|
|
50
|
+
return data.get("biz_data", {})
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def _to_uploaded_file(raw: dict[str, Any]) -> UploadedFile:
|
|
54
|
+
return UploadedFile(
|
|
55
|
+
id=str(raw["id"]),
|
|
56
|
+
name=str(raw.get("file_name", "")),
|
|
57
|
+
size=int(raw.get("file_size", 0)),
|
|
58
|
+
status=str(raw.get("status", "UNKNOWN")),
|
|
59
|
+
is_image=bool(raw.get("is_image", False)),
|
|
60
|
+
model_kind=str(raw.get("model_kind", "")),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def upload(self, path: str | Path, *, model: str = "vision") -> UploadedFile:
|
|
64
|
+
"""Upload one local file and return its initial server record."""
|
|
65
|
+
|
|
66
|
+
file_path = Path(path).expanduser()
|
|
67
|
+
if not file_path.is_file():
|
|
68
|
+
raise FileNotFoundError(f"File does not exist: {file_path}")
|
|
69
|
+
|
|
70
|
+
size = file_path.stat().st_size
|
|
71
|
+
mime_type = mimetypes.guess_type(file_path.name)[0] or "application/octet-stream"
|
|
72
|
+
headers = {
|
|
73
|
+
"x-thinking-enabled": "true",
|
|
74
|
+
"x-model-type": model,
|
|
75
|
+
"x-file-size": str(size),
|
|
76
|
+
"x-ds-pow-response": self._client.pow.build_header(self.UPLOAD_PATH),
|
|
77
|
+
}
|
|
78
|
+
with file_path.open("rb") as handle:
|
|
79
|
+
response = self._post(
|
|
80
|
+
self.UPLOAD_ENDPOINT,
|
|
81
|
+
files={"file": (file_path.name, handle, mime_type)},
|
|
82
|
+
headers=headers,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
response.raise_for_status()
|
|
86
|
+
return self._to_uploaded_file(self._unwrap(response.json()))
|
|
87
|
+
|
|
88
|
+
def fetch(self, file_ids: Iterable[str]) -> list[UploadedFile]:
|
|
89
|
+
"""Fetch the current processing state for one or more file IDs."""
|
|
90
|
+
|
|
91
|
+
ids = [str(file_id) for file_id in file_ids]
|
|
92
|
+
if not ids:
|
|
93
|
+
return []
|
|
94
|
+
response = self._get(
|
|
95
|
+
self.FETCH_ENDPOINT,
|
|
96
|
+
params={"file_ids": ",".join(ids)},
|
|
97
|
+
)
|
|
98
|
+
response.raise_for_status()
|
|
99
|
+
raw = self._unwrap(response.json())
|
|
100
|
+
return [self._to_uploaded_file(item) for item in raw.get("files", [])]
|
|
101
|
+
|
|
102
|
+
def wait_until_ready(
|
|
103
|
+
self,
|
|
104
|
+
file_id: str,
|
|
105
|
+
*,
|
|
106
|
+
timeout: float = 60.0,
|
|
107
|
+
poll_interval: float = 0.5,
|
|
108
|
+
) -> UploadedFile:
|
|
109
|
+
"""Poll until a file succeeds, fails, or the timeout expires."""
|
|
110
|
+
|
|
111
|
+
deadline = time.monotonic() + timeout
|
|
112
|
+
while time.monotonic() < deadline:
|
|
113
|
+
records = self.fetch([file_id])
|
|
114
|
+
if records:
|
|
115
|
+
record = records[0]
|
|
116
|
+
status = record.status.upper()
|
|
117
|
+
if status == "SUCCESS":
|
|
118
|
+
return record
|
|
119
|
+
if status in self.FAILED_STATUSES or status.endswith("_FAILED"):
|
|
120
|
+
raise RuntimeError(
|
|
121
|
+
f"File processing failed for {file_id}: {record.status}"
|
|
122
|
+
)
|
|
123
|
+
time.sleep(poll_interval)
|
|
124
|
+
raise TimeoutError(f"Timed out waiting for file processing: {file_id}")
|
|
125
|
+
|
|
126
|
+
def upload_and_wait(
|
|
127
|
+
self,
|
|
128
|
+
path: str | Path,
|
|
129
|
+
*,
|
|
130
|
+
model: str = "vision",
|
|
131
|
+
timeout: float = 60.0,
|
|
132
|
+
) -> UploadedFile:
|
|
133
|
+
"""Upload a file and wait until it is ready for use in a prompt."""
|
|
134
|
+
|
|
135
|
+
uploaded = self.upload(path, model=model)
|
|
136
|
+
return self.wait_until_ready(uploaded.id, timeout=timeout)
|
|
@@ -4,7 +4,7 @@ import uuid
|
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
6
|
from deepwrap.core import SessionManager, Auth
|
|
7
|
-
from deepwrap.api import ChatsAPI, PowAPI
|
|
7
|
+
from deepwrap.api import ChatsAPI, FilesAPI, PowAPI
|
|
8
8
|
from deepwrap.api.chat_session import ChatSession
|
|
9
9
|
from deepwrap.utils.config_store import ConfigStore
|
|
10
10
|
from deepwrap.utils.bearer_token_extractor import BearerTokenExtractor
|
|
@@ -53,6 +53,7 @@ class Client:
|
|
|
53
53
|
|
|
54
54
|
self.session = SessionManager(bearer_token = bearer_token)
|
|
55
55
|
self.pow = PowAPI(self)
|
|
56
|
+
self.files = FilesAPI(self)
|
|
56
57
|
self.chats = ChatsAPI(self)
|
|
57
58
|
|
|
58
59
|
self._conversations: dict[str, ChatSession] = {}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Callable, Mapping, Sequence
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
TOOL_CALL_PATTERN = re.compile(
|
|
11
|
+
r"<deepwrap_tool_call>\s*(\{.*?\})\s*</deepwrap_tool_call>", re.DOTALL
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Tool:
|
|
17
|
+
"""Description of a callable function exposed to the model."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
description: str
|
|
21
|
+
parameters: dict[str, Any]
|
|
22
|
+
|
|
23
|
+
def as_dict(self) -> dict[str, Any]:
|
|
24
|
+
return {
|
|
25
|
+
"name": self.name,
|
|
26
|
+
"description": self.description,
|
|
27
|
+
"parameters": self.parameters,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class ToolCall:
|
|
33
|
+
name: str
|
|
34
|
+
arguments: dict[str, Any]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class ToolResponse:
|
|
39
|
+
content: str
|
|
40
|
+
tool_calls: tuple[ToolCall, ...] = ()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_tool_prompt(prompt: str, tools: Sequence[Tool]) -> str:
|
|
44
|
+
"""Create the synthetic system protocol prepended to a user message."""
|
|
45
|
+
|
|
46
|
+
definitions = json.dumps(
|
|
47
|
+
[tool.as_dict() for tool in tools], ensure_ascii=False, separators=(",", ":")
|
|
48
|
+
)
|
|
49
|
+
return (
|
|
50
|
+
"[DEEPWRAP SYSTEM TOOL PROTOCOL]\n"
|
|
51
|
+
"You have access to the tools listed below. If a tool is required, reply "
|
|
52
|
+
"with exactly one call and no other text:\n"
|
|
53
|
+
'<deepwrap_tool_call>{"name":"tool_name","arguments":{}}</deepwrap_tool_call>\n'
|
|
54
|
+
"Arguments must be valid JSON and conform to the tool's parameters schema. "
|
|
55
|
+
"Never invent a tool. If no tool is required, answer the user normally and "
|
|
56
|
+
"do not emit a tool-call tag.\n"
|
|
57
|
+
f"TOOLS={definitions}\n"
|
|
58
|
+
"[END DEEPWRAP SYSTEM TOOL PROTOCOL]\n\n"
|
|
59
|
+
f"USER MESSAGE:\n{prompt}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_tool_calls(text: str) -> tuple[ToolCall, ...]:
|
|
64
|
+
"""Parse and validate DeepWrap tool-call envelopes from model output."""
|
|
65
|
+
|
|
66
|
+
calls: list[ToolCall] = []
|
|
67
|
+
for match in TOOL_CALL_PATTERN.finditer(text):
|
|
68
|
+
try:
|
|
69
|
+
payload = json.loads(match.group(1))
|
|
70
|
+
except json.JSONDecodeError as exc:
|
|
71
|
+
raise ValueError(f"Model returned invalid tool-call JSON: {exc}") from exc
|
|
72
|
+
name = payload.get("name")
|
|
73
|
+
arguments = payload.get("arguments", {})
|
|
74
|
+
if not isinstance(name, str) or not name:
|
|
75
|
+
raise ValueError("Tool call must contain a non-empty string 'name'.")
|
|
76
|
+
if not isinstance(arguments, dict):
|
|
77
|
+
raise ValueError("Tool call 'arguments' must be a JSON object.")
|
|
78
|
+
calls.append(ToolCall(name=name, arguments=arguments))
|
|
79
|
+
return tuple(calls)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def execute_tool_call(
|
|
83
|
+
call: ToolCall, functions: Mapping[str, Callable[..., Any]]
|
|
84
|
+
) -> Any:
|
|
85
|
+
"""Execute a parsed call against an explicit name-to-callable mapping."""
|
|
86
|
+
|
|
87
|
+
function = functions.get(call.name)
|
|
88
|
+
if function is None:
|
|
89
|
+
raise ValueError(f"No function registered for tool: {call.name}")
|
|
90
|
+
return function(**call.arguments)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_tool_result(call: ToolCall, result: Any) -> str:
|
|
94
|
+
"""Serialize a tool result for the next model turn."""
|
|
95
|
+
|
|
96
|
+
encoded = json.dumps(result, ensure_ascii=False, default=str)
|
|
97
|
+
return (
|
|
98
|
+
"[DEEPWRAP TOOL RESULT]\n"
|
|
99
|
+
f"name={call.name}\n"
|
|
100
|
+
f"result={encoded}\n"
|
|
101
|
+
"Use this result to answer the original user request. Call another tool only "
|
|
102
|
+
"if it is necessary.\n"
|
|
103
|
+
"[END DEEPWRAP TOOL RESULT]"
|
|
104
|
+
)
|