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.
Files changed (69) hide show
  1. deepy/__init__.py +9 -0
  2. deepy/__main__.py +7 -0
  3. deepy/cli.py +413 -0
  4. deepy/config/__init__.py +21 -0
  5. deepy/config/settings.py +237 -0
  6. deepy/data/__init__.py +1 -0
  7. deepy/data/tools/AskUserQuestion.md +10 -0
  8. deepy/data/tools/WebFetch.md +9 -0
  9. deepy/data/tools/WebSearch.md +9 -0
  10. deepy/data/tools/__init__.py +1 -0
  11. deepy/data/tools/bash.md +7 -0
  12. deepy/data/tools/edit.md +13 -0
  13. deepy/data/tools/modify.md +17 -0
  14. deepy/data/tools/read.md +8 -0
  15. deepy/data/tools/write.md +12 -0
  16. deepy/errors.py +63 -0
  17. deepy/llm/__init__.py +13 -0
  18. deepy/llm/agent.py +31 -0
  19. deepy/llm/context.py +109 -0
  20. deepy/llm/events.py +187 -0
  21. deepy/llm/model_capabilities.py +7 -0
  22. deepy/llm/provider.py +81 -0
  23. deepy/llm/replay.py +120 -0
  24. deepy/llm/runner.py +412 -0
  25. deepy/llm/thinking.py +30 -0
  26. deepy/prompts/__init__.py +6 -0
  27. deepy/prompts/compact.py +100 -0
  28. deepy/prompts/rules.py +24 -0
  29. deepy/prompts/runtime_context.py +98 -0
  30. deepy/prompts/system.py +72 -0
  31. deepy/prompts/tool_docs.py +21 -0
  32. deepy/sessions/__init__.py +17 -0
  33. deepy/sessions/jsonl.py +306 -0
  34. deepy/sessions/manager.py +202 -0
  35. deepy/skills.py +202 -0
  36. deepy/status.py +65 -0
  37. deepy/tools/__init__.py +6 -0
  38. deepy/tools/agents.py +343 -0
  39. deepy/tools/builtin.py +2113 -0
  40. deepy/tools/file_state.py +85 -0
  41. deepy/tools/result.py +54 -0
  42. deepy/tools/shell_utils.py +83 -0
  43. deepy/ui/__init__.py +5 -0
  44. deepy/ui/app.py +118 -0
  45. deepy/ui/ask_user_question.py +182 -0
  46. deepy/ui/exit_summary.py +142 -0
  47. deepy/ui/loading_text.py +87 -0
  48. deepy/ui/markdown.py +152 -0
  49. deepy/ui/message_view.py +546 -0
  50. deepy/ui/prompt_buffer.py +176 -0
  51. deepy/ui/prompt_input.py +286 -0
  52. deepy/ui/session_list.py +140 -0
  53. deepy/ui/session_picker.py +179 -0
  54. deepy/ui/slash_commands.py +67 -0
  55. deepy/ui/styles.py +21 -0
  56. deepy/ui/terminal.py +959 -0
  57. deepy/ui/thinking_state.py +29 -0
  58. deepy/ui/welcome.py +195 -0
  59. deepy/update_check.py +195 -0
  60. deepy/usage.py +192 -0
  61. deepy/utils/__init__.py +15 -0
  62. deepy/utils/debug_logger.py +62 -0
  63. deepy/utils/error_logger.py +107 -0
  64. deepy/utils/json.py +29 -0
  65. deepy/utils/notify.py +66 -0
  66. deepy_cli-0.1.1.dist-info/METADATA +205 -0
  67. deepy_cli-0.1.1.dist-info/RECORD +69 -0
  68. deepy_cli-0.1.1.dist-info/WHEEL +4 -0
  69. deepy_cli-0.1.1.dist-info/entry_points.txt +3 -0
deepy/llm/replay.py ADDED
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+ from collections.abc import Iterable
5
+ from typing import Any, cast
6
+
7
+
8
+ def sanitize_model_input_for_chat_completions(input_value: Any) -> Any:
9
+ if not isinstance(input_value, list):
10
+ return input_value
11
+ return sanitize_replay_items(input_value)
12
+
13
+
14
+ def sanitize_sdk_items_for_replay(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
15
+ return cast(list[dict[str, Any]], sanitize_replay_items(items))
16
+
17
+
18
+ def sanitize_replay_items(items: Iterable[Any]) -> list[Any]:
19
+ item_list = list(items)
20
+ remaining_outputs = Counter(
21
+ call_id
22
+ for item in item_list
23
+ if _item_type(item) == "function_call_output" and (call_id := _call_id(item))
24
+ )
25
+ open_call_ids: set[str] = set()
26
+ sanitized: list[Any] = []
27
+
28
+ for item in item_list:
29
+ item_type = _item_type(item)
30
+ if item_type == "function_call":
31
+ sanitized.append(item)
32
+ if call_id := _call_id(item):
33
+ open_call_ids.add(call_id)
34
+ continue
35
+
36
+ if item_type == "function_call_output":
37
+ if call_id := _call_id(item):
38
+ if remaining_outputs[call_id] > 0:
39
+ remaining_outputs[call_id] -= 1
40
+ open_call_ids.discard(call_id)
41
+ sanitized.append(item)
42
+ continue
43
+
44
+ if _is_empty_assistant_message(item) and any(
45
+ remaining_outputs[call_id] > 0 for call_id in open_call_ids
46
+ ):
47
+ continue
48
+
49
+ sanitized.append(item)
50
+
51
+ return sanitized
52
+
53
+
54
+ def sanitize_model_response_output(items: list[Any]) -> list[Any]:
55
+ return [item for item in items if not _is_empty_assistant_message(item)]
56
+
57
+
58
+ def sanitize_chat_completion_stream_event(event: Any) -> Any | None:
59
+ if getattr(event, "type", None) == "response.output_item.done" and _is_empty_assistant_message(
60
+ getattr(event, "item", None)
61
+ ):
62
+ return None
63
+
64
+ if getattr(event, "type", None) == "response.completed":
65
+ response = getattr(event, "response", None)
66
+ output = getattr(response, "output", None)
67
+ if isinstance(output, list):
68
+ try:
69
+ response.output = sanitize_model_response_output(output)
70
+ except Exception:
71
+ pass
72
+ return event
73
+
74
+
75
+ def _is_empty_assistant_message(item: Any) -> bool:
76
+ if _item_type(item) != "message" or _role(item) != "assistant":
77
+ return False
78
+
79
+ content = _get_value(item, "content")
80
+ if content in ("", None, []):
81
+ return True
82
+ if isinstance(content, str):
83
+ return not content.strip()
84
+ if not isinstance(content, list):
85
+ return False
86
+
87
+ saw_text_part = False
88
+ for part in content:
89
+ text = _get_value(part, "text")
90
+ if text is None:
91
+ text = _get_value(part, "refusal")
92
+ if not isinstance(text, str):
93
+ return False
94
+ saw_text_part = True
95
+ if text.strip():
96
+ return False
97
+ return saw_text_part
98
+
99
+
100
+ def _item_type(item: Any) -> str:
101
+ value = _get_value(item, "type")
102
+ return value if isinstance(value, str) else ""
103
+
104
+
105
+ def _role(item: Any) -> str:
106
+ value = _get_value(item, "role")
107
+ return value if isinstance(value, str) else ""
108
+
109
+
110
+ def _call_id(item: Any) -> str:
111
+ value = _get_value(item, "call_id")
112
+ if value is None:
113
+ value = _get_value(item, "id")
114
+ return value if isinstance(value, str) else ""
115
+
116
+
117
+ def _get_value(item: Any, key: str) -> Any:
118
+ if isinstance(item, dict):
119
+ return item.get(key)
120
+ return getattr(item, key, None)
deepy/llm/runner.py ADDED
@@ -0,0 +1,412 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from collections.abc import Callable
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any, Literal
8
+
9
+ from deepy.config import Settings, load_settings
10
+ from deepy.sessions.jsonl import DeepyJsonlSession
11
+ from deepy.skills import discover_skills, find_skill, match_skills_for_prompt
12
+ from deepy.tools import ToolRuntime
13
+ from deepy.usage import TokenUsage, merge_usage, normalize_usage, usage_from_run_result
14
+ from deepy.utils import json as json_utils
15
+ from deepy.utils import launch_notify_script, log_api_error, log_debug_event
16
+
17
+ from .agent import build_deepy_agent
18
+ from .context import build_session_input_callback
19
+ from .events import DeepyStreamEvent, normalize_stream_event
20
+ from .provider import ProviderBundle, build_provider_bundle
21
+
22
+ DEFAULT_MAX_TURNS = 100
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class RunSummary:
27
+ output: str
28
+ session_id: str
29
+ complete: bool
30
+ interrupted: bool = False
31
+ status: str = "completed"
32
+ pending_questions: list[dict[str, Any]] = field(default_factory=list)
33
+ usage: TokenUsage = field(default_factory=TokenUsage)
34
+ duration_ms: int = 0
35
+
36
+
37
+ async def run_prompt_once(
38
+ prompt: str,
39
+ *,
40
+ project_root: Path | None = None,
41
+ settings: Settings | None = None,
42
+ provider: ProviderBundle | None = None,
43
+ emit: Callable[[str], None] | None = None,
44
+ emit_event: Callable[[DeepyStreamEvent], None] | None = None,
45
+ max_turns: int = DEFAULT_MAX_TURNS,
46
+ session_id: str | None = None,
47
+ skill_names: list[str] | None = None,
48
+ should_interrupt: Callable[[], bool] | None = None,
49
+ cancel_mode: Literal["immediate", "after_turn"] = "immediate",
50
+ ) -> RunSummary:
51
+ from agents import RunConfig, Runner
52
+ from agents.exceptions import MaxTurnsExceeded
53
+ from openai import APIStatusError
54
+
55
+ root = (project_root or Path.cwd()).resolve()
56
+ resolved_settings = settings or load_settings()
57
+ resolved_provider = provider or build_provider_bundle(resolved_settings)
58
+ runtime = ToolRuntime(cwd=root, settings=resolved_settings)
59
+ loaded_skills = _resolve_loaded_skills(root, prompt, skill_names)
60
+ agent = build_deepy_agent(
61
+ resolved_settings,
62
+ runtime,
63
+ project_root=root,
64
+ provider=resolved_provider,
65
+ loaded_skills=loaded_skills,
66
+ )
67
+ session = (
68
+ DeepyJsonlSession.open(root, session_id)
69
+ if session_id
70
+ else DeepyJsonlSession.create(root)
71
+ )
72
+ run_config = RunConfig(
73
+ workflow_name="Deepy",
74
+ trace_include_sensitive_data=False,
75
+ reasoning_item_id_policy="omit",
76
+ session_input_callback=build_session_input_callback(resolved_settings),
77
+ )
78
+
79
+ started_at = time.time()
80
+ chunks: list[str] = []
81
+ result: Any | None = None
82
+ interrupted = False
83
+ waiting_for_user = False
84
+ pending_questions: list[dict[str, Any]] = []
85
+ usage = TokenUsage()
86
+ try:
87
+ result = Runner.run_streamed(
88
+ agent,
89
+ input=prompt,
90
+ max_turns=max_turns,
91
+ run_config=run_config,
92
+ session=session,
93
+ )
94
+ async for event in result.stream_events():
95
+ if should_interrupt is not None and should_interrupt():
96
+ _cancel_stream_result(result, mode=cancel_mode)
97
+ interrupted = True
98
+ break
99
+ normalized = normalize_stream_event(event)
100
+ if normalized is None:
101
+ continue
102
+ if normalized.kind == "usage":
103
+ usage = merge_usage(usage, normalize_usage(normalized.payload.get("usage")))
104
+ if emit_event is not None:
105
+ emit_event(normalized)
106
+ if normalized.kind == "tool_output":
107
+ questions = _pending_questions_from_tool_output(normalized.text)
108
+ if questions:
109
+ pending_questions = questions
110
+ waiting_for_user = True
111
+ _cancel_stream_result(result, mode="after_turn")
112
+ break
113
+ if normalized.kind != "text_delta" or not normalized.text:
114
+ continue
115
+ chunks.append(normalized.text)
116
+ if emit is not None:
117
+ emit(normalized.text)
118
+ if should_interrupt is not None and should_interrupt():
119
+ _cancel_stream_result(result, mode=cancel_mode)
120
+ interrupted = True
121
+ break
122
+ except MaxTurnsExceeded:
123
+ result_usage = usage_from_run_result(result)
124
+ if result_usage.known:
125
+ usage = result_usage
126
+ duration_ms = int((time.time() - started_at) * 1000)
127
+ session.record_usage(usage)
128
+ return RunSummary(
129
+ output=_max_turns_output(chunks, max_turns=max_turns),
130
+ session_id=session.session_id,
131
+ complete=False,
132
+ status="max_turns_exceeded",
133
+ usage=usage,
134
+ duration_ms=duration_ms,
135
+ )
136
+ except APIStatusError as exc:
137
+ log_api_error(
138
+ {
139
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
140
+ "location": "deepy.llm.runner.run_prompt_once",
141
+ "requestId": session.session_id,
142
+ "sessionId": session.session_id,
143
+ "model": resolved_settings.model.name,
144
+ "baseURL": resolved_settings.model.base_url,
145
+ "error": exc,
146
+ "response": _api_status_error_response(exc),
147
+ "request": {
148
+ "input": prompt,
149
+ "max_turns": max_turns,
150
+ },
151
+ }
152
+ )
153
+ result_usage = usage_from_run_result(result)
154
+ if result_usage.known:
155
+ usage = result_usage
156
+ duration_ms = int((time.time() - started_at) * 1000)
157
+ session.record_usage(usage)
158
+ return RunSummary(
159
+ output=format_deepseek_api_error(exc),
160
+ session_id=session.session_id,
161
+ complete=False,
162
+ status="api_error",
163
+ usage=usage,
164
+ duration_ms=duration_ms,
165
+ )
166
+ except Exception as exc:
167
+ log_api_error(
168
+ {
169
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
170
+ "location": "deepy.llm.runner.run_prompt_once",
171
+ "requestId": session.session_id,
172
+ "sessionId": session.session_id,
173
+ "model": resolved_settings.model.name,
174
+ "baseURL": resolved_settings.model.base_url,
175
+ "error": exc,
176
+ "request": {
177
+ "input": prompt,
178
+ "max_turns": max_turns,
179
+ },
180
+ }
181
+ )
182
+ raise
183
+
184
+ final_output = getattr(result, "final_output", None)
185
+ output = final_output if isinstance(final_output, str) else "".join(chunks)
186
+ result_usage = usage_from_run_result(result)
187
+ if result_usage.known:
188
+ usage = result_usage
189
+ session.record_usage(usage)
190
+ duration_ms = int((time.time() - started_at) * 1000)
191
+ if resolved_settings.logging.debug:
192
+ log_debug_event(
193
+ {
194
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
195
+ "location": "deepy.llm.runner.run_prompt_once",
196
+ "sessionId": session.session_id,
197
+ "model": resolved_settings.model.name,
198
+ "baseURL": resolved_settings.model.base_url,
199
+ "durationMs": duration_ms,
200
+ "request": {
201
+ "input": prompt,
202
+ "max_turns": max_turns,
203
+ },
204
+ "response": {"output": output, "usage": usage.to_dict()},
205
+ }
206
+ )
207
+ if resolved_settings.notify.enabled and resolved_settings.notify.command:
208
+ launch_notify_script(resolved_settings.notify.command, duration_ms, root)
209
+ return RunSummary(
210
+ output=output,
211
+ session_id=session.session_id,
212
+ complete=False
213
+ if interrupted or waiting_for_user
214
+ else bool(getattr(result, "is_complete", True)),
215
+ interrupted=interrupted,
216
+ status=_run_status(
217
+ interrupted=interrupted,
218
+ waiting_for_user=waiting_for_user,
219
+ complete=bool(getattr(result, "is_complete", True)),
220
+ ),
221
+ pending_questions=pending_questions,
222
+ usage=usage,
223
+ duration_ms=duration_ms,
224
+ )
225
+
226
+
227
+ def _resolve_loaded_skills(
228
+ root: Path,
229
+ prompt: str,
230
+ skill_names: list[str] | None,
231
+ ) -> list[Any]:
232
+ if skill_names:
233
+ loaded_skills = []
234
+ for skill_name in skill_names:
235
+ skill = find_skill(root, skill_name)
236
+ if skill is None:
237
+ raise ValueError(f"Skill not found: {skill_name}")
238
+ loaded_skills.append(skill)
239
+ return loaded_skills
240
+ return match_skills_for_prompt(discover_skills(root), prompt)
241
+
242
+
243
+ def _max_turns_output(chunks: list[str], *, max_turns: int) -> str:
244
+ message = (
245
+ f"Stopped after reaching the max turn limit ({max_turns}). "
246
+ "The session was preserved; review the tool output above and ask Deepy to continue, "
247
+ "or narrow the request if it keeps looping."
248
+ )
249
+ partial = "".join(chunks).strip()
250
+ return f"{partial}\n\n{message}" if partial else message
251
+
252
+
253
+ def format_deepseek_api_error(error: Any) -> str:
254
+ status_code = _safe_int(getattr(error, "status_code", None))
255
+ status = DEEPSEEK_ERROR_CODES.get(status_code)
256
+ title = f"DeepSeek API error {status_code}" if status_code is not None else "DeepSeek API error"
257
+ if status is not None:
258
+ title = f"{title}: {status.title}"
259
+
260
+ lines = [title]
261
+ server_message = _api_status_error_message(error)
262
+ if server_message:
263
+ lines.extend(["", f"Server message: {server_message}"])
264
+ if status is not None:
265
+ lines.extend(["", f"Reason: {status.reason}", f"Suggestion: {status.suggestion}"])
266
+
267
+ error_code = _api_error_body_field(error, "code")
268
+ error_type = _api_error_body_field(error, "type")
269
+ if error_code or error_type:
270
+ detail_parts = [
271
+ part
272
+ for part in (
273
+ f"code={error_code}" if error_code else "",
274
+ f"type={error_type}" if error_type else "",
275
+ )
276
+ if part
277
+ ]
278
+ detail = ", ".join(detail_parts)
279
+ lines.append(f"Detail: {detail}")
280
+ return "\n".join(lines)
281
+
282
+
283
+ @dataclass(frozen=True)
284
+ class DeepSeekErrorStatus:
285
+ title: str
286
+ reason: str
287
+ suggestion: str
288
+
289
+
290
+ DEEPSEEK_ERROR_CODES: dict[int, DeepSeekErrorStatus] = {
291
+ 400: DeepSeekErrorStatus(
292
+ title="格式错误",
293
+ reason="请求体格式错误。",
294
+ suggestion="请根据错误信息提示修改请求体。",
295
+ ),
296
+ 401: DeepSeekErrorStatus(
297
+ title="认证失败",
298
+ reason="API key 错误,认证失败。",
299
+ suggestion="请检查 API key 是否正确;如果还没有 API key,请先创建 API key。",
300
+ ),
301
+ 402: DeepSeekErrorStatus(
302
+ title="余额不足",
303
+ reason="账号余额不足。",
304
+ suggestion="请确认账户余额,并前往 DeepSeek 充值页面充值。",
305
+ ),
306
+ 422: DeepSeekErrorStatus(
307
+ title="参数错误",
308
+ reason="请求体参数错误。",
309
+ suggestion="请根据错误信息提示修改相关参数。",
310
+ ),
311
+ 429: DeepSeekErrorStatus(
312
+ title="请求速率达到上限",
313
+ reason="请求速率(TPM 或 RPM)达到上限。",
314
+ suggestion="请合理规划请求速率,稍后重试。",
315
+ ),
316
+ 500: DeepSeekErrorStatus(
317
+ title="服务器故障",
318
+ reason="DeepSeek 服务器内部故障。",
319
+ suggestion="请等待后重试;如果问题持续存在,请联系 DeepSeek 支持。",
320
+ ),
321
+ 503: DeepSeekErrorStatus(
322
+ title="服务器繁忙",
323
+ reason="服务器负载过高。",
324
+ suggestion="请稍后重试请求。",
325
+ ),
326
+ }
327
+
328
+
329
+ def _api_status_error_message(error: Any) -> str:
330
+ body_message = _api_error_body_field(error, "message")
331
+ if body_message:
332
+ return body_message
333
+ message = getattr(error, "message", None)
334
+ return str(message).strip() if message else str(error).strip()
335
+
336
+
337
+ def _api_status_error_response(error: Any) -> dict[str, Any]:
338
+ response = getattr(error, "response", None)
339
+ result: dict[str, Any] = {}
340
+ status_code = _safe_int(getattr(error, "status_code", None))
341
+ if status_code is not None:
342
+ result["statusCode"] = status_code
343
+ request_id = getattr(error, "request_id", None)
344
+ if request_id:
345
+ result["requestId"] = request_id
346
+ body = getattr(error, "body", None)
347
+ if body is not None:
348
+ result["body"] = body
349
+ if response is not None:
350
+ url = getattr(response, "url", None)
351
+ if url is not None:
352
+ result["url"] = str(url)
353
+ return result
354
+
355
+
356
+ def _api_error_body_field(error: Any, field: str) -> str:
357
+ body = getattr(error, "body", None)
358
+ if isinstance(body, dict):
359
+ body_error = body.get("error")
360
+ if isinstance(body_error, dict):
361
+ value = body_error.get(field)
362
+ return str(value).strip() if value is not None else ""
363
+ value = body.get(field)
364
+ return str(value).strip() if value is not None else ""
365
+ return ""
366
+
367
+
368
+ def _safe_int(value: Any) -> int | None:
369
+ try:
370
+ return int(value)
371
+ except (TypeError, ValueError):
372
+ return None
373
+
374
+
375
+ def _cancel_stream_result(
376
+ result: Any,
377
+ *,
378
+ mode: Literal["immediate", "after_turn"],
379
+ ) -> None:
380
+ cancel = getattr(result, "cancel", None)
381
+ if not callable(cancel):
382
+ return
383
+ try:
384
+ cancel(mode=mode)
385
+ except TypeError:
386
+ cancel()
387
+
388
+
389
+ def _pending_questions_from_tool_output(output: str) -> list[dict[str, Any]]:
390
+ if not output.strip():
391
+ return []
392
+ try:
393
+ payload = json_utils.loads(output)
394
+ except json_utils.JSONDecodeError:
395
+ return []
396
+ if not isinstance(payload, dict) or payload.get("awaitUserResponse") is not True:
397
+ return []
398
+ metadata = payload.get("metadata")
399
+ if not isinstance(metadata, dict) or metadata.get("kind") != "ask_user_question":
400
+ return []
401
+ questions = metadata.get("questions")
402
+ if not isinstance(questions, list):
403
+ return []
404
+ return [question for question in questions if isinstance(question, dict)]
405
+
406
+
407
+ def _run_status(*, interrupted: bool, waiting_for_user: bool, complete: bool) -> str:
408
+ if interrupted:
409
+ return "interrupted"
410
+ if waiting_for_user:
411
+ return "waiting_for_user"
412
+ return "completed" if complete else "incomplete"
deepy/llm/thinking.py ADDED
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from deepy.config import Settings
6
+
7
+
8
+ def build_thinking_extra_body(
9
+ thinking_enabled: bool,
10
+ reasoning_effort: str = "max",
11
+ ) -> dict[str, Any]:
12
+ body: dict[str, Any] = {
13
+ "thinking": {"type": "enabled" if thinking_enabled else "disabled"}
14
+ }
15
+ if thinking_enabled:
16
+ body["reasoning_effort"] = reasoning_effort if reasoning_effort in {"high", "max"} else "max"
17
+ return body
18
+
19
+
20
+ def build_model_settings(settings: Settings):
21
+ from agents import ModelSettings
22
+
23
+ return ModelSettings(
24
+ include_usage=True,
25
+ store=False,
26
+ extra_body=build_thinking_extra_body(
27
+ settings.model.thinking_enabled,
28
+ settings.model.reasoning_effort,
29
+ ),
30
+ )
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from .compact import build_compact_prompt
4
+ from .system import build_system_prompt
5
+
6
+ __all__ = ["build_compact_prompt", "build_system_prompt"]
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from deepy.utils import json as json_utils
6
+
7
+
8
+ COMPACT_PROMPT_BASE = """Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
9
+ This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.
10
+
11
+ Before providing your final summary, wrap your analysis in <analysis> tags to organize your thoughts and ensure you've covered all necessary points. In your analysis process:
12
+
13
+ 1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify:
14
+ - The user's explicit requests and intents
15
+ - Your approach to addressing the user's requests
16
+ - Key decisions, technical concepts and code patterns
17
+ - Specific details like:
18
+ - file names
19
+ - full code snippets
20
+ - function signatures
21
+ - file edits
22
+ - Errors that you ran into and how you fixed them
23
+ - Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
24
+ 2. Double-check for technical accuracy and completeness, addressing each required element thoroughly.
25
+
26
+ Your summary should include the following sections:
27
+
28
+ 1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
29
+ 2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
30
+ 3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Pay special attention to the most recent messages and include full code snippets where applicable and include a summary of why this file read or edit is important.
31
+ 4. Errors and fixes: List all errors that you ran into, and how you fixed the error. Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
32
+ 5. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
33
+ 6. All user messages: List ALL user messages that are not tool results. These are critical for understanding the users' feedback and changing intent.
34
+ 7. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
35
+ 8. Current Work: Describe in detail precisely what was being worked on immediately before this summary request, paying special attention to the most recent messages from both user and assistant. Include file names and code snippets where applicable.
36
+ 9. Optional Next Step: List the next step that you will take that is related to the most recent work you were doing. IMPORTANT: ensure that this step is DIRECTLY in line with the user's most recent explicit requests and the task you were working on immediately before this summary request. If your last task was concluded, then only list next steps if they are explicitly in line with the user's request. Do not start on tangential requests or old requests that were already completed without confirming with the user first.
37
+
38
+ Output structure:
39
+
40
+ <analysis>
41
+ [Your thought process, ensuring all points are covered thoroughly and accurately]
42
+ </analysis>
43
+
44
+ <summary>
45
+ 1. Primary Request and Intent:
46
+ [Detailed description]
47
+
48
+ 2. Key Technical Concepts:
49
+ - [Concept 1]
50
+ - [Concept 2]
51
+
52
+ 3. Files and Code Sections:
53
+ - [File Name 1]
54
+ - [Summary of why this file is important]
55
+ - [Summary of the changes made to this file, if any]
56
+ - [Important Code Snippet]
57
+
58
+ 4. Errors and fixes:
59
+ - [Detailed description of error 1]:
60
+ - [How you fixed the error]
61
+ - [User feedback on the error if any]
62
+
63
+ 5. Problem Solving:
64
+ [Description of solved problems and ongoing troubleshooting]
65
+
66
+ 6. All user messages:
67
+ - [Detailed non-tool user message]
68
+
69
+ 7. Pending Tasks:
70
+ - [Task 1]
71
+
72
+ 8. Current Work:
73
+ [Precise description of current work]
74
+
75
+ 9. Optional Next Step:
76
+ [Optional next step to take]
77
+ </summary>"""
78
+
79
+
80
+ COMPACT_MESSAGE_KEYS = (
81
+ "id",
82
+ "role",
83
+ "content",
84
+ "type",
85
+ "name",
86
+ "tool_calls",
87
+ "tool_call_id",
88
+ "output",
89
+ "created_at",
90
+ )
91
+
92
+
93
+ def build_compact_prompt(session_messages: list[dict[str, Any]]) -> str:
94
+ jsonl = "\n".join(_compact_message_json(message) for message in session_messages)
95
+ return f"{COMPACT_PROMPT_BASE}\n\nconversation below:\n\n```jsonl\n{jsonl}\n```"
96
+
97
+
98
+ def _compact_message_json(message: dict[str, Any]) -> str:
99
+ payload = {key: message[key] for key in COMPACT_MESSAGE_KEYS if key in message}
100
+ return json_utils.dumps(payload)
deepy/prompts/rules.py ADDED
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ AGENT_DRIFT_GUARD = (
6
+ "Keep the latest user task in focus; preserve explicit constraints, paths, commands, "
7
+ "test results, and decisions when compressing context."
8
+ )
9
+
10
+
11
+ def load_project_rules(project_root: Path, *, home: Path | None = None) -> str:
12
+ home_dir = home or Path.home()
13
+ candidates = [
14
+ project_root / "AGENTS.md",
15
+ home_dir / ".deepy" / "AGENTS.md",
16
+ ]
17
+ blocks: list[str] = []
18
+ for path in candidates:
19
+ if not path.is_file():
20
+ continue
21
+ text = path.read_text(encoding="utf-8", errors="replace").strip()
22
+ if text:
23
+ blocks.append(f"From {path}:\n{text}")
24
+ return "\n\n".join(blocks)