zjcode 0.0.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.
- deepagents_code/__init__.py +42 -0
- deepagents_code/__main__.py +6 -0
- deepagents_code/_ask_user_types.py +90 -0
- deepagents_code/_cli_context.py +96 -0
- deepagents_code/_constants.py +41 -0
- deepagents_code/_debug.py +148 -0
- deepagents_code/_debug_buffer.py +204 -0
- deepagents_code/_env_vars.py +411 -0
- deepagents_code/_fake_models.py +66 -0
- deepagents_code/_git.py +521 -0
- deepagents_code/_paths.py +69 -0
- deepagents_code/_server_config.py +576 -0
- deepagents_code/_session_stats.py +235 -0
- deepagents_code/_startup_error.py +45 -0
- deepagents_code/_testing_models.py +305 -0
- deepagents_code/_textual_patches.py +420 -0
- deepagents_code/_tool_stream.py +694 -0
- deepagents_code/_version.py +46 -0
- deepagents_code/agent.py +1976 -0
- deepagents_code/app.py +19239 -0
- deepagents_code/app.tcss +438 -0
- deepagents_code/approval_mode.py +131 -0
- deepagents_code/ask_user.py +306 -0
- deepagents_code/auth_display.py +185 -0
- deepagents_code/auth_store.py +545 -0
- deepagents_code/built_in_skills/__init__.py +5 -0
- deepagents_code/built_in_skills/remember/SKILL.md +118 -0
- deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
- deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
- deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
- deepagents_code/client/__init__.py +1 -0
- deepagents_code/client/commands/__init__.py +1 -0
- deepagents_code/client/commands/auth.py +541 -0
- deepagents_code/client/commands/config.py +714 -0
- deepagents_code/client/commands/mcp.py +250 -0
- deepagents_code/client/commands/tools.py +416 -0
- deepagents_code/client/launch/__init__.py +1 -0
- deepagents_code/client/launch/server.py +978 -0
- deepagents_code/client/launch/server_manager.py +540 -0
- deepagents_code/client/non_interactive.py +1758 -0
- deepagents_code/client/remote_client.py +794 -0
- deepagents_code/clipboard.py +217 -0
- deepagents_code/command_registry.py +450 -0
- deepagents_code/config.py +4829 -0
- deepagents_code/config_manifest.py +1451 -0
- deepagents_code/configurable_model.py +577 -0
- deepagents_code/default_agent_prompt.md +12 -0
- deepagents_code/doctor.py +559 -0
- deepagents_code/editor.py +142 -0
- deepagents_code/event_bus.py +411 -0
- deepagents_code/extras_info.py +661 -0
- deepagents_code/file_ops.py +576 -0
- deepagents_code/formatting.py +135 -0
- deepagents_code/goal_rubric.py +497 -0
- deepagents_code/goal_tools.py +459 -0
- deepagents_code/hooks.py +348 -0
- deepagents_code/input.py +1041 -0
- deepagents_code/integrations/__init__.py +1 -0
- deepagents_code/integrations/openai_codex.py +551 -0
- deepagents_code/integrations/sandbox_config.py +198 -0
- deepagents_code/integrations/sandbox_factory.py +1124 -0
- deepagents_code/integrations/sandbox_provider.py +137 -0
- deepagents_code/integrations/sandbox_registry.py +350 -0
- deepagents_code/iterm_cursor_guide.py +176 -0
- deepagents_code/local_context.py +926 -0
- deepagents_code/main.py +3985 -0
- deepagents_code/managed_tools.py +642 -0
- deepagents_code/mcp_auth.py +1950 -0
- deepagents_code/mcp_config.py +176 -0
- deepagents_code/mcp_disabled.py +212 -0
- deepagents_code/mcp_login_service.py +281 -0
- deepagents_code/mcp_oauth_ui.py +199 -0
- deepagents_code/mcp_providers/__init__.py +23 -0
- deepagents_code/mcp_providers/_registry.py +39 -0
- deepagents_code/mcp_providers/base.py +133 -0
- deepagents_code/mcp_providers/github.py +102 -0
- deepagents_code/mcp_providers/slack.py +175 -0
- deepagents_code/mcp_tools.py +2427 -0
- deepagents_code/mcp_trust.py +207 -0
- deepagents_code/media_utils.py +826 -0
- deepagents_code/memory_guard.py +474 -0
- deepagents_code/model_config.py +4156 -0
- deepagents_code/notifications.py +247 -0
- deepagents_code/offload.py +402 -0
- deepagents_code/onboarding.py +223 -0
- deepagents_code/output.py +69 -0
- deepagents_code/paste_collapse.py +103 -0
- deepagents_code/project_utils.py +231 -0
- deepagents_code/py.typed +0 -0
- deepagents_code/reasoning_effort.py +641 -0
- deepagents_code/reliable_rubric.py +97 -0
- deepagents_code/resume_state.py +237 -0
- deepagents_code/server_graph.py +310 -0
- deepagents_code/session_end_summary.py +329 -0
- deepagents_code/sessions.py +1716 -0
- deepagents_code/skills/__init__.py +18 -0
- deepagents_code/skills/commands.py +1252 -0
- deepagents_code/skills/invocation.py +112 -0
- deepagents_code/skills/load.py +196 -0
- deepagents_code/skills/trust.py +547 -0
- deepagents_code/state_migration.py +136 -0
- deepagents_code/subagents.py +278 -0
- deepagents_code/system_prompt.md +204 -0
- deepagents_code/terminal_capabilities.py +115 -0
- deepagents_code/terminal_escape.py +287 -0
- deepagents_code/theme.py +891 -0
- deepagents_code/todo_list_prompt.md +12 -0
- deepagents_code/tool_catalog.py +509 -0
- deepagents_code/tool_display.py +367 -0
- deepagents_code/tools.py +516 -0
- deepagents_code/tui/__init__.py +1 -0
- deepagents_code/tui/textual_adapter.py +2553 -0
- deepagents_code/tui/widgets/__init__.py +9 -0
- deepagents_code/tui/widgets/_js_eval_display.py +139 -0
- deepagents_code/tui/widgets/_links.py +261 -0
- deepagents_code/tui/widgets/agent_selector.py +420 -0
- deepagents_code/tui/widgets/approval.py +602 -0
- deepagents_code/tui/widgets/ask_user.py +515 -0
- deepagents_code/tui/widgets/auth.py +1997 -0
- deepagents_code/tui/widgets/autocomplete.py +890 -0
- deepagents_code/tui/widgets/chat_input.py +3181 -0
- deepagents_code/tui/widgets/codex_auth.py +452 -0
- deepagents_code/tui/widgets/cwd_switch.py +242 -0
- deepagents_code/tui/widgets/debug_console.py +868 -0
- deepagents_code/tui/widgets/diff.py +252 -0
- deepagents_code/tui/widgets/effort_selector.py +189 -0
- deepagents_code/tui/widgets/goal_review.py +390 -0
- deepagents_code/tui/widgets/goal_status.py +50 -0
- deepagents_code/tui/widgets/history.py +194 -0
- deepagents_code/tui/widgets/install_confirm.py +248 -0
- deepagents_code/tui/widgets/launch_init.py +482 -0
- deepagents_code/tui/widgets/loading.py +227 -0
- deepagents_code/tui/widgets/mcp_login.py +539 -0
- deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
- deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
- deepagents_code/tui/widgets/message_store.py +999 -0
- deepagents_code/tui/widgets/messages.py +3846 -0
- deepagents_code/tui/widgets/model_selector.py +2343 -0
- deepagents_code/tui/widgets/notification_center.py +456 -0
- deepagents_code/tui/widgets/notification_detail.py +257 -0
- deepagents_code/tui/widgets/notification_settings.py +170 -0
- deepagents_code/tui/widgets/restart_prompt.py +158 -0
- deepagents_code/tui/widgets/skill_trust.py +131 -0
- deepagents_code/tui/widgets/startup_tip.py +91 -0
- deepagents_code/tui/widgets/status.py +781 -0
- deepagents_code/tui/widgets/subagent_panel.py +969 -0
- deepagents_code/tui/widgets/theme_selector.py +401 -0
- deepagents_code/tui/widgets/thread_selector.py +2564 -0
- deepagents_code/tui/widgets/tool_renderers.py +190 -0
- deepagents_code/tui/widgets/tool_widgets.py +304 -0
- deepagents_code/tui/widgets/update_available.py +311 -0
- deepagents_code/tui/widgets/update_confirm.py +184 -0
- deepagents_code/tui/widgets/update_progress.py +327 -0
- deepagents_code/tui/widgets/welcome.py +508 -0
- deepagents_code/turn_end_summary.py +522 -0
- deepagents_code/ui.py +858 -0
- deepagents_code/unicode_security.py +563 -0
- deepagents_code/update_check.py +3124 -0
- zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
- zjcode-0.0.1.dist-info/METADATA +220 -0
- zjcode-0.0.1.dist-info/RECORD +163 -0
- zjcode-0.0.1.dist-info/WHEEL +4 -0
- zjcode-0.0.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Ask user middleware for interactive question-answering during agent execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING, Annotated, Any, cast
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from collections.abc import Awaitable, Callable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
from langchain.agents.middleware.types import (
|
|
13
|
+
AgentMiddleware,
|
|
14
|
+
ContextT,
|
|
15
|
+
ModelRequest,
|
|
16
|
+
ModelResponse,
|
|
17
|
+
ResponseT,
|
|
18
|
+
)
|
|
19
|
+
from langchain.tools import InjectedToolCallId
|
|
20
|
+
from langchain_core.messages import AIMessage, SystemMessage, ToolMessage
|
|
21
|
+
from langchain_core.tools import tool
|
|
22
|
+
from langgraph.types import Command, interrupt
|
|
23
|
+
|
|
24
|
+
from deepagents_code._ask_user_types import AskUserRequest, Question
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
ASK_USER_TOOL_DESCRIPTION = """Ask the user one or more questions when you need clarification or input before proceeding.
|
|
30
|
+
|
|
31
|
+
Each question can be either:
|
|
32
|
+
- "text": Free-form text response from the user
|
|
33
|
+
- "multiple_choice": User selects from predefined options (an "Other" option is always available)
|
|
34
|
+
|
|
35
|
+
For multiple choice questions, provide a list of choices. The user can pick one or type a custom answer via the "Other" option.
|
|
36
|
+
|
|
37
|
+
By default all questions are required. Set "required" to false for optional questions that the user can skip. Do not include "(required)", "(optional)", "- optional", or similar annotations in the question text — the UI renders that separately based on the "required" field.
|
|
38
|
+
|
|
39
|
+
Use this tool when:
|
|
40
|
+
- You need clarification on ambiguous requirements
|
|
41
|
+
- You want the user to choose between multiple valid approaches
|
|
42
|
+
- You need specific information only the user can provide
|
|
43
|
+
- You want to confirm a plan before executing it
|
|
44
|
+
|
|
45
|
+
Do NOT use this tool for:
|
|
46
|
+
- Simple yes/no confirmations (just proceed with your best judgment)
|
|
47
|
+
- Questions you can answer yourself from context
|
|
48
|
+
- Trivial decisions that don't meaningfully affect the outcome""" # noqa: E501
|
|
49
|
+
|
|
50
|
+
ASK_USER_SYSTEM_PROMPT = """## `ask_user`
|
|
51
|
+
|
|
52
|
+
You have access to the `ask_user` tool to ask the user questions when you need clarification or input.
|
|
53
|
+
Use this tool sparingly - only when you genuinely need information from the user that you cannot determine from context.
|
|
54
|
+
|
|
55
|
+
When using `ask_user`:
|
|
56
|
+
- Be concise and specific with your questions
|
|
57
|
+
- Use multiple choice when there are clear options to choose from
|
|
58
|
+
- Use text input when you need free-form responses
|
|
59
|
+
- Group related questions into a single ask_user call rather than making multiple calls
|
|
60
|
+
- Never ask questions you can answer yourself from the available context""" # noqa: E501
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _validate_questions(questions: list[Question]) -> None:
|
|
64
|
+
"""Validate ask_user question structure before interrupting.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
questions: Question definitions provided to the `ask_user` tool.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ValueError: If the questions list or an individual question is invalid.
|
|
71
|
+
"""
|
|
72
|
+
if not questions:
|
|
73
|
+
msg = "ask_user requires at least one question"
|
|
74
|
+
raise ValueError(msg)
|
|
75
|
+
|
|
76
|
+
for q in questions:
|
|
77
|
+
question_text = q.get("question")
|
|
78
|
+
if not isinstance(question_text, str) or not question_text.strip():
|
|
79
|
+
msg = "ask_user questions must have non-empty 'question' text"
|
|
80
|
+
raise ValueError(msg)
|
|
81
|
+
|
|
82
|
+
question_type = q.get("type")
|
|
83
|
+
if question_type not in {"text", "multiple_choice"}:
|
|
84
|
+
msg = f"unsupported ask_user question type: {question_type!r}"
|
|
85
|
+
raise ValueError(msg)
|
|
86
|
+
|
|
87
|
+
if question_type == "multiple_choice" and not q.get("choices"):
|
|
88
|
+
msg = (
|
|
89
|
+
f"multiple_choice question "
|
|
90
|
+
f"{q.get('question')!r} requires a "
|
|
91
|
+
f"non-empty 'choices' list"
|
|
92
|
+
)
|
|
93
|
+
raise ValueError(msg)
|
|
94
|
+
|
|
95
|
+
if question_type == "text" and q.get("choices"):
|
|
96
|
+
msg = f"text question {q.get('question')!r} must not define 'choices'"
|
|
97
|
+
raise ValueError(msg)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _parse_answers(
|
|
101
|
+
response: object,
|
|
102
|
+
questions: list[Question],
|
|
103
|
+
tool_call_id: str,
|
|
104
|
+
) -> Command[Any]:
|
|
105
|
+
"""Parse an interrupt response into a `Command` with a `ToolMessage`.
|
|
106
|
+
|
|
107
|
+
Supports explicit status signaling from the adapter:
|
|
108
|
+
|
|
109
|
+
- `answered` (default): consume provided `answers`
|
|
110
|
+
- `cancelled`: synthesize `(cancelled)` answers
|
|
111
|
+
- `error`: synthesize `(error: ...)` answers
|
|
112
|
+
|
|
113
|
+
Malformed payloads are converted into explicit error answers instead of
|
|
114
|
+
silently defaulting to `(no answer)`.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
response: Raw value returned by `interrupt()`.
|
|
118
|
+
questions: The questions that were asked.
|
|
119
|
+
tool_call_id: Originating tool call ID for the `ToolMessage`.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
`Command` containing a formatted `ToolMessage` with Q&A pairs.
|
|
123
|
+
"""
|
|
124
|
+
status: str = "answered"
|
|
125
|
+
error_text: str | None = None
|
|
126
|
+
answers: list[str]
|
|
127
|
+
if not isinstance(response, dict):
|
|
128
|
+
logger.error(
|
|
129
|
+
"ask_user received malformed resume payload "
|
|
130
|
+
"(expected dict, got %s); returning explicit error answers",
|
|
131
|
+
type(response).__name__,
|
|
132
|
+
)
|
|
133
|
+
answers = []
|
|
134
|
+
status = "error"
|
|
135
|
+
error_text = "invalid ask_user response payload"
|
|
136
|
+
else:
|
|
137
|
+
response_dict = cast("dict[str, Any]", response)
|
|
138
|
+
response_status = response_dict.get("status")
|
|
139
|
+
if isinstance(response_status, str):
|
|
140
|
+
status = response_status
|
|
141
|
+
|
|
142
|
+
if "answers" not in response_dict:
|
|
143
|
+
if status == "answered":
|
|
144
|
+
logger.error(
|
|
145
|
+
"ask_user received resume payload without 'answers'; "
|
|
146
|
+
"returning explicit error answers"
|
|
147
|
+
)
|
|
148
|
+
answers = []
|
|
149
|
+
status = "error"
|
|
150
|
+
error_text = "missing ask_user answers payload"
|
|
151
|
+
else:
|
|
152
|
+
answers = []
|
|
153
|
+
else:
|
|
154
|
+
raw_answers = response_dict["answers"]
|
|
155
|
+
if isinstance(raw_answers, list):
|
|
156
|
+
answers = [str(answer) for answer in raw_answers]
|
|
157
|
+
else:
|
|
158
|
+
logger.error(
|
|
159
|
+
"ask_user received non-list 'answers' payload (%s); "
|
|
160
|
+
"returning explicit error answers",
|
|
161
|
+
type(raw_answers).__name__,
|
|
162
|
+
)
|
|
163
|
+
answers = []
|
|
164
|
+
status = "error"
|
|
165
|
+
error_text = "invalid ask_user answers payload"
|
|
166
|
+
|
|
167
|
+
if status == "error":
|
|
168
|
+
response_error = response_dict.get("error")
|
|
169
|
+
if isinstance(response_error, str) and response_error:
|
|
170
|
+
error_text = response_error
|
|
171
|
+
elif status == "cancelled":
|
|
172
|
+
answers = ["(cancelled)" for _ in questions]
|
|
173
|
+
elif status == "answered":
|
|
174
|
+
if len(answers) != len(questions):
|
|
175
|
+
logger.warning(
|
|
176
|
+
"ask_user answer count mismatch: expected %d, got %d",
|
|
177
|
+
len(questions),
|
|
178
|
+
len(answers),
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
logger.error(
|
|
182
|
+
"ask_user received unknown status %r; returning explicit error answers",
|
|
183
|
+
status,
|
|
184
|
+
)
|
|
185
|
+
answers = []
|
|
186
|
+
status = "error"
|
|
187
|
+
error_text = "invalid ask_user response status"
|
|
188
|
+
|
|
189
|
+
if status == "error":
|
|
190
|
+
detail = error_text or "ask_user interaction failed"
|
|
191
|
+
answers = [f"(error: {detail})" for _ in questions]
|
|
192
|
+
|
|
193
|
+
formatted_answers = []
|
|
194
|
+
for i, q in enumerate(questions):
|
|
195
|
+
answer = answers[i] if i < len(answers) else "(no answer)"
|
|
196
|
+
formatted_answers.append(f"Q: {q['question']}\nA: {answer}")
|
|
197
|
+
result_text = "\n\n".join(formatted_answers)
|
|
198
|
+
return Command(
|
|
199
|
+
update={
|
|
200
|
+
"messages": [ToolMessage(result_text, tool_call_id=tool_call_id)],
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class AskUserMiddleware(AgentMiddleware[Any, ContextT, ResponseT]):
|
|
206
|
+
"""Middleware that provides an ask_user tool for interactive questioning.
|
|
207
|
+
|
|
208
|
+
This middleware adds an `ask_user` tool that allows agents to ask the user
|
|
209
|
+
questions during execution. Questions can be free-form text or multiple choice.
|
|
210
|
+
The tool uses LangGraph interrupts to pause execution and wait for user input.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
def __init__(
|
|
214
|
+
self,
|
|
215
|
+
*,
|
|
216
|
+
system_prompt: str = ASK_USER_SYSTEM_PROMPT,
|
|
217
|
+
tool_description: str = ASK_USER_TOOL_DESCRIPTION,
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Initialize AskUserMiddleware.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
system_prompt: System-level instructions injected into every LLM
|
|
223
|
+
request to guide `ask_user` usage.
|
|
224
|
+
tool_description: Description string passed to the `ask_user` tool
|
|
225
|
+
decorator, visible to the LLM in the tool schema.
|
|
226
|
+
"""
|
|
227
|
+
super().__init__()
|
|
228
|
+
self.system_prompt = system_prompt
|
|
229
|
+
self.tool_description = tool_description
|
|
230
|
+
|
|
231
|
+
@tool(description=self.tool_description)
|
|
232
|
+
def _ask_user(
|
|
233
|
+
questions: list[Question],
|
|
234
|
+
tool_call_id: Annotated[str, InjectedToolCallId],
|
|
235
|
+
) -> Command[Any]:
|
|
236
|
+
"""Ask the user one or more questions.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
questions: Questions to present to the user.
|
|
240
|
+
tool_call_id: Tool call identifier injected by LangChain.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
`Command` containing the parsed user answers as a `ToolMessage`.
|
|
244
|
+
"""
|
|
245
|
+
_validate_questions(questions)
|
|
246
|
+
ask_request = AskUserRequest(
|
|
247
|
+
type="ask_user",
|
|
248
|
+
questions=questions,
|
|
249
|
+
tool_call_id=tool_call_id,
|
|
250
|
+
)
|
|
251
|
+
# interrupt() raises GraphInterrupt from INSIDE tool execution,
|
|
252
|
+
# within ToolNode's wrap_tool_call chain. Any
|
|
253
|
+
# wrap_tool_call middleware that catches exceptions MUST re-raise
|
|
254
|
+
# GraphBubbleUp — a broad `except Exception` (e.g. ToolRetryMiddleware)
|
|
255
|
+
# would swallow this interrupt and silently break ask_user.
|
|
256
|
+
response = interrupt(ask_request)
|
|
257
|
+
return _parse_answers(response, questions, tool_call_id)
|
|
258
|
+
|
|
259
|
+
_ask_user.name = "ask_user"
|
|
260
|
+
self.tools = [_ask_user]
|
|
261
|
+
|
|
262
|
+
def wrap_model_call(
|
|
263
|
+
self,
|
|
264
|
+
request: ModelRequest[ContextT],
|
|
265
|
+
handler: Callable[[ModelRequest[ContextT]], ModelResponse[ResponseT]],
|
|
266
|
+
) -> ModelResponse[ResponseT] | AIMessage:
|
|
267
|
+
"""Inject the ask_user system prompt.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Model response from the wrapped handler.
|
|
271
|
+
"""
|
|
272
|
+
if request.system_message is not None:
|
|
273
|
+
new_system_content = [
|
|
274
|
+
*request.system_message.content_blocks,
|
|
275
|
+
{"type": "text", "text": f"\n\n{self.system_prompt}"},
|
|
276
|
+
]
|
|
277
|
+
else:
|
|
278
|
+
new_system_content = [{"type": "text", "text": self.system_prompt}]
|
|
279
|
+
new_system_message = SystemMessage(
|
|
280
|
+
content=cast("list[str | dict[str, str]]", new_system_content)
|
|
281
|
+
)
|
|
282
|
+
return handler(request.override(system_message=new_system_message))
|
|
283
|
+
|
|
284
|
+
async def awrap_model_call(
|
|
285
|
+
self,
|
|
286
|
+
request: ModelRequest[ContextT],
|
|
287
|
+
handler: Callable[
|
|
288
|
+
[ModelRequest[ContextT]], Awaitable[ModelResponse[ResponseT]]
|
|
289
|
+
],
|
|
290
|
+
) -> ModelResponse[ResponseT] | AIMessage:
|
|
291
|
+
"""Inject the ask_user system prompt (async).
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Model response from the wrapped handler.
|
|
295
|
+
"""
|
|
296
|
+
if request.system_message is not None:
|
|
297
|
+
new_system_content = [
|
|
298
|
+
*request.system_message.content_blocks,
|
|
299
|
+
{"type": "text", "text": f"\n\n{self.system_prompt}"},
|
|
300
|
+
]
|
|
301
|
+
else:
|
|
302
|
+
new_system_content = [{"type": "text", "text": self.system_prompt}]
|
|
303
|
+
new_system_message = SystemMessage(
|
|
304
|
+
content=cast("list[str | dict[str, str]]", new_system_content)
|
|
305
|
+
)
|
|
306
|
+
return await handler(request.override(system_message=new_system_message))
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Shared provider auth status formatting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, assert_never
|
|
6
|
+
|
|
7
|
+
from textual.content import Content
|
|
8
|
+
|
|
9
|
+
from deepagents_code.model_config import (
|
|
10
|
+
CODEX_PROVIDER,
|
|
11
|
+
ProviderAuthSource,
|
|
12
|
+
ProviderAuthState,
|
|
13
|
+
ProviderAuthStatus,
|
|
14
|
+
resolved_env_var_name,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from deepagents_code.config import Glyphs
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def format_auth_badge(status: ProviderAuthStatus) -> Content:
|
|
22
|
+
"""Format an auth manager badge for a provider.
|
|
23
|
+
|
|
24
|
+
Used by the `/auth` manager, where each provider renders a bracketed,
|
|
25
|
+
styled badge (e.g. `[stored]`, `[env set: ANTHROPIC_API_KEY]`, `[missing]`).
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
status: Provider auth/readiness status.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
A styled badge `Content` for the auth manager surface.
|
|
32
|
+
"""
|
|
33
|
+
# The ChatGPT-OAuth codex provider has no API key, so its credential is a
|
|
34
|
+
# file-backed OAuth token rather than a stored key. Give it distinctive
|
|
35
|
+
# badges (`[chatgpt]` / `[sign in to chatgpt]`) so users don't read the
|
|
36
|
+
# generic `[stored]`/`[missing]` as a literal API key on disk.
|
|
37
|
+
if status.provider == CODEX_PROVIDER:
|
|
38
|
+
return _format_codex_badge(status)
|
|
39
|
+
|
|
40
|
+
state = status.state
|
|
41
|
+
match state:
|
|
42
|
+
case ProviderAuthState.CONFIGURED:
|
|
43
|
+
return _format_configured_badge(status)
|
|
44
|
+
case ProviderAuthState.MISSING:
|
|
45
|
+
return Content.styled("[missing]", "bold $warning")
|
|
46
|
+
case ProviderAuthState.NOT_REQUIRED:
|
|
47
|
+
return _auth_badge(status.detail or "no API key required")
|
|
48
|
+
case ProviderAuthState.IMPLICIT:
|
|
49
|
+
return _auth_badge(status.detail or "implicit auth")
|
|
50
|
+
case ProviderAuthState.MANAGED:
|
|
51
|
+
return _auth_badge(status.detail or "custom auth")
|
|
52
|
+
case ProviderAuthState.UNKNOWN:
|
|
53
|
+
return _auth_badge(status.detail or "credentials unknown", prefix="? ")
|
|
54
|
+
case _:
|
|
55
|
+
assert_never(state)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def format_auth_indicator(status: ProviderAuthStatus, glyphs: Glyphs) -> str:
|
|
59
|
+
"""Format a model selector provider-header indicator.
|
|
60
|
+
|
|
61
|
+
Used by `/model`, where the indicator is plain text shown next to the
|
|
62
|
+
provider name. Returns an empty string for `CONFIGURED` providers, which
|
|
63
|
+
need no indicator.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
status: Provider auth/readiness status.
|
|
67
|
+
glyphs: Glyph table for the active terminal mode.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Text shown next to the provider name, or an empty string when no
|
|
71
|
+
indicator should be rendered (e.g., `CONFIGURED`).
|
|
72
|
+
"""
|
|
73
|
+
state = status.state
|
|
74
|
+
match state:
|
|
75
|
+
case ProviderAuthState.CONFIGURED:
|
|
76
|
+
return ""
|
|
77
|
+
case ProviderAuthState.MISSING:
|
|
78
|
+
return f"{glyphs.warning} missing credentials"
|
|
79
|
+
case ProviderAuthState.NOT_REQUIRED:
|
|
80
|
+
return status.detail or "no API key required"
|
|
81
|
+
case ProviderAuthState.IMPLICIT:
|
|
82
|
+
return status.detail or "implicit auth"
|
|
83
|
+
case ProviderAuthState.MANAGED:
|
|
84
|
+
return status.detail or "custom auth"
|
|
85
|
+
case ProviderAuthState.UNKNOWN:
|
|
86
|
+
detail = status.detail or "credentials unknown"
|
|
87
|
+
return f"{glyphs.question} {detail}"
|
|
88
|
+
case _:
|
|
89
|
+
assert_never(state)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _auth_badge(detail: str, *, prefix: str = "") -> Content:
|
|
93
|
+
"""Format a muted auth manager badge.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
detail: Badge text inside the brackets.
|
|
97
|
+
prefix: Text prepended verbatim before `detail` inside the brackets.
|
|
98
|
+
Callers include any separator themselves (e.g. `"? "`); this
|
|
99
|
+
helper does not insert one.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Formatted auth manager badge content.
|
|
103
|
+
"""
|
|
104
|
+
return Content.assemble(
|
|
105
|
+
("[", "$text-muted"),
|
|
106
|
+
(prefix, "$text-muted"),
|
|
107
|
+
Content.styled(detail, "$text-muted"),
|
|
108
|
+
("]", "$text-muted"),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _format_codex_badge(status: ProviderAuthStatus) -> Content:
|
|
113
|
+
"""Format the auth manager badge for the ChatGPT-OAuth codex provider.
|
|
114
|
+
|
|
115
|
+
Codex signs in through ChatGPT OAuth rather than an API key, so a
|
|
116
|
+
`CONFIGURED` status renders as `[chatgpt]` (carrying the plan name when the
|
|
117
|
+
status detail includes one) and any other state renders as a
|
|
118
|
+
`[sign in to chatgpt]` prompt.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
status: The codex provider's auth status.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
A styled badge reflecting ChatGPT sign-in state.
|
|
125
|
+
"""
|
|
126
|
+
if status.state is ProviderAuthState.CONFIGURED:
|
|
127
|
+
badge_text = "[chatgpt]"
|
|
128
|
+
# `_get_codex_auth_status` encodes the plan as a trailing "(plan)" in
|
|
129
|
+
# the detail string (e.g. "signed in to ChatGPT (pro)").
|
|
130
|
+
if status.detail and (plan := _codex_plan_from_detail(status.detail)):
|
|
131
|
+
badge_text = f"[chatgpt: {plan}]"
|
|
132
|
+
return Content.styled(badge_text, "bold $success")
|
|
133
|
+
return Content.styled("[sign in to chatgpt]", "bold $warning")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _codex_plan_from_detail(detail: str) -> str | None:
|
|
137
|
+
"""Extract the ChatGPT plan from a codex auth detail string.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
detail: Human-readable codex auth status detail.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The ChatGPT plan text, or `None` when no bounded plan is present.
|
|
144
|
+
"""
|
|
145
|
+
_, marker, plan_tail = detail.partition("(")
|
|
146
|
+
if not marker:
|
|
147
|
+
return None
|
|
148
|
+
plan, marker, _ = plan_tail.partition(")")
|
|
149
|
+
if not marker or not plan:
|
|
150
|
+
return None
|
|
151
|
+
return plan
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _format_configured_badge(status: ProviderAuthStatus) -> Content:
|
|
155
|
+
"""Format the auth manager badge for a `CONFIGURED` provider.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
status: A `CONFIGURED` provider auth status.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
A styled badge naming the credential source (`[stored]` or `[env set: …]`).
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
ValueError: If the status carries no source. `ProviderAuthStatus`
|
|
165
|
+
guarantees `CONFIGURED` implies a source, so this guards against
|
|
166
|
+
that invariant being violated rather than a normal input.
|
|
167
|
+
"""
|
|
168
|
+
match status.source:
|
|
169
|
+
case ProviderAuthSource.STORED:
|
|
170
|
+
return Content.styled("[stored]", "bold $success")
|
|
171
|
+
case ProviderAuthSource.ENV:
|
|
172
|
+
if status.env_var:
|
|
173
|
+
return Content.assemble(
|
|
174
|
+
("[env set: ", "$text-muted"),
|
|
175
|
+
Content.styled(
|
|
176
|
+
resolved_env_var_name(status.env_var), "$text-muted"
|
|
177
|
+
),
|
|
178
|
+
("]", "$text-muted"),
|
|
179
|
+
)
|
|
180
|
+
return Content.styled("[env]", "$text-muted")
|
|
181
|
+
case None:
|
|
182
|
+
msg = f"CONFIGURED auth status has no source: {status!r}"
|
|
183
|
+
raise ValueError(msg)
|
|
184
|
+
case _:
|
|
185
|
+
assert_never(status.source)
|