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
deepy/__init__.py
ADDED
deepy/__main__.py
ADDED
deepy/cli.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Sequence
|
|
9
|
+
|
|
10
|
+
import tomli_w
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .config import Settings, load_settings, settings_to_toml_dict
|
|
14
|
+
from .config.settings import DEFAULT_BASE_URL, DEFAULT_MODEL
|
|
15
|
+
from .errors import format_error_display
|
|
16
|
+
from .llm.provider import build_provider_bundle
|
|
17
|
+
from .llm.runner import DEFAULT_MAX_TURNS, run_prompt_once
|
|
18
|
+
from .sessions import DeepyJsonlSession, list_session_entries
|
|
19
|
+
from .skills import discover_skills, find_skill, format_skills_for_terminal, read_skill_body
|
|
20
|
+
from .status import build_status_report, format_status_report, status_report_to_dict
|
|
21
|
+
from .usage import TokenUsage, format_usage_line, usage_from_run_result
|
|
22
|
+
from .ui import run_interactive
|
|
23
|
+
from .utils import json as json_utils
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
27
|
+
parser = argparse.ArgumentParser(
|
|
28
|
+
prog="deepy",
|
|
29
|
+
description="Deepy - Vibe coding for DeepSeek models in your terminal.",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument("--version", action="version", version=f"Deepy {__version__}")
|
|
32
|
+
parser.add_argument("--config", type=Path, help="Path to config.toml.")
|
|
33
|
+
|
|
34
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
35
|
+
|
|
36
|
+
config_parser = subparsers.add_parser("config", help="Inspect local Deepy config.")
|
|
37
|
+
config_sub = config_parser.add_subparsers(dest="config_command", required=True)
|
|
38
|
+
show_parser = config_sub.add_parser("show", help="Print resolved TOML config.")
|
|
39
|
+
show_parser.add_argument("--show-secret", action="store_true", help="Show API key.")
|
|
40
|
+
show_parser.add_argument("--json", action="store_true", help="Print JSON instead of TOML.")
|
|
41
|
+
init_parser = config_sub.add_parser("init", help="Create a TOML config file.")
|
|
42
|
+
init_parser.add_argument("--api-key", help="DeepSeek API key.")
|
|
43
|
+
init_parser.add_argument("--model", default=DEFAULT_MODEL, help="Model name.")
|
|
44
|
+
init_parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="OpenAI-compatible base URL.")
|
|
45
|
+
init_parser.add_argument("--force", action="store_true", help="Overwrite existing config.")
|
|
46
|
+
setup_parser = config_sub.add_parser("setup", help="Interactively configure Deepy.")
|
|
47
|
+
setup_parser.add_argument("--force", action="store_true", help="Overwrite existing config.")
|
|
48
|
+
|
|
49
|
+
doctor_parser = subparsers.add_parser("doctor", help="Validate local Deepy setup.")
|
|
50
|
+
doctor_parser.add_argument("--json", action="store_true", help="Print JSON diagnostics.")
|
|
51
|
+
doctor_parser.add_argument("--live", action="store_true", help="Send one minimal live DeepSeek request.")
|
|
52
|
+
|
|
53
|
+
run_parser = subparsers.add_parser("run", help="Run a single non-interactive prompt.")
|
|
54
|
+
run_parser.add_argument("prompt", nargs="+", help="Prompt text to send to Deepy.")
|
|
55
|
+
run_parser.add_argument(
|
|
56
|
+
"--max-turns",
|
|
57
|
+
type=int,
|
|
58
|
+
default=DEFAULT_MAX_TURNS,
|
|
59
|
+
help="Maximum agent turns.",
|
|
60
|
+
)
|
|
61
|
+
run_parser.add_argument("--session", help="Resume an existing session id.")
|
|
62
|
+
run_parser.add_argument("--skill", action="append", default=[], help="Load a skill by name.")
|
|
63
|
+
|
|
64
|
+
sessions_parser = subparsers.add_parser("sessions", help="Inspect project sessions.")
|
|
65
|
+
sessions_sub = sessions_parser.add_subparsers(dest="sessions_command", required=True)
|
|
66
|
+
sessions_sub.add_parser("list", help="List sessions for the current project.")
|
|
67
|
+
sessions_show = sessions_sub.add_parser("show", help="Print session items as JSON.")
|
|
68
|
+
sessions_show.add_argument("session_id", help="Session id.")
|
|
69
|
+
|
|
70
|
+
skills_parser = subparsers.add_parser("skills", help="Inspect available skills.")
|
|
71
|
+
skills_sub = skills_parser.add_subparsers(dest="skills_command", required=True)
|
|
72
|
+
skills_sub.add_parser("list", help="List user and project skills.")
|
|
73
|
+
skills_show = skills_sub.add_parser("show", help="Print a skill document.")
|
|
74
|
+
skills_show.add_argument("name", help="Skill name.")
|
|
75
|
+
|
|
76
|
+
status_parser = subparsers.add_parser("status", help="Print current Deepy project status.")
|
|
77
|
+
status_parser.add_argument("--json", action="store_true", help="Print JSON status.")
|
|
78
|
+
|
|
79
|
+
return parser
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _cmd_config_show(args: argparse.Namespace) -> int:
|
|
83
|
+
settings = load_settings(args.config)
|
|
84
|
+
data = settings_to_toml_dict(settings, reveal_secret=args.show_secret)
|
|
85
|
+
if args.json:
|
|
86
|
+
print(json_utils.dumps_pretty(data))
|
|
87
|
+
else:
|
|
88
|
+
print(tomli_w.dumps(data), end="")
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _cmd_config_init(args: argparse.Namespace) -> int:
|
|
93
|
+
config_path = args.config.expanduser() if args.config else Path.home() / ".deepy" / "config.toml"
|
|
94
|
+
if config_path.suffix == ".json":
|
|
95
|
+
raise ValueError("Deepy only supports TOML config files; JSON config is not supported.")
|
|
96
|
+
if config_path.exists() and not args.force:
|
|
97
|
+
print(f"Config already exists: {config_path}", file=sys.stderr)
|
|
98
|
+
return 1
|
|
99
|
+
_write_config(
|
|
100
|
+
config_path,
|
|
101
|
+
api_key=args.api_key or "",
|
|
102
|
+
model=args.model,
|
|
103
|
+
base_url=args.base_url,
|
|
104
|
+
)
|
|
105
|
+
print(f"Wrote {config_path}")
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _cmd_config_setup(args: argparse.Namespace) -> int:
|
|
110
|
+
config_path = args.config.expanduser() if args.config else Path.home() / ".deepy" / "config.toml"
|
|
111
|
+
if config_path.suffix == ".json":
|
|
112
|
+
raise ValueError("Deepy only supports TOML config files; JSON config is not supported.")
|
|
113
|
+
if config_path.exists() and not args.force:
|
|
114
|
+
existing = load_settings(config_path)
|
|
115
|
+
else:
|
|
116
|
+
existing = Settings(path=config_path)
|
|
117
|
+
|
|
118
|
+
print("DeepSeek API keys: https://platform.deepseek.com/api_keys")
|
|
119
|
+
api_key = _prompt_config_value("API key", default=existing.model.api_key or "", is_password=True)
|
|
120
|
+
model = _prompt_config_value("Model", default=existing.model.name)
|
|
121
|
+
base_url = _prompt_config_value("Base URL", default=existing.model.base_url)
|
|
122
|
+
_write_config(config_path, api_key=api_key, model=model, base_url=base_url)
|
|
123
|
+
print(f"Wrote {config_path}")
|
|
124
|
+
return 0
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _prompt_config_value(label: str, *, default: str, is_password: bool = False) -> str:
|
|
128
|
+
from prompt_toolkit import PromptSession
|
|
129
|
+
|
|
130
|
+
prompt = f"{label}"
|
|
131
|
+
if default and not is_password:
|
|
132
|
+
prompt += f" [{default}]"
|
|
133
|
+
prompt += ": "
|
|
134
|
+
value = PromptSession().prompt(prompt, default="" if is_password else default, is_password=is_password)
|
|
135
|
+
value = value.strip()
|
|
136
|
+
return value or default
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _write_config(config_path: Path, *, api_key: str, model: str, base_url: str) -> None:
|
|
140
|
+
payload = {
|
|
141
|
+
"model": {
|
|
142
|
+
"name": model,
|
|
143
|
+
"base_url": base_url,
|
|
144
|
+
"api_key": api_key,
|
|
145
|
+
"thinking": True,
|
|
146
|
+
"reasoning_effort": "max",
|
|
147
|
+
},
|
|
148
|
+
"context": {
|
|
149
|
+
"window_tokens": 1_048_576,
|
|
150
|
+
"compact_trigger_ratio": 0.8,
|
|
151
|
+
"compact_prompt_token_threshold": 838_861,
|
|
152
|
+
},
|
|
153
|
+
"logging": {
|
|
154
|
+
"debug": False,
|
|
155
|
+
},
|
|
156
|
+
"notify": {
|
|
157
|
+
"enabled": False,
|
|
158
|
+
"command": "",
|
|
159
|
+
},
|
|
160
|
+
"tools": {
|
|
161
|
+
"web_search": {
|
|
162
|
+
"command": "",
|
|
163
|
+
"api_url": "",
|
|
164
|
+
"machine_id": "",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
config_path.write_text(tomli_w.dumps(payload), encoding="utf-8")
|
|
170
|
+
os.chmod(config_path, 0o600)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _doctor(args: argparse.Namespace) -> tuple[int, dict[str, object]]:
|
|
174
|
+
settings = load_settings(args.config)
|
|
175
|
+
checks: list[dict[str, object]] = []
|
|
176
|
+
|
|
177
|
+
def check(name: str, ok: bool, detail: str) -> None:
|
|
178
|
+
checks.append({"name": name, "ok": ok, "detail": detail})
|
|
179
|
+
|
|
180
|
+
check("config", True, str(settings.path))
|
|
181
|
+
check("config_permissions", *_config_permissions_check(settings.path))
|
|
182
|
+
check(
|
|
183
|
+
"api_key",
|
|
184
|
+
bool(settings.model.api_key),
|
|
185
|
+
"configured" if settings.model.api_key else "missing; run `deepy config setup`",
|
|
186
|
+
)
|
|
187
|
+
check("model", bool(settings.model.name), settings.model.name)
|
|
188
|
+
check("base_url", bool(settings.model.base_url), settings.model.base_url)
|
|
189
|
+
check(
|
|
190
|
+
"context_window",
|
|
191
|
+
settings.context.window_tokens >= 1_000_000,
|
|
192
|
+
str(settings.context.window_tokens),
|
|
193
|
+
)
|
|
194
|
+
check(
|
|
195
|
+
"compact_threshold",
|
|
196
|
+
settings.context.resolved_compact_threshold
|
|
197
|
+
== int(settings.context.window_tokens * 0.8 + 0.999999)
|
|
198
|
+
or settings.context.resolved_compact_threshold > 0,
|
|
199
|
+
str(settings.context.resolved_compact_threshold),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
build_provider_bundle(settings)
|
|
204
|
+
except Exception as exc:
|
|
205
|
+
check("openai_agents_provider", False, str(exc))
|
|
206
|
+
else:
|
|
207
|
+
check("openai_agents_provider", True, "OpenAIChatCompletionsModel ready")
|
|
208
|
+
|
|
209
|
+
ok = all(bool(item["ok"]) for item in checks)
|
|
210
|
+
return 0 if ok else 1, {
|
|
211
|
+
"ok": ok,
|
|
212
|
+
"checks": checks,
|
|
213
|
+
"thinking": {
|
|
214
|
+
"enabled": settings.model.thinking_enabled,
|
|
215
|
+
"reasoning_effort": settings.model.reasoning_effort,
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
async def _doctor_live(settings: Settings) -> dict[str, object]:
|
|
221
|
+
from agents import Agent, RunConfig, Runner
|
|
222
|
+
|
|
223
|
+
provider = build_provider_bundle(settings)
|
|
224
|
+
agent = Agent(
|
|
225
|
+
name="Deepy Doctor",
|
|
226
|
+
instructions="Reply with OK.",
|
|
227
|
+
model=provider.model,
|
|
228
|
+
model_settings=provider.model_settings,
|
|
229
|
+
tools=[],
|
|
230
|
+
)
|
|
231
|
+
result = await Runner.run(
|
|
232
|
+
agent,
|
|
233
|
+
"Reply with OK.",
|
|
234
|
+
max_turns=1,
|
|
235
|
+
run_config=RunConfig(
|
|
236
|
+
workflow_name="Deepy Doctor",
|
|
237
|
+
trace_include_sensitive_data=False,
|
|
238
|
+
reasoning_item_id_policy="omit",
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
output = getattr(result, "final_output", "")
|
|
242
|
+
usage = usage_from_run_result(result)
|
|
243
|
+
return {
|
|
244
|
+
"ok": True,
|
|
245
|
+
"model": settings.model.name,
|
|
246
|
+
"base_url": settings.model.base_url,
|
|
247
|
+
"api_key": "configured",
|
|
248
|
+
"response_summary": str(output).strip()[:200],
|
|
249
|
+
"usage": usage.to_dict(),
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _config_permissions_check(path: Path | None) -> tuple[bool, str]:
|
|
254
|
+
if path is None or not path.exists():
|
|
255
|
+
return False, "missing"
|
|
256
|
+
mode = path.stat().st_mode & 0o777
|
|
257
|
+
if mode & 0o077:
|
|
258
|
+
return False, f"{mode:o}; expected private permissions like 600"
|
|
259
|
+
return True, f"{mode:o}"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _cmd_doctor(args: argparse.Namespace) -> int:
|
|
263
|
+
code, report = _doctor(args)
|
|
264
|
+
if args.live:
|
|
265
|
+
settings = load_settings(args.config)
|
|
266
|
+
if code != 0:
|
|
267
|
+
report["live"] = {"ok": False, "error": "local doctor checks failed"}
|
|
268
|
+
else:
|
|
269
|
+
try:
|
|
270
|
+
report["live"] = asyncio.run(_doctor_live(settings))
|
|
271
|
+
except Exception as exc:
|
|
272
|
+
report["live"] = {"ok": False, "error": format_error_display(exc)}
|
|
273
|
+
code = 1
|
|
274
|
+
if args.json:
|
|
275
|
+
print(json_utils.dumps_pretty(report))
|
|
276
|
+
return code
|
|
277
|
+
for item in report["checks"]:
|
|
278
|
+
status = "ok" if item["ok"] else "fail"
|
|
279
|
+
print(f"{status:4} {item['name']}: {item['detail']}")
|
|
280
|
+
thinking = report["thinking"]
|
|
281
|
+
print(f"info thinking: enabled={thinking['enabled']} effort={thinking['reasoning_effort']}")
|
|
282
|
+
live = report.get("live")
|
|
283
|
+
if isinstance(live, dict):
|
|
284
|
+
if live.get("ok"):
|
|
285
|
+
usage = live.get("usage")
|
|
286
|
+
print(
|
|
287
|
+
"ok live: "
|
|
288
|
+
f"model={live.get('model')} base_url={live.get('base_url')} "
|
|
289
|
+
f"response={live.get('response_summary')!r} "
|
|
290
|
+
f"{format_usage_line(usage if isinstance(usage, dict) else TokenUsage())}"
|
|
291
|
+
)
|
|
292
|
+
else:
|
|
293
|
+
print(f"fail live: {live.get('error')}")
|
|
294
|
+
return code
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _cmd_run(args: argparse.Namespace) -> int:
|
|
298
|
+
settings = load_settings(args.config)
|
|
299
|
+
prompt = " ".join(args.prompt)
|
|
300
|
+
|
|
301
|
+
def emit(delta: str) -> None:
|
|
302
|
+
print(delta, end="", flush=True)
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
summary = asyncio.run(
|
|
306
|
+
run_prompt_once(
|
|
307
|
+
prompt,
|
|
308
|
+
settings=settings,
|
|
309
|
+
emit=emit,
|
|
310
|
+
max_turns=args.max_turns,
|
|
311
|
+
session_id=args.session,
|
|
312
|
+
skill_names=args.skill,
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
except Exception as exc:
|
|
316
|
+
print(f"deepy run failed: {format_error_display(exc)}", file=sys.stderr)
|
|
317
|
+
return 1
|
|
318
|
+
if summary.output and not summary.output.endswith("\n"):
|
|
319
|
+
print()
|
|
320
|
+
return 0 if summary.complete else 1
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _cmd_sessions(args: argparse.Namespace) -> int:
|
|
324
|
+
if args.sessions_command == "list":
|
|
325
|
+
entries = list_session_entries(Path.cwd())
|
|
326
|
+
if not entries:
|
|
327
|
+
print("No sessions found.")
|
|
328
|
+
return 0
|
|
329
|
+
for entry in entries:
|
|
330
|
+
print(
|
|
331
|
+
f"{entry.id}\tupdated={entry.updated_at}\thistory_tokens={entry.active_tokens}\t"
|
|
332
|
+
f"{format_usage_line(entry.usage)}"
|
|
333
|
+
)
|
|
334
|
+
return 0
|
|
335
|
+
if args.sessions_command == "show":
|
|
336
|
+
session = DeepyJsonlSession.open(Path.cwd(), args.session_id)
|
|
337
|
+
items = asyncio.run(session.get_items())
|
|
338
|
+
entry = next(
|
|
339
|
+
(item for item in list_session_entries(Path.cwd()) if item.id == args.session_id),
|
|
340
|
+
None,
|
|
341
|
+
)
|
|
342
|
+
print(
|
|
343
|
+
json_utils.dumps_pretty(
|
|
344
|
+
{
|
|
345
|
+
"session_id": args.session_id,
|
|
346
|
+
"usage": entry.usage if entry is not None else None,
|
|
347
|
+
"items": items,
|
|
348
|
+
}
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
return 0
|
|
352
|
+
return 1
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _cmd_skills(args: argparse.Namespace) -> int:
|
|
356
|
+
if args.skills_command == "list":
|
|
357
|
+
print(format_skills_for_terminal(discover_skills(Path.cwd())))
|
|
358
|
+
return 0
|
|
359
|
+
if args.skills_command == "show":
|
|
360
|
+
skill = find_skill(Path.cwd(), args.name)
|
|
361
|
+
if skill is None:
|
|
362
|
+
print(f"Skill not found: {args.name}", file=sys.stderr)
|
|
363
|
+
return 1
|
|
364
|
+
print(read_skill_body(skill))
|
|
365
|
+
return 0
|
|
366
|
+
return 1
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _cmd_status(args: argparse.Namespace) -> int:
|
|
370
|
+
settings = load_settings(args.config)
|
|
371
|
+
report = build_status_report(Path.cwd(), settings)
|
|
372
|
+
if args.json:
|
|
373
|
+
print(json_utils.dumps_pretty(status_report_to_dict(report)))
|
|
374
|
+
else:
|
|
375
|
+
print(format_status_report(report))
|
|
376
|
+
return 0
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
380
|
+
parser = _build_parser()
|
|
381
|
+
args = parser.parse_args(argv)
|
|
382
|
+
|
|
383
|
+
if args.command == "config":
|
|
384
|
+
if args.config_command == "show":
|
|
385
|
+
return _cmd_config_show(args)
|
|
386
|
+
if args.config_command == "init":
|
|
387
|
+
return _cmd_config_init(args)
|
|
388
|
+
if args.config_command == "setup":
|
|
389
|
+
return _cmd_config_setup(args)
|
|
390
|
+
if args.command == "doctor":
|
|
391
|
+
return _cmd_doctor(args)
|
|
392
|
+
if args.command == "run":
|
|
393
|
+
return _cmd_run(args)
|
|
394
|
+
if args.command == "sessions":
|
|
395
|
+
return _cmd_sessions(args)
|
|
396
|
+
if args.command == "skills":
|
|
397
|
+
return _cmd_skills(args)
|
|
398
|
+
if args.command == "status":
|
|
399
|
+
return _cmd_status(args)
|
|
400
|
+
|
|
401
|
+
if not sys.stdin.isatty():
|
|
402
|
+
parser.error("interactive mode requires a TTY; use `deepy doctor` or `deepy config show`.")
|
|
403
|
+
settings = load_settings(args.config)
|
|
404
|
+
if not settings.model.api_key:
|
|
405
|
+
print("Deepy needs a DeepSeek API key before starting interactive mode.")
|
|
406
|
+
setup_args = argparse.Namespace(config=args.config, force=True)
|
|
407
|
+
_cmd_config_setup(setup_args)
|
|
408
|
+
settings = load_settings(args.config)
|
|
409
|
+
return run_interactive(settings)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
if __name__ == "__main__":
|
|
413
|
+
raise SystemExit(main())
|
deepy/config/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .settings import (
|
|
4
|
+
ContextConfig,
|
|
5
|
+
ModelConfig,
|
|
6
|
+
Settings,
|
|
7
|
+
default_config_path,
|
|
8
|
+
load_settings,
|
|
9
|
+
mask_secret,
|
|
10
|
+
settings_to_toml_dict,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"ContextConfig",
|
|
15
|
+
"ModelConfig",
|
|
16
|
+
"Settings",
|
|
17
|
+
"default_config_path",
|
|
18
|
+
"load_settings",
|
|
19
|
+
"mask_secret",
|
|
20
|
+
"settings_to_toml_dict",
|
|
21
|
+
]
|
deepy/config/settings.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tomllib
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Mapping, Self
|
|
8
|
+
|
|
9
|
+
DEFAULT_MODEL = "deepseek-v4-pro"
|
|
10
|
+
DEFAULT_BASE_URL = "https://api.deepseek.com"
|
|
11
|
+
DEFAULT_CONTEXT_WINDOW_TOKENS = 1_048_576
|
|
12
|
+
DEFAULT_COMPACT_TRIGGER_RATIO = 0.8
|
|
13
|
+
DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 838_861
|
|
14
|
+
REASONING_EFFORTS = {"high", "max"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def default_config_path() -> Path:
|
|
18
|
+
return Path.home() / ".deepy" / "config.toml"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def mask_secret(value: str | None) -> str:
|
|
22
|
+
if not value:
|
|
23
|
+
return ""
|
|
24
|
+
if len(value) <= 8:
|
|
25
|
+
return "***"
|
|
26
|
+
return f"{value[:4]}...{value[-4:]}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _as_mapping(value: Any) -> Mapping[str, Any]:
|
|
30
|
+
return value if isinstance(value, Mapping) else {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _as_bool(value: Any, default: bool) -> bool:
|
|
34
|
+
return value if isinstance(value, bool) else default
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _as_int(value: Any, default: int) -> int:
|
|
38
|
+
if isinstance(value, bool):
|
|
39
|
+
return default
|
|
40
|
+
if isinstance(value, int) and value > 0:
|
|
41
|
+
return value
|
|
42
|
+
return default
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _as_float(value: Any, default: float) -> float:
|
|
46
|
+
if isinstance(value, bool):
|
|
47
|
+
return default
|
|
48
|
+
if isinstance(value, int | float) and value > 0:
|
|
49
|
+
return float(value)
|
|
50
|
+
return default
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _as_str(value: Any, default: str = "") -> str:
|
|
54
|
+
return value.strip() if isinstance(value, str) and value.strip() else default
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class ModelConfig:
|
|
59
|
+
name: str = DEFAULT_MODEL
|
|
60
|
+
base_url: str = DEFAULT_BASE_URL
|
|
61
|
+
api_key: str | None = None
|
|
62
|
+
thinking: bool | None = None
|
|
63
|
+
reasoning_effort: str = "max"
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_mapping(cls, raw: Mapping[str, Any], env: Mapping[str, str] | None = None) -> Self:
|
|
67
|
+
env = env or {}
|
|
68
|
+
name = _as_str(env.get("DEEPY_MODEL"), _as_str(raw.get("name"), DEFAULT_MODEL))
|
|
69
|
+
base_url = _as_str(
|
|
70
|
+
env.get("DEEPY_BASE_URL"),
|
|
71
|
+
_as_str(raw.get("base_url"), DEFAULT_BASE_URL),
|
|
72
|
+
)
|
|
73
|
+
api_key = _as_str(env.get("DEEPY_API_KEY"), _as_str(raw.get("api_key"), "")) or None
|
|
74
|
+
effort = _as_str(raw.get("reasoning_effort"), "max")
|
|
75
|
+
if effort not in REASONING_EFFORTS:
|
|
76
|
+
effort = "max"
|
|
77
|
+
|
|
78
|
+
thinking_value = raw.get("thinking")
|
|
79
|
+
thinking = thinking_value if isinstance(thinking_value, bool) else None
|
|
80
|
+
|
|
81
|
+
return cls(
|
|
82
|
+
name=name,
|
|
83
|
+
base_url=base_url,
|
|
84
|
+
api_key=api_key,
|
|
85
|
+
thinking=thinking,
|
|
86
|
+
reasoning_effort=effort,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def thinking_enabled(self) -> bool:
|
|
91
|
+
if self.thinking is not None:
|
|
92
|
+
return self.thinking
|
|
93
|
+
return self.name.lower() in {"deepseek-v4-pro", "deepseek-v4-flash"}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class ContextConfig:
|
|
98
|
+
window_tokens: int = DEFAULT_CONTEXT_WINDOW_TOKENS
|
|
99
|
+
compact_trigger_ratio: float = DEFAULT_COMPACT_TRIGGER_RATIO
|
|
100
|
+
compact_prompt_token_threshold: int | None = None
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_mapping(cls, raw: Mapping[str, Any]) -> Self:
|
|
104
|
+
window_tokens = _as_int(raw.get("window_tokens"), DEFAULT_CONTEXT_WINDOW_TOKENS)
|
|
105
|
+
ratio = _as_float(raw.get("compact_trigger_ratio"), DEFAULT_COMPACT_TRIGGER_RATIO)
|
|
106
|
+
if ratio <= 0 or ratio > 1:
|
|
107
|
+
ratio = DEFAULT_COMPACT_TRIGGER_RATIO
|
|
108
|
+
threshold = raw.get("compact_prompt_token_threshold")
|
|
109
|
+
compact_threshold = _as_int(threshold, 0) if threshold is not None else None
|
|
110
|
+
return cls(
|
|
111
|
+
window_tokens=window_tokens,
|
|
112
|
+
compact_trigger_ratio=ratio,
|
|
113
|
+
compact_prompt_token_threshold=compact_threshold or None,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def resolved_compact_threshold(self) -> int:
|
|
118
|
+
if self.compact_prompt_token_threshold:
|
|
119
|
+
return self.compact_prompt_token_threshold
|
|
120
|
+
return int(self.window_tokens * self.compact_trigger_ratio + 0.999999)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class LoggingConfig:
|
|
125
|
+
debug: bool = False
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def from_mapping(cls, raw: Mapping[str, Any]) -> Self:
|
|
129
|
+
return cls(debug=_as_bool(raw.get("debug"), False))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(frozen=True)
|
|
133
|
+
class NotifyConfig:
|
|
134
|
+
enabled: bool = False
|
|
135
|
+
command: str | None = None
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def from_mapping(cls, raw: Mapping[str, Any]) -> Self:
|
|
139
|
+
command = _as_str(raw.get("command")) or None
|
|
140
|
+
return cls(enabled=_as_bool(raw.get("enabled"), bool(command)), command=command)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass(frozen=True)
|
|
144
|
+
class WebSearchToolConfig:
|
|
145
|
+
command: str | None = None
|
|
146
|
+
api_url: str | None = None
|
|
147
|
+
machine_id: str | None = None
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def from_mapping(cls, raw: Mapping[str, Any]) -> Self:
|
|
151
|
+
return cls(
|
|
152
|
+
command=_as_str(raw.get("command")) or None,
|
|
153
|
+
api_url=_as_str(raw.get("api_url")) or None,
|
|
154
|
+
machine_id=_as_str(raw.get("machine_id")) or None,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass(frozen=True)
|
|
159
|
+
class ToolsConfig:
|
|
160
|
+
web_search: WebSearchToolConfig = field(default_factory=WebSearchToolConfig)
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def from_mapping(cls, raw: Mapping[str, Any]) -> Self:
|
|
164
|
+
return cls(web_search=WebSearchToolConfig.from_mapping(_as_mapping(raw.get("web_search"))))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(frozen=True)
|
|
168
|
+
class Settings:
|
|
169
|
+
model: ModelConfig = field(default_factory=ModelConfig)
|
|
170
|
+
context: ContextConfig = field(default_factory=ContextConfig)
|
|
171
|
+
logging: LoggingConfig = field(default_factory=LoggingConfig)
|
|
172
|
+
notify: NotifyConfig = field(default_factory=NotifyConfig)
|
|
173
|
+
tools: ToolsConfig = field(default_factory=ToolsConfig)
|
|
174
|
+
path: Path | None = None
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def from_mapping(
|
|
178
|
+
cls,
|
|
179
|
+
raw: Mapping[str, Any],
|
|
180
|
+
*,
|
|
181
|
+
path: Path | None = None,
|
|
182
|
+
env: Mapping[str, str] | None = None,
|
|
183
|
+
) -> Self:
|
|
184
|
+
return cls(
|
|
185
|
+
model=ModelConfig.from_mapping(_as_mapping(raw.get("model")), env=env),
|
|
186
|
+
context=ContextConfig.from_mapping(_as_mapping(raw.get("context"))),
|
|
187
|
+
logging=LoggingConfig.from_mapping(_as_mapping(raw.get("logging"))),
|
|
188
|
+
notify=NotifyConfig.from_mapping(_as_mapping(raw.get("notify"))),
|
|
189
|
+
tools=ToolsConfig.from_mapping(_as_mapping(raw.get("tools"))),
|
|
190
|
+
path=path,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def load_settings(
|
|
195
|
+
path: str | os.PathLike[str] | None = None,
|
|
196
|
+
*,
|
|
197
|
+
env: Mapping[str, str] | None = None,
|
|
198
|
+
) -> Settings:
|
|
199
|
+
config_path = Path(path).expanduser() if path is not None else default_config_path()
|
|
200
|
+
if config_path.suffix == ".json":
|
|
201
|
+
raise ValueError("Deepy only supports TOML config files; JSON config is not supported.")
|
|
202
|
+
env = env or os.environ
|
|
203
|
+
if not config_path.exists():
|
|
204
|
+
return Settings.from_mapping({}, path=config_path, env=env)
|
|
205
|
+
|
|
206
|
+
with config_path.open("rb") as fh:
|
|
207
|
+
raw = tomllib.load(fh)
|
|
208
|
+
return Settings.from_mapping(raw, path=config_path, env=env)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def settings_to_toml_dict(settings: Settings, *, reveal_secret: bool = False) -> dict[str, Any]:
|
|
212
|
+
data = _drop_empty(asdict(settings))
|
|
213
|
+
data.pop("path", None)
|
|
214
|
+
api_key = settings.model.api_key
|
|
215
|
+
if api_key:
|
|
216
|
+
data["model"]["api_key"] = api_key if reveal_secret else mask_secret(api_key)
|
|
217
|
+
data["model"]["thinking"] = settings.model.thinking_enabled
|
|
218
|
+
data["context"]["compact_prompt_token_threshold"] = (
|
|
219
|
+
settings.context.resolved_compact_threshold
|
|
220
|
+
)
|
|
221
|
+
return _drop_empty(data)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _drop_empty(value: Any) -> Any:
|
|
225
|
+
if isinstance(value, dict):
|
|
226
|
+
result = {}
|
|
227
|
+
for key, item in value.items():
|
|
228
|
+
if item is None:
|
|
229
|
+
continue
|
|
230
|
+
cleaned = _drop_empty(item)
|
|
231
|
+
if cleaned == {}:
|
|
232
|
+
continue
|
|
233
|
+
result[key] = cleaned
|
|
234
|
+
return result
|
|
235
|
+
if isinstance(value, list):
|
|
236
|
+
return [_drop_empty(item) for item in value]
|
|
237
|
+
return value
|
deepy/data/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Package data for Deepy."""
|