deepy-cli 0.1.1__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.
- deepy/__init__.py +9 -0
- deepy/__main__.py +7 -0
- deepy/cli.py +413 -0
- deepy/config/__init__.py +21 -0
- deepy/config/settings.py +237 -0
- deepy/data/__init__.py +1 -0
- deepy/data/tools/AskUserQuestion.md +10 -0
- deepy/data/tools/WebFetch.md +9 -0
- deepy/data/tools/WebSearch.md +9 -0
- deepy/data/tools/__init__.py +1 -0
- deepy/data/tools/bash.md +7 -0
- deepy/data/tools/edit.md +13 -0
- deepy/data/tools/modify.md +17 -0
- deepy/data/tools/read.md +8 -0
- deepy/data/tools/write.md +12 -0
- deepy/errors.py +63 -0
- deepy/llm/__init__.py +13 -0
- deepy/llm/agent.py +31 -0
- deepy/llm/context.py +109 -0
- deepy/llm/events.py +187 -0
- deepy/llm/model_capabilities.py +7 -0
- deepy/llm/provider.py +81 -0
- deepy/llm/replay.py +120 -0
- deepy/llm/runner.py +412 -0
- deepy/llm/thinking.py +30 -0
- deepy/prompts/__init__.py +6 -0
- deepy/prompts/compact.py +100 -0
- deepy/prompts/rules.py +24 -0
- deepy/prompts/runtime_context.py +98 -0
- deepy/prompts/system.py +72 -0
- deepy/prompts/tool_docs.py +21 -0
- deepy/sessions/__init__.py +17 -0
- deepy/sessions/jsonl.py +306 -0
- deepy/sessions/manager.py +202 -0
- deepy/skills.py +202 -0
- deepy/status.py +65 -0
- deepy/tools/__init__.py +6 -0
- deepy/tools/agents.py +343 -0
- deepy/tools/builtin.py +2113 -0
- deepy/tools/file_state.py +85 -0
- deepy/tools/result.py +54 -0
- deepy/tools/shell_utils.py +83 -0
- deepy/ui/__init__.py +5 -0
- deepy/ui/app.py +118 -0
- deepy/ui/ask_user_question.py +182 -0
- deepy/ui/exit_summary.py +142 -0
- deepy/ui/loading_text.py +87 -0
- deepy/ui/markdown.py +152 -0
- deepy/ui/message_view.py +546 -0
- deepy/ui/prompt_buffer.py +176 -0
- deepy/ui/prompt_input.py +286 -0
- deepy/ui/session_list.py +140 -0
- deepy/ui/session_picker.py +179 -0
- deepy/ui/slash_commands.py +67 -0
- deepy/ui/styles.py +21 -0
- deepy/ui/terminal.py +959 -0
- deepy/ui/thinking_state.py +29 -0
- deepy/ui/welcome.py +195 -0
- deepy/update_check.py +195 -0
- deepy/usage.py +192 -0
- deepy/utils/__init__.py +15 -0
- deepy/utils/debug_logger.py +62 -0
- deepy/utils/error_logger.py +107 -0
- deepy/utils/json.py +29 -0
- deepy/utils/notify.py +66 -0
- deepy_cli-0.1.1.dist-info/METADATA +205 -0
- deepy_cli-0.1.1.dist-info/RECORD +69 -0
- deepy_cli-0.1.1.dist-info/WHEEL +4 -0
- deepy_cli-0.1.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
## AskUserQuestion
|
|
2
|
+
|
|
3
|
+
Ask the user only when progress is blocked by missing intent or a required decision.
|
|
4
|
+
|
|
5
|
+
Args: `questions` (non-empty array). Each question needs `question` and non-empty `options`;
|
|
6
|
+
each option needs `label` and may include `description`. Use `multiSelect=true` only when
|
|
7
|
+
multiple choices are allowed.
|
|
8
|
+
|
|
9
|
+
Returns standard JSON with `awaitUserResponse=true`, `metadata.kind="ask_user_question"`,
|
|
10
|
+
and normalized questions.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## WebFetch
|
|
2
|
+
|
|
3
|
+
Fetch a specific web page when the user provides a complete URL.
|
|
4
|
+
|
|
5
|
+
Args: `url`.
|
|
6
|
+
|
|
7
|
+
Accepts only complete `http://` or `https://` URLs. Returns the final URL, title,
|
|
8
|
+
content type, and extracted readable text for HTML pages. Use `WebSearch` to
|
|
9
|
+
discover URLs; use `WebFetch` when the URL is already known.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## WebSearch
|
|
2
|
+
|
|
3
|
+
Search when current or external information is required.
|
|
4
|
+
|
|
5
|
+
Args: `query`.
|
|
6
|
+
|
|
7
|
+
Uses the configured local command first, then configured API endpoint if present. If
|
|
8
|
+
neither is configured, uses Deepy's built-in local web search implementation and
|
|
9
|
+
returns parsed search result titles, URLs, and snippets.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Built-in tool documentation."""
|
deepy/data/tools/bash.md
ADDED
deepy/data/tools/edit.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## edit
|
|
2
|
+
|
|
3
|
+
Replace existing text after reading the file first.
|
|
4
|
+
|
|
5
|
+
Prefer `edit` over `write` for existing files when changing specific code, tests, imports,
|
|
6
|
+
comments, blocks, or lines; it keeps changes scoped and reviewable.
|
|
7
|
+
|
|
8
|
+
Args: `path`, `old`, `new`, optional `replace_all`, optional `snippet_id`.
|
|
9
|
+
|
|
10
|
+
The file must be read first. Stale files are rejected. Repeated matches are rejected unless
|
|
11
|
+
`replace_all` is true; candidate snippets can be reused with `snippet_id`. If exact text is
|
|
12
|
+
missing, simple over-escaping may be corrected or closest-match metadata returned. Success
|
|
13
|
+
includes diff metadata.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
## modify
|
|
2
|
+
|
|
3
|
+
Create new files or edit existing files.
|
|
4
|
+
|
|
5
|
+
Use `content` only when the target file does not exist. For existing files, read the
|
|
6
|
+
file first, then use `old_string` and `new_string` for the smallest reliable
|
|
7
|
+
replacement. Do not rewrite an existing scaffolded file with full content; replace the
|
|
8
|
+
specific generated block instead.
|
|
9
|
+
|
|
10
|
+
Args for new files: `file_path`, `content`.
|
|
11
|
+
|
|
12
|
+
Args for existing files: `file_path`, `old_string`, `new_string`, optional
|
|
13
|
+
`replace_all`, optional `snippet_id`.
|
|
14
|
+
|
|
15
|
+
Existing files must be read before editing. Stale edits are rejected. Repeated matches
|
|
16
|
+
are rejected unless `replace_all` is true; candidate snippets can be reused with
|
|
17
|
+
`snippet_id`. Success includes diff metadata.
|
deepy/data/tools/read.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
## write
|
|
2
|
+
|
|
3
|
+
Create a new file or explicit whole-file replacement.
|
|
4
|
+
|
|
5
|
+
Prefer `edit` over `write` for existing targeted changes: code, tests, imports,
|
|
6
|
+
comments, blocks, or lines. Do not rewrite an existing file just because final content
|
|
7
|
+
is known; preserve surrounding user changes with `edit`.
|
|
8
|
+
|
|
9
|
+
Args: `path`, `content` (complete file content).
|
|
10
|
+
|
|
11
|
+
Existing files must be read first. If rejected for unread state, read and usually switch
|
|
12
|
+
to `edit` unless a full rewrite was requested. Stale writes are rejected. Success includes diff.
|
deepy/errors.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class ErrorDisplay:
|
|
9
|
+
category: str
|
|
10
|
+
detail: str
|
|
11
|
+
hint: str = ""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def classify_error(error: BaseException | str) -> ErrorDisplay:
|
|
15
|
+
text = str(error).strip()
|
|
16
|
+
normalized = text.casefold()
|
|
17
|
+
status = _status_code(error)
|
|
18
|
+
|
|
19
|
+
if status in {401, 403} or any(token in normalized for token in ("unauthorized", "forbidden")):
|
|
20
|
+
return ErrorDisplay(
|
|
21
|
+
category="api_auth",
|
|
22
|
+
detail=text or "API authentication failed.",
|
|
23
|
+
hint="Check the API key with `deepy config setup`.",
|
|
24
|
+
)
|
|
25
|
+
if status in {408, 429, 500, 502, 503, 504} or any(
|
|
26
|
+
token in normalized
|
|
27
|
+
for token in ("timeout", "timed out", "connection", "network", "dns", "temporarily")
|
|
28
|
+
):
|
|
29
|
+
return ErrorDisplay(
|
|
30
|
+
category="network",
|
|
31
|
+
detail=text or "Network request failed.",
|
|
32
|
+
hint="Retry later or check base_url and network connectivity.",
|
|
33
|
+
)
|
|
34
|
+
if any(token in normalized for token in ("tool", "functiontool", "max_turns", "run failed")):
|
|
35
|
+
return ErrorDisplay(
|
|
36
|
+
category="sdk_tool_failure",
|
|
37
|
+
detail=text or "Agent or tool execution failed.",
|
|
38
|
+
hint="Check the tool output above or rerun with debug logging enabled.",
|
|
39
|
+
)
|
|
40
|
+
if any(token in normalized for token in ("api_key", "api key", "config", "toml")):
|
|
41
|
+
return ErrorDisplay(
|
|
42
|
+
category="config",
|
|
43
|
+
detail=text or "Deepy configuration is incomplete.",
|
|
44
|
+
hint="Run `deepy config setup`.",
|
|
45
|
+
)
|
|
46
|
+
return ErrorDisplay(category="unknown", detail=text or error.__class__.__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def format_error_display(error: BaseException | str) -> str:
|
|
50
|
+
display = classify_error(error)
|
|
51
|
+
if display.category == "unknown" and not display.hint:
|
|
52
|
+
return display.detail
|
|
53
|
+
hint = f" Hint: {display.hint}" if display.hint else ""
|
|
54
|
+
return f"{display.category}: {display.detail}{hint}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _status_code(error: Any) -> int | None:
|
|
58
|
+
value = getattr(error, "status_code", None)
|
|
59
|
+
if isinstance(value, int):
|
|
60
|
+
return value
|
|
61
|
+
response = getattr(error, "response", None)
|
|
62
|
+
response_value = getattr(response, "status_code", None)
|
|
63
|
+
return response_value if isinstance(response_value, int) else None
|
deepy/llm/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .context import build_session_input_callback
|
|
4
|
+
from .events import DeepyStreamEvent, normalize_stream_event
|
|
5
|
+
from .thinking import build_model_settings, build_thinking_extra_body
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"DeepyStreamEvent",
|
|
9
|
+
"build_model_settings",
|
|
10
|
+
"build_session_input_callback",
|
|
11
|
+
"build_thinking_extra_body",
|
|
12
|
+
"normalize_stream_event",
|
|
13
|
+
]
|
deepy/llm/agent.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from deepy.config import Settings
|
|
6
|
+
from deepy.prompts import build_system_prompt
|
|
7
|
+
from deepy.skills import SkillInfo
|
|
8
|
+
from deepy.tools import ToolRuntime
|
|
9
|
+
from deepy.tools.agents import build_function_tools
|
|
10
|
+
|
|
11
|
+
from .provider import ProviderBundle, build_provider_bundle
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_deepy_agent(
|
|
15
|
+
settings: Settings,
|
|
16
|
+
runtime: ToolRuntime,
|
|
17
|
+
*,
|
|
18
|
+
project_root: Path,
|
|
19
|
+
provider: ProviderBundle | None = None,
|
|
20
|
+
loaded_skills: list[SkillInfo] | None = None,
|
|
21
|
+
):
|
|
22
|
+
from agents import Agent
|
|
23
|
+
|
|
24
|
+
provider = provider or build_provider_bundle(settings)
|
|
25
|
+
return Agent(
|
|
26
|
+
name="Deepy",
|
|
27
|
+
instructions=build_system_prompt(project_root, settings, loaded_skills=loaded_skills),
|
|
28
|
+
model=provider.model,
|
|
29
|
+
model_settings=provider.model_settings,
|
|
30
|
+
tools=build_function_tools(runtime),
|
|
31
|
+
)
|
deepy/llm/context.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from math import ceil
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from deepy.config import Settings
|
|
8
|
+
from deepy.utils import json as json_utils
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import tiktoken
|
|
12
|
+
except Exception: # pragma: no cover - optional dependency fallback.
|
|
13
|
+
tiktoken = None # type: ignore[assignment]
|
|
14
|
+
|
|
15
|
+
_ENCODING = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def estimate_tokens_for_text(text: str) -> int:
|
|
19
|
+
if not text:
|
|
20
|
+
return 0
|
|
21
|
+
encoding = _token_encoding()
|
|
22
|
+
if encoding is not None:
|
|
23
|
+
return max(1, len(encoding.encode(text)))
|
|
24
|
+
return max(1, ceil(len(text) / 4))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def estimate_tokens_for_item(item: Any) -> int:
|
|
28
|
+
if isinstance(item, str):
|
|
29
|
+
return estimate_tokens_for_text(item)
|
|
30
|
+
if isinstance(item, dict):
|
|
31
|
+
return estimate_tokens_for_text(json_utils.dumps(item))
|
|
32
|
+
if isinstance(item, list):
|
|
33
|
+
return sum(estimate_tokens_for_item(part) for part in item)
|
|
34
|
+
return estimate_tokens_for_text(str(item))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def estimate_tokens_for_items(items: list[dict[str, Any]]) -> int:
|
|
38
|
+
return sum(estimate_tokens_for_item(item) for item in items)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def compact_items_for_context(
|
|
42
|
+
history: list[dict[str, Any]],
|
|
43
|
+
new_input: list[dict[str, Any]],
|
|
44
|
+
*,
|
|
45
|
+
threshold_tokens: int,
|
|
46
|
+
) -> list[dict[str, Any]]:
|
|
47
|
+
combined = history + new_input
|
|
48
|
+
if estimate_tokens_for_items(combined) <= threshold_tokens:
|
|
49
|
+
return combined
|
|
50
|
+
|
|
51
|
+
recent: list[dict[str, Any]] = []
|
|
52
|
+
recent_tokens = estimate_tokens_for_items(new_input)
|
|
53
|
+
budget = max(threshold_tokens - _compact_notice_token_budget(), 1)
|
|
54
|
+
|
|
55
|
+
for item in reversed(history):
|
|
56
|
+
item_tokens = estimate_tokens_for_item(item)
|
|
57
|
+
if recent and recent_tokens + item_tokens > budget:
|
|
58
|
+
break
|
|
59
|
+
if recent_tokens + item_tokens > budget:
|
|
60
|
+
continue
|
|
61
|
+
recent.insert(0, item)
|
|
62
|
+
recent_tokens += item_tokens
|
|
63
|
+
|
|
64
|
+
omitted = len(history) - len(recent)
|
|
65
|
+
if omitted <= 0:
|
|
66
|
+
return recent + new_input
|
|
67
|
+
|
|
68
|
+
notice = {
|
|
69
|
+
"role": "system",
|
|
70
|
+
"content": (
|
|
71
|
+
"Earlier conversation history was compacted by Deepy to fit the configured context "
|
|
72
|
+
f"window. Omitted history items: {omitted}. Continue using the remaining recent "
|
|
73
|
+
"history and the current user request."
|
|
74
|
+
),
|
|
75
|
+
}
|
|
76
|
+
return [notice] + recent + new_input
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def build_session_input_callback(settings: Settings) -> Callable[
|
|
80
|
+
[list[dict[str, Any]], list[dict[str, Any]]],
|
|
81
|
+
list[dict[str, Any]],
|
|
82
|
+
]:
|
|
83
|
+
threshold = settings.context.resolved_compact_threshold
|
|
84
|
+
|
|
85
|
+
def callback(history: list[dict[str, Any]], new_input: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
86
|
+
return compact_items_for_context(
|
|
87
|
+
history,
|
|
88
|
+
new_input,
|
|
89
|
+
threshold_tokens=threshold,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return callback
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _compact_notice_token_budget() -> int:
|
|
96
|
+
return 80
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _token_encoding() -> Any | None:
|
|
100
|
+
global _ENCODING
|
|
101
|
+
if _ENCODING is not None:
|
|
102
|
+
return _ENCODING
|
|
103
|
+
if tiktoken is None:
|
|
104
|
+
return None
|
|
105
|
+
try:
|
|
106
|
+
_ENCODING = tiktoken.get_encoding("cl100k_base")
|
|
107
|
+
except Exception:
|
|
108
|
+
return None
|
|
109
|
+
return _ENCODING
|
deepy/llm/events.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
StreamKind = Literal[
|
|
7
|
+
"text_delta",
|
|
8
|
+
"reasoning_delta",
|
|
9
|
+
"reasoning_item",
|
|
10
|
+
"tool_call",
|
|
11
|
+
"tool_output",
|
|
12
|
+
"message",
|
|
13
|
+
"agent_updated",
|
|
14
|
+
"usage",
|
|
15
|
+
"raw_response",
|
|
16
|
+
"unknown",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
TEXT_DELTA_TYPES = {
|
|
20
|
+
"",
|
|
21
|
+
"response.output_text.delta",
|
|
22
|
+
"response.refusal.delta",
|
|
23
|
+
}
|
|
24
|
+
REASONING_DELTA_TYPES = {
|
|
25
|
+
"response.reasoning_summary_text.delta",
|
|
26
|
+
"response.reasoning_text.delta",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class DeepyStreamEvent:
|
|
32
|
+
kind: StreamKind
|
|
33
|
+
text: str = ""
|
|
34
|
+
name: str = ""
|
|
35
|
+
payload: dict[str, Any] = field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def normalize_stream_event(event: Any) -> DeepyStreamEvent | None:
|
|
39
|
+
event_type = getattr(event, "type", None)
|
|
40
|
+
if event_type == "raw_response_event":
|
|
41
|
+
data = getattr(event, "data", None)
|
|
42
|
+
data_type = getattr(data, "type", "")
|
|
43
|
+
delta = _raw_delta(event)
|
|
44
|
+
if data_type in REASONING_DELTA_TYPES and delta:
|
|
45
|
+
return DeepyStreamEvent(kind="reasoning_delta", text=delta, payload={"raw": data})
|
|
46
|
+
if delta and data_type in TEXT_DELTA_TYPES:
|
|
47
|
+
return DeepyStreamEvent(kind="text_delta", text=delta)
|
|
48
|
+
if delta:
|
|
49
|
+
return DeepyStreamEvent(
|
|
50
|
+
kind="raw_response",
|
|
51
|
+
name=str(data_type or ""),
|
|
52
|
+
text=delta,
|
|
53
|
+
payload={"raw": data},
|
|
54
|
+
)
|
|
55
|
+
if data_type == "response.completed":
|
|
56
|
+
usage = _response_usage(data)
|
|
57
|
+
return DeepyStreamEvent(
|
|
58
|
+
kind="usage",
|
|
59
|
+
payload={"usage": usage, "raw": data},
|
|
60
|
+
)
|
|
61
|
+
return DeepyStreamEvent(kind="raw_response", name=str(data_type or ""), payload={"raw": data})
|
|
62
|
+
|
|
63
|
+
if event_type == "agent_updated_stream_event":
|
|
64
|
+
agent = getattr(event, "new_agent", None)
|
|
65
|
+
name = getattr(agent, "name", "") if agent is not None else ""
|
|
66
|
+
return DeepyStreamEvent(kind="agent_updated", name=name, text=name)
|
|
67
|
+
|
|
68
|
+
if event_type != "run_item_stream_event":
|
|
69
|
+
return DeepyStreamEvent(kind="unknown", name=str(event_type or ""), payload={"event": event})
|
|
70
|
+
|
|
71
|
+
item = getattr(event, "item", None)
|
|
72
|
+
name = getattr(event, "name", "")
|
|
73
|
+
if name == "tool_called":
|
|
74
|
+
tool_name = _tool_name(item)
|
|
75
|
+
return DeepyStreamEvent(
|
|
76
|
+
kind="tool_call",
|
|
77
|
+
name=tool_name,
|
|
78
|
+
text=tool_name,
|
|
79
|
+
payload={"call_id": _call_id(item), "arguments": _tool_arguments(item)},
|
|
80
|
+
)
|
|
81
|
+
if name == "tool_output":
|
|
82
|
+
output = getattr(item, "output", "")
|
|
83
|
+
return DeepyStreamEvent(
|
|
84
|
+
kind="tool_output",
|
|
85
|
+
name=_tool_name(item),
|
|
86
|
+
text=output if isinstance(output, str) else str(output),
|
|
87
|
+
payload={"call_id": _call_id(item)},
|
|
88
|
+
)
|
|
89
|
+
if name == "message_output_created":
|
|
90
|
+
text = _message_text(item)
|
|
91
|
+
return DeepyStreamEvent(kind="message", text=text)
|
|
92
|
+
if name == "reasoning_item_created":
|
|
93
|
+
return DeepyStreamEvent(kind="reasoning_item", payload={"item": item})
|
|
94
|
+
|
|
95
|
+
return DeepyStreamEvent(kind="unknown", name=str(name), payload={"item": item})
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _raw_delta(event: Any) -> str:
|
|
99
|
+
data = getattr(event, "data", None)
|
|
100
|
+
delta = getattr(data, "delta", None)
|
|
101
|
+
if isinstance(delta, str):
|
|
102
|
+
return delta
|
|
103
|
+
return ""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _response_usage(data: Any) -> Any:
|
|
107
|
+
response = getattr(data, "response", None)
|
|
108
|
+
usage = getattr(response, "usage", None)
|
|
109
|
+
return _to_payload(usage)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _to_payload(value: Any) -> Any:
|
|
113
|
+
if value is None or isinstance(value, (str, int, float, bool, list, dict)):
|
|
114
|
+
return value
|
|
115
|
+
model_dump = getattr(value, "model_dump", None)
|
|
116
|
+
if callable(model_dump):
|
|
117
|
+
return model_dump()
|
|
118
|
+
if hasattr(value, "__dict__"):
|
|
119
|
+
return dict(value.__dict__)
|
|
120
|
+
return value
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _tool_name(item: Any) -> str:
|
|
124
|
+
if item is None:
|
|
125
|
+
return ""
|
|
126
|
+
tool_name = getattr(item, "tool_name", None)
|
|
127
|
+
if isinstance(tool_name, str) and tool_name:
|
|
128
|
+
return tool_name
|
|
129
|
+
raw_item = getattr(item, "raw_item", None)
|
|
130
|
+
if isinstance(raw_item, dict):
|
|
131
|
+
value = raw_item.get("name")
|
|
132
|
+
return value if isinstance(value, str) else ""
|
|
133
|
+
value = getattr(raw_item, "name", "")
|
|
134
|
+
return value if isinstance(value, str) else ""
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _tool_arguments(item: Any) -> str:
|
|
138
|
+
if item is None:
|
|
139
|
+
return ""
|
|
140
|
+
arguments = getattr(item, "arguments", None)
|
|
141
|
+
if isinstance(arguments, str):
|
|
142
|
+
return arguments
|
|
143
|
+
raw_item = getattr(item, "raw_item", None)
|
|
144
|
+
if isinstance(raw_item, dict):
|
|
145
|
+
value = raw_item.get("arguments")
|
|
146
|
+
if isinstance(value, str):
|
|
147
|
+
return value
|
|
148
|
+
function = raw_item.get("function")
|
|
149
|
+
if isinstance(function, dict):
|
|
150
|
+
value = function.get("arguments")
|
|
151
|
+
return value if isinstance(value, str) else ""
|
|
152
|
+
return ""
|
|
153
|
+
value = getattr(raw_item, "arguments", None)
|
|
154
|
+
if isinstance(value, str):
|
|
155
|
+
return value
|
|
156
|
+
function = getattr(raw_item, "function", None)
|
|
157
|
+
value = getattr(function, "arguments", None)
|
|
158
|
+
return value if isinstance(value, str) else ""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _call_id(item: Any) -> str:
|
|
162
|
+
if item is None:
|
|
163
|
+
return ""
|
|
164
|
+
call_id = getattr(item, "call_id", None)
|
|
165
|
+
if isinstance(call_id, str):
|
|
166
|
+
return call_id
|
|
167
|
+
raw_item = getattr(item, "raw_item", None)
|
|
168
|
+
if isinstance(raw_item, dict):
|
|
169
|
+
value = raw_item.get("call_id") or raw_item.get("id")
|
|
170
|
+
return str(value) if value is not None else ""
|
|
171
|
+
value = getattr(raw_item, "call_id", None) or getattr(raw_item, "id", None)
|
|
172
|
+
return str(value) if value is not None else ""
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _message_text(item: Any) -> str:
|
|
176
|
+
raw_item = getattr(item, "raw_item", None)
|
|
177
|
+
content = getattr(raw_item, "content", None)
|
|
178
|
+
if isinstance(content, str):
|
|
179
|
+
return content
|
|
180
|
+
if isinstance(content, list):
|
|
181
|
+
parts: list[str] = []
|
|
182
|
+
for part in content:
|
|
183
|
+
text = part.get("text") if isinstance(part, dict) else getattr(part, "text", "")
|
|
184
|
+
if isinstance(text, str):
|
|
185
|
+
parts.append(text)
|
|
186
|
+
return "".join(parts)
|
|
187
|
+
return ""
|
deepy/llm/provider.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from agents import OpenAIChatCompletionsModel
|
|
7
|
+
|
|
8
|
+
from deepy.config import Settings
|
|
9
|
+
|
|
10
|
+
from .replay import (
|
|
11
|
+
sanitize_chat_completion_stream_event,
|
|
12
|
+
sanitize_model_input_for_chat_completions,
|
|
13
|
+
sanitize_model_response_output,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ProviderBundle:
|
|
19
|
+
client: object
|
|
20
|
+
model: object
|
|
21
|
+
model_settings: object
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DeepyOpenAIChatCompletionsModel(OpenAIChatCompletionsModel):
|
|
25
|
+
async def get_response(self, *args: Any, **kwargs: Any) -> Any:
|
|
26
|
+
response = await super().get_response(*args, **kwargs)
|
|
27
|
+
response.output = sanitize_model_response_output(response.output)
|
|
28
|
+
return response
|
|
29
|
+
|
|
30
|
+
async def stream_response(self, *args: Any, **kwargs: Any) -> Any:
|
|
31
|
+
async for event in super().stream_response(*args, **kwargs):
|
|
32
|
+
sanitized = sanitize_chat_completion_stream_event(event)
|
|
33
|
+
if sanitized is not None:
|
|
34
|
+
yield sanitized
|
|
35
|
+
|
|
36
|
+
async def _fetch_response(
|
|
37
|
+
self,
|
|
38
|
+
system_instructions: str | None,
|
|
39
|
+
input: Any,
|
|
40
|
+
*args: Any,
|
|
41
|
+
**kwargs: Any,
|
|
42
|
+
) -> Any:
|
|
43
|
+
return await super()._fetch_response(
|
|
44
|
+
system_instructions,
|
|
45
|
+
sanitize_model_input_for_chat_completions(input),
|
|
46
|
+
*args,
|
|
47
|
+
**kwargs,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def should_replay_deepseek_reasoning_content(context: object) -> bool:
|
|
52
|
+
model = str(getattr(context, "model", "")).lower()
|
|
53
|
+
if "deepseek" not in model:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
reasoning = getattr(context, "reasoning", None)
|
|
57
|
+
origin_model = getattr(reasoning, "origin_model", None)
|
|
58
|
+
provider_data = getattr(reasoning, "provider_data", {}) or {}
|
|
59
|
+
return (
|
|
60
|
+
isinstance(origin_model, str)
|
|
61
|
+
and "deepseek" in origin_model.lower()
|
|
62
|
+
) or provider_data == {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def build_provider_bundle(settings: Settings) -> ProviderBundle:
|
|
66
|
+
from agents import set_tracing_disabled
|
|
67
|
+
from openai import AsyncOpenAI
|
|
68
|
+
|
|
69
|
+
from .thinking import build_model_settings
|
|
70
|
+
|
|
71
|
+
if not settings.model.api_key:
|
|
72
|
+
raise ValueError(f"DeepSeek API key is missing in {settings.path or 'Deepy config'}.")
|
|
73
|
+
|
|
74
|
+
set_tracing_disabled(disabled=True)
|
|
75
|
+
client = AsyncOpenAI(base_url=settings.model.base_url, api_key=settings.model.api_key)
|
|
76
|
+
model = DeepyOpenAIChatCompletionsModel(
|
|
77
|
+
model=settings.model.name,
|
|
78
|
+
openai_client=client,
|
|
79
|
+
should_replay_reasoning_content=should_replay_deepseek_reasoning_content,
|
|
80
|
+
)
|
|
81
|
+
return ProviderBundle(client=client, model=model, model_settings=build_model_settings(settings))
|