shotgun-sh 0.2.6.dev2__py3-none-any.whl → 0.2.6.dev4__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.
Potentially problematic release.
This version of shotgun-sh might be problematic. Click here for more details.
- shotgun/agents/agent_manager.py +89 -42
- shotgun/agents/common.py +5 -47
- shotgun/agents/export.py +4 -5
- shotgun/agents/history/token_counting/anthropic.py +22 -2
- shotgun/agents/models.py +24 -0
- shotgun/agents/plan.py +4 -5
- shotgun/agents/research.py +4 -5
- shotgun/agents/specify.py +4 -5
- shotgun/agents/tasks.py +4 -5
- shotgun/agents/tools/__init__.py +0 -4
- shotgun/prompts/agents/export.j2 +18 -1
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +4 -1
- shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
- shotgun/prompts/agents/plan.j2 +1 -1
- shotgun/prompts/agents/research.j2 +1 -1
- shotgun/prompts/agents/tasks.j2 +1 -1
- shotgun/telemetry.py +8 -0
- shotgun/tui/screens/chat.py +135 -85
- shotgun/tui/screens/chat_screen/history.py +12 -88
- {shotgun_sh-0.2.6.dev2.dist-info → shotgun_sh-0.2.6.dev4.dist-info}/METADATA +1 -1
- {shotgun_sh-0.2.6.dev2.dist-info → shotgun_sh-0.2.6.dev4.dist-info}/RECORD +24 -26
- shotgun/agents/tools/ask_questions.py +0 -55
- shotgun/agents/tools/ask_user.py +0 -37
- {shotgun_sh-0.2.6.dev2.dist-info → shotgun_sh-0.2.6.dev4.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.6.dev2.dist-info → shotgun_sh-0.2.6.dev4.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.6.dev2.dist-info → shotgun_sh-0.2.6.dev4.dist-info}/licenses/LICENSE +0 -0
shotgun/prompts/agents/export.j2
CHANGED
|
@@ -124,6 +124,7 @@ content_tasks = read_file('tasks.md') # Read implementation details
|
|
|
124
124
|
- `plan.md` - Extract development approach and stages
|
|
125
125
|
- `tasks.md` - Understand implementation tasks and structure
|
|
126
126
|
2. **Map content to agents.md standard sections**:
|
|
127
|
+
- **Research, Specifications, and Planning**: ALWAYS include this section first. Check which pipeline files exist in `.shotgun/` (research.md, specification.md, plan.md, tasks.md) and list only the ones that exist. If none exist, omit this section.
|
|
127
128
|
- **Project Overview**: Brief description and key technologies from specification.md
|
|
128
129
|
- **Dev Environment Setup**: Installation, dependencies, dev server commands
|
|
129
130
|
- **Code Style Guidelines**: Coding conventions and patterns from research.md
|
|
@@ -170,6 +171,14 @@ For additional specialized exports (only if specifically requested):
|
|
|
170
171
|
<CORRECT_CONTENT_TEMPLATE>
|
|
171
172
|
# Agents.md - [Project Name]
|
|
172
173
|
|
|
174
|
+
## Research, Specifications, and Planning
|
|
175
|
+
|
|
176
|
+
The `.shotgun/` folder contains background research, specifications, and implementation planning files. Refer to these files for additional context:
|
|
177
|
+
- `research.md` - Codebase analysis and research findings
|
|
178
|
+
- `specification.md` - Project requirements and specifications
|
|
179
|
+
- `plan.md` - Development plan and implementation approach
|
|
180
|
+
- `tasks.md` - Task breakdown and implementation progress
|
|
181
|
+
|
|
173
182
|
## Project Overview
|
|
174
183
|
- Brief description of what the project does
|
|
175
184
|
- Key technologies and frameworks used
|
|
@@ -253,6 +262,14 @@ This project is about [making assumptions without reading files]...
|
|
|
253
262
|
<GOOD_CONTENT_EXAMPLE>
|
|
254
263
|
# Agents.md - E-commerce API Project
|
|
255
264
|
|
|
265
|
+
## Research, Specifications, and Planning
|
|
266
|
+
|
|
267
|
+
The `.shotgun/` folder contains background research, specifications, and implementation planning files. Refer to these files for additional context:
|
|
268
|
+
- `research.md` - Codebase analysis and research findings
|
|
269
|
+
- `specification.md` - Project requirements and specifications
|
|
270
|
+
- `plan.md` - Development plan and implementation approach
|
|
271
|
+
- `tasks.md` - Task breakdown and implementation progress
|
|
272
|
+
|
|
256
273
|
## Project Overview
|
|
257
274
|
- REST API for product catalog management with authentication
|
|
258
275
|
- Built with Python/FastAPI for high performance async operations
|
|
@@ -316,7 +333,7 @@ This project is about [making assumptions without reading files]...
|
|
|
316
333
|
USER INTERACTION - CLARIFY EXPORT REQUIREMENTS:
|
|
317
334
|
|
|
318
335
|
- ALWAYS ask clarifying questions when export requirements are unclear
|
|
319
|
-
- Use
|
|
336
|
+
- Use clarifying questions to gather specific details about:
|
|
320
337
|
- Target format and file type preferences
|
|
321
338
|
- Intended use case and audience for the export
|
|
322
339
|
- Specific content sections to include/exclude from files
|
|
@@ -7,7 +7,10 @@ Your extensive expertise spans, among other things:
|
|
|
7
7
|
## KEY RULES
|
|
8
8
|
|
|
9
9
|
{% if interactive_mode %}
|
|
10
|
-
0. Always ask CLARIFYING QUESTIONS using
|
|
10
|
+
0. Always ask CLARIFYING QUESTIONS using structured output if the user's request is ambiguous or lacks sufficient detail.
|
|
11
|
+
- Return your response with the clarifying_questions field populated
|
|
12
|
+
- Do not make assumptions about what the user wants
|
|
13
|
+
- Questions should be clear, specific, and answerable
|
|
11
14
|
{% endif %}
|
|
12
15
|
1. Above all, prefer using tools to do the work and NEVER respond with text.
|
|
13
16
|
2. IMPORTANT: Always ask for review and go ahead to move forward after using write_file().
|
|
@@ -6,20 +6,37 @@
|
|
|
6
6
|
{% if interactive_mode -%}
|
|
7
7
|
IMPORTANT: USER INTERACTION IS ENABLED (interactive mode).
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
## Structured Output Format
|
|
10
|
+
|
|
11
|
+
You must return responses using this structured format:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"response": "Your main response text here",
|
|
16
|
+
"clarifying_questions": ["Question 1?", "Question 2?"] // Optional, only when needed
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## When to Use Clarifying Questions
|
|
21
|
+
|
|
22
|
+
- BEFORE GETTING TO WORK: If the user's request is ambiguous, use clarifying_questions to ask what they want
|
|
23
|
+
- DURING WORK: After using write_file(), you can suggest that the user review it and ask any clarifying questions with clarifying_questions
|
|
12
24
|
- Don't assume - ask for confirmation of your understanding
|
|
13
|
-
- When in doubt about any aspect of the goal,
|
|
25
|
+
- When in doubt about any aspect of the goal, include clarifying_questions
|
|
26
|
+
|
|
27
|
+
## Important Notes
|
|
28
|
+
|
|
29
|
+
- If you don't need to ask questions, set clarifying_questions to null or omit it
|
|
30
|
+
- Keep response field concise - a paragraph at most for user communication
|
|
31
|
+
- Questions should be clear, specific, and independently answerable
|
|
32
|
+
- Don't ask multiple questions in one string - use separate array items
|
|
14
33
|
|
|
15
34
|
{% else -%}
|
|
16
35
|
|
|
17
36
|
IMPORTANT: USER INTERACTION IS DISABLED (non-interactive mode).
|
|
18
|
-
- You cannot ask clarifying questions
|
|
37
|
+
- You cannot ask clarifying questions (clarifying_questions will be ignored)
|
|
19
38
|
- Make reasonable assumptions based on best practices
|
|
20
39
|
- Use sensible defaults when information is missing
|
|
21
|
-
- Make reasonable assumptions based on industry best practices
|
|
22
|
-
- Use sensible defaults when specific details are not provided
|
|
23
40
|
- When in doubt, make reasonable assumptions and proceed with best practices
|
|
24
41
|
{% endif %}
|
|
25
42
|
|
shotgun/prompts/agents/plan.j2
CHANGED
|
@@ -118,7 +118,7 @@ USER INTERACTION - REDUCE UNCERTAINTY:
|
|
|
118
118
|
- FIRST read `research.md` and `specification.md` before asking ANY questions
|
|
119
119
|
- ONLY ask clarifying questions AFTER reading the context files
|
|
120
120
|
- Questions should be about gaps not covered in research/specification
|
|
121
|
-
- Use
|
|
121
|
+
- Use clarifying questions to gather specific details about:
|
|
122
122
|
- Information not found in the context files
|
|
123
123
|
- Clarifications on ambiguous specifications
|
|
124
124
|
- Priorities when multiple options exist
|
|
@@ -39,7 +39,7 @@ For research tasks:
|
|
|
39
39
|
## RESEARCH PRINCIPLES
|
|
40
40
|
|
|
41
41
|
{% if interactive_mode -%}
|
|
42
|
-
- CRITICAL: BEFORE RUNNING ANY SEARCH TOOL, ASK THE USER FOR APPROVAL
|
|
42
|
+
- CRITICAL: BEFORE RUNNING ANY SEARCH TOOL, ASK THE USER FOR APPROVAL using clarifying questions. Include what you plan to search for and ask if they want you to proceed.
|
|
43
43
|
{% endif -%}
|
|
44
44
|
- Build upon existing research rather than starting from scratch
|
|
45
45
|
- Focus on practical, actionable information over theoretical concepts
|
shotgun/prompts/agents/tasks.j2
CHANGED
|
@@ -99,7 +99,7 @@ Then organize tasks into logical sections:
|
|
|
99
99
|
USER INTERACTION - ASK CLARIFYING QUESTIONS:
|
|
100
100
|
|
|
101
101
|
- ALWAYS ask clarifying questions when the request is vague or ambiguous
|
|
102
|
-
- Use
|
|
102
|
+
- Use clarifying questions to gather specific details about:
|
|
103
103
|
- Specific features or functionality to prioritize
|
|
104
104
|
- Technical constraints or preferences
|
|
105
105
|
- Timeline and resource constraints
|
shotgun/telemetry.py
CHANGED
|
@@ -65,6 +65,14 @@ def setup_logfire_observability() -> bool:
|
|
|
65
65
|
# Instrument Pydantic AI for better observability
|
|
66
66
|
logfire.instrument_pydantic_ai()
|
|
67
67
|
|
|
68
|
+
# Add LogfireLoggingHandler to root logger so logfire logs also go to file
|
|
69
|
+
import logging
|
|
70
|
+
|
|
71
|
+
root_logger = logging.getLogger()
|
|
72
|
+
logfire_handler = logfire.LogfireLoggingHandler()
|
|
73
|
+
root_logger.addHandler(logfire_handler)
|
|
74
|
+
logger.debug("Added LogfireLoggingHandler to root logger for file integration")
|
|
75
|
+
|
|
68
76
|
# Set user context using baggage for all logs and spans
|
|
69
77
|
try:
|
|
70
78
|
from opentelemetry import baggage, context
|
shotgun/tui/screens/chat.py
CHANGED
|
@@ -24,6 +24,7 @@ from textual.widgets import Button, Label, Static
|
|
|
24
24
|
|
|
25
25
|
from shotgun.agents.agent_manager import (
|
|
26
26
|
AgentManager,
|
|
27
|
+
ClarifyingQuestionsMessage,
|
|
27
28
|
MessageHistoryUpdated,
|
|
28
29
|
PartialResponseMessage,
|
|
29
30
|
)
|
|
@@ -37,8 +38,6 @@ from shotgun.agents.models import (
|
|
|
37
38
|
AgentDeps,
|
|
38
39
|
AgentType,
|
|
39
40
|
FileOperationTracker,
|
|
40
|
-
MultipleUserQuestions,
|
|
41
|
-
UserQuestion,
|
|
42
41
|
)
|
|
43
42
|
from shotgun.codebase.core.manager import CodebaseAlreadyIndexedError
|
|
44
43
|
from shotgun.codebase.models import IndexProgress, ProgressPhase
|
|
@@ -114,6 +113,18 @@ class StatusBar(Widget):
|
|
|
114
113
|
self.working = working
|
|
115
114
|
|
|
116
115
|
def render(self) -> str:
|
|
116
|
+
# Check if in Q&A mode first (highest priority)
|
|
117
|
+
try:
|
|
118
|
+
chat_screen = self.screen
|
|
119
|
+
if isinstance(chat_screen, ChatScreen) and chat_screen.qa_mode:
|
|
120
|
+
return (
|
|
121
|
+
"[$foreground-muted][bold $text]esc[/] to exit Q&A mode • "
|
|
122
|
+
"[bold $text]enter[/] to send answer • [bold $text]ctrl+j[/] for newline[/]"
|
|
123
|
+
)
|
|
124
|
+
except Exception: # noqa: S110
|
|
125
|
+
# If we can't access chat screen, continue with normal display
|
|
126
|
+
pass
|
|
127
|
+
|
|
117
128
|
if self.working:
|
|
118
129
|
return (
|
|
119
130
|
"[$foreground-muted][bold $text]esc[/] to stop • "
|
|
@@ -151,6 +162,18 @@ class ModeIndicator(Widget):
|
|
|
151
162
|
|
|
152
163
|
def render(self) -> str:
|
|
153
164
|
"""Render the mode indicator."""
|
|
165
|
+
# Check if in Q&A mode first
|
|
166
|
+
try:
|
|
167
|
+
chat_screen = self.screen
|
|
168
|
+
if isinstance(chat_screen, ChatScreen) and chat_screen.qa_mode:
|
|
169
|
+
return (
|
|
170
|
+
"[bold $text-accent]Q&A mode[/]"
|
|
171
|
+
"[$foreground-muted] (Answer the clarifying questions or ESC to cancel)[/]"
|
|
172
|
+
)
|
|
173
|
+
except Exception: # noqa: S110
|
|
174
|
+
# If we can't access chat screen, continue with normal display
|
|
175
|
+
pass
|
|
176
|
+
|
|
154
177
|
mode_display = {
|
|
155
178
|
AgentType.RESEARCH: "Research",
|
|
156
179
|
AgentType.PLAN: "Planning",
|
|
@@ -258,11 +281,16 @@ class ChatScreen(Screen[None]):
|
|
|
258
281
|
history: PromptHistory = PromptHistory()
|
|
259
282
|
messages = reactive(list[ModelMessage | HintMessage]())
|
|
260
283
|
working = reactive(False)
|
|
261
|
-
question: reactive[UserQuestion | MultipleUserQuestions | None] = reactive(None)
|
|
262
284
|
indexing_job: reactive[CodebaseIndexSelection | None] = reactive(None)
|
|
263
285
|
partial_message: reactive[ModelMessage | None] = reactive(None)
|
|
264
286
|
_current_worker = None # Track the current running worker for cancellation
|
|
265
287
|
|
|
288
|
+
# Q&A mode state (for structured output clarifying questions)
|
|
289
|
+
qa_mode = reactive(False)
|
|
290
|
+
qa_questions: list[str] = []
|
|
291
|
+
qa_current_index = reactive(0)
|
|
292
|
+
qa_answers: list[str] = []
|
|
293
|
+
|
|
266
294
|
def __init__(self, continue_session: bool = False) -> None:
|
|
267
295
|
super().__init__()
|
|
268
296
|
# Get the model configuration and services
|
|
@@ -302,11 +330,19 @@ class ChatScreen(Screen[None]):
|
|
|
302
330
|
self._load_conversation()
|
|
303
331
|
|
|
304
332
|
self.call_later(self.check_if_codebase_is_indexed)
|
|
305
|
-
# Start the question listener worker to handle ask_user interactions
|
|
306
|
-
self.call_later(self.add_question_listener)
|
|
307
333
|
|
|
308
334
|
async def on_key(self, event: events.Key) -> None:
|
|
309
335
|
"""Handle key presses for cancellation."""
|
|
336
|
+
# If escape is pressed during Q&A mode, exit Q&A
|
|
337
|
+
if event.key in (Keys.Escape, Keys.ControlC) and self.qa_mode:
|
|
338
|
+
self._exit_qa_mode()
|
|
339
|
+
# Re-enable the input
|
|
340
|
+
prompt_input = self.query_one(PromptInput)
|
|
341
|
+
prompt_input.focus()
|
|
342
|
+
# Prevent the event from propagating (don't quit the app)
|
|
343
|
+
event.stop()
|
|
344
|
+
return
|
|
345
|
+
|
|
310
346
|
# If escape or ctrl+c is pressed while agent is working, cancel the operation
|
|
311
347
|
if (
|
|
312
348
|
event.key in (Keys.Escape, Keys.ControlC)
|
|
@@ -392,6 +428,17 @@ class ChatScreen(Screen[None]):
|
|
|
392
428
|
status_bar.working = is_working
|
|
393
429
|
status_bar.refresh()
|
|
394
430
|
|
|
431
|
+
def watch_qa_mode(self, qa_mode_active: bool) -> None:
|
|
432
|
+
"""Update UI when Q&A mode state changes."""
|
|
433
|
+
if self.is_mounted:
|
|
434
|
+
# Update status bar to show "ESC to exit Q&A mode"
|
|
435
|
+
status_bar = self.query_one(StatusBar)
|
|
436
|
+
status_bar.refresh()
|
|
437
|
+
|
|
438
|
+
# Update mode indicator to show "Q&A mode"
|
|
439
|
+
mode_indicator = self.query_one(ModeIndicator)
|
|
440
|
+
mode_indicator.refresh()
|
|
441
|
+
|
|
395
442
|
def watch_messages(self, messages: list[ModelMessage | HintMessage]) -> None:
|
|
396
443
|
"""Update the chat history when messages change."""
|
|
397
444
|
if self.is_mounted:
|
|
@@ -399,6 +446,15 @@ class ChatScreen(Screen[None]):
|
|
|
399
446
|
chat_history.update_messages(messages)
|
|
400
447
|
|
|
401
448
|
def action_toggle_mode(self) -> None:
|
|
449
|
+
# Prevent mode switching during Q&A
|
|
450
|
+
if self.qa_mode:
|
|
451
|
+
self.notify(
|
|
452
|
+
"Cannot switch modes while answering questions",
|
|
453
|
+
severity="warning",
|
|
454
|
+
timeout=3,
|
|
455
|
+
)
|
|
456
|
+
return
|
|
457
|
+
|
|
402
458
|
modes = [
|
|
403
459
|
AgentType.RESEARCH,
|
|
404
460
|
AgentType.SPECIFY,
|
|
@@ -419,44 +475,6 @@ class ChatScreen(Screen[None]):
|
|
|
419
475
|
else:
|
|
420
476
|
self.notify("No usage hint available", severity="error")
|
|
421
477
|
|
|
422
|
-
@work
|
|
423
|
-
async def add_question_listener(self) -> None:
|
|
424
|
-
while True:
|
|
425
|
-
question = await self.deps.queue.get()
|
|
426
|
-
|
|
427
|
-
if isinstance(question, MultipleUserQuestions):
|
|
428
|
-
# Set question state - handle_submit will add Q&A to chat
|
|
429
|
-
question.current_index = 0
|
|
430
|
-
self.question = question
|
|
431
|
-
|
|
432
|
-
# Show intro message with total question count
|
|
433
|
-
num_questions = len(question.questions)
|
|
434
|
-
self.agent_manager.add_hint_message(
|
|
435
|
-
HintMessage(message=f"I'm going to ask {num_questions} questions:")
|
|
436
|
-
)
|
|
437
|
-
|
|
438
|
-
# Show all questions in a numbered list so users can see what's coming
|
|
439
|
-
if question.questions:
|
|
440
|
-
questions_list = "\n".join(
|
|
441
|
-
f"{i + 1}. {q}" for i, q in enumerate(question.questions)
|
|
442
|
-
)
|
|
443
|
-
self.agent_manager.add_hint_message(
|
|
444
|
-
HintMessage(message=questions_list)
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
# Now show the first question prompt to indicate where to start answering
|
|
448
|
-
first_q = question.questions[0]
|
|
449
|
-
self.agent_manager.add_hint_message(
|
|
450
|
-
HintMessage(message=f"**Q1:** {first_q}")
|
|
451
|
-
)
|
|
452
|
-
else:
|
|
453
|
-
# Handle single question (original behavior)
|
|
454
|
-
self.question = question
|
|
455
|
-
await question.result
|
|
456
|
-
self.question = None
|
|
457
|
-
|
|
458
|
-
self.deps.queue.task_done()
|
|
459
|
-
|
|
460
478
|
def compose(self) -> ComposeResult:
|
|
461
479
|
"""Create child widgets for the app."""
|
|
462
480
|
with Container(id="window"):
|
|
@@ -502,6 +520,42 @@ class ChatScreen(Screen[None]):
|
|
|
502
520
|
partial_response_widget = self.query_one(ChatHistory)
|
|
503
521
|
partial_response_widget.partial_response = None
|
|
504
522
|
|
|
523
|
+
def _exit_qa_mode(self) -> None:
|
|
524
|
+
"""Exit Q&A mode and clean up state."""
|
|
525
|
+
# Track cancellation event
|
|
526
|
+
track_event(
|
|
527
|
+
"qa_mode_cancelled",
|
|
528
|
+
{
|
|
529
|
+
"questions_total": len(self.qa_questions),
|
|
530
|
+
"questions_answered": len(self.qa_answers),
|
|
531
|
+
},
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
# Clear Q&A state
|
|
535
|
+
self.qa_mode = False
|
|
536
|
+
self.qa_questions = []
|
|
537
|
+
self.qa_answers = []
|
|
538
|
+
self.qa_current_index = 0
|
|
539
|
+
|
|
540
|
+
# Show cancellation message
|
|
541
|
+
self.mount_hint("⚠️ Q&A cancelled - You can continue the conversation.")
|
|
542
|
+
|
|
543
|
+
@on(ClarifyingQuestionsMessage)
|
|
544
|
+
def handle_clarifying_questions(self, event: ClarifyingQuestionsMessage) -> None:
|
|
545
|
+
"""Handle clarifying questions from agent structured output.
|
|
546
|
+
|
|
547
|
+
Note: Hints are now added synchronously in agent_manager.run() before this
|
|
548
|
+
handler is called, so we only need to set up Q&A mode state here.
|
|
549
|
+
"""
|
|
550
|
+
# Clear any streaming partial response (removes final_result JSON)
|
|
551
|
+
self._clear_partial_response()
|
|
552
|
+
|
|
553
|
+
# Enter Q&A mode
|
|
554
|
+
self.qa_mode = True
|
|
555
|
+
self.qa_questions = event.questions
|
|
556
|
+
self.qa_current_index = 0
|
|
557
|
+
self.qa_answers = []
|
|
558
|
+
|
|
505
559
|
@on(MessageHistoryUpdated)
|
|
506
560
|
def handle_message_history_updated(self, event: MessageHistoryUpdated) -> None:
|
|
507
561
|
"""Handle message history updates from the agent manager."""
|
|
@@ -548,8 +602,6 @@ class ChatScreen(Screen[None]):
|
|
|
548
602
|
|
|
549
603
|
@on(PromptInput.Submitted)
|
|
550
604
|
async def handle_submit(self, message: PromptInput.Submitted) -> None:
|
|
551
|
-
from shotgun.agents.models import UserAnswer
|
|
552
|
-
|
|
553
605
|
text = message.text.strip()
|
|
554
606
|
|
|
555
607
|
# If empty text, just clear input and return
|
|
@@ -559,58 +611,56 @@ class ChatScreen(Screen[None]):
|
|
|
559
611
|
self.value = ""
|
|
560
612
|
return
|
|
561
613
|
|
|
562
|
-
#
|
|
563
|
-
if self.
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
# Q1 already shown by handle_message_history_updated,
|
|
567
|
-
# Q2+ shown after previous answer. So we only need to add the answer to chat
|
|
568
|
-
self.agent_manager.add_hint_message(
|
|
569
|
-
HintMessage(message=f"**A{q_num}:** {text}")
|
|
570
|
-
)
|
|
614
|
+
# Handle Q&A mode (from structured output clarifying questions)
|
|
615
|
+
if self.qa_mode and self.qa_questions:
|
|
616
|
+
# Collect answer
|
|
617
|
+
self.qa_answers.append(text)
|
|
571
618
|
|
|
572
|
-
#
|
|
573
|
-
self.
|
|
619
|
+
# Show answer
|
|
620
|
+
if len(self.qa_questions) == 1:
|
|
621
|
+
self.agent_manager.add_hint_message(
|
|
622
|
+
HintMessage(message=f"**A:** {text}")
|
|
623
|
+
)
|
|
624
|
+
else:
|
|
625
|
+
q_num = self.qa_current_index + 1
|
|
626
|
+
self.agent_manager.add_hint_message(
|
|
627
|
+
HintMessage(message=f"**A{q_num}:** {text}")
|
|
628
|
+
)
|
|
574
629
|
|
|
575
|
-
# Move to next
|
|
576
|
-
self.
|
|
630
|
+
# Move to next or finish
|
|
631
|
+
self.qa_current_index += 1
|
|
577
632
|
|
|
578
|
-
if self.
|
|
579
|
-
# Show
|
|
580
|
-
next_q = self.
|
|
581
|
-
next_q_num = self.
|
|
633
|
+
if self.qa_current_index < len(self.qa_questions):
|
|
634
|
+
# Show next question
|
|
635
|
+
next_q = self.qa_questions[self.qa_current_index]
|
|
636
|
+
next_q_num = self.qa_current_index + 1
|
|
582
637
|
self.agent_manager.add_hint_message(
|
|
583
638
|
HintMessage(message=f"**Q{next_q_num}:** {next_q}")
|
|
584
639
|
)
|
|
585
640
|
else:
|
|
586
|
-
# All
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
641
|
+
# All answered - format and send back
|
|
642
|
+
if len(self.qa_questions) == 1:
|
|
643
|
+
# Single question - just send the answer
|
|
644
|
+
formatted_qa = f"Q: {self.qa_questions[0]}\nA: {self.qa_answers[0]}"
|
|
645
|
+
else:
|
|
646
|
+
# Multiple questions - format all Q&A pairs
|
|
647
|
+
formatted_qa = "\n\n".join(
|
|
648
|
+
f"Q{i + 1}: {q}\nA{i + 1}: {a}"
|
|
649
|
+
for i, (q, a) in enumerate(
|
|
650
|
+
zip(self.qa_questions, self.qa_answers, strict=True)
|
|
651
|
+
)
|
|
591
652
|
)
|
|
592
|
-
)
|
|
593
|
-
|
|
594
|
-
# Resolve the original future with formatted Q&A (this goes to the agent)
|
|
595
|
-
final_answer = UserAnswer(
|
|
596
|
-
answer=formatted_qa,
|
|
597
|
-
tool_call_id=self.question.tool_call_id,
|
|
598
|
-
)
|
|
599
|
-
self.question.result.set_result(final_answer)
|
|
600
|
-
|
|
601
|
-
# Clear question state
|
|
602
|
-
self.question = None
|
|
603
653
|
|
|
604
|
-
#
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
self.
|
|
654
|
+
# Exit Q&A mode
|
|
655
|
+
self.qa_mode = False
|
|
656
|
+
self.qa_questions = []
|
|
657
|
+
self.qa_answers = []
|
|
658
|
+
self.qa_current_index = 0
|
|
608
659
|
|
|
609
|
-
# Send
|
|
660
|
+
# Send answers back to agent
|
|
610
661
|
self.run_agent(formatted_qa)
|
|
611
|
-
return
|
|
612
662
|
|
|
613
|
-
# Clear input
|
|
663
|
+
# Clear input
|
|
614
664
|
prompt_input = self.query_one(PromptInput)
|
|
615
665
|
prompt_input.clear()
|
|
616
666
|
self.value = ""
|
|
@@ -19,7 +19,6 @@ from textual.reactive import reactive
|
|
|
19
19
|
from textual.widget import Widget
|
|
20
20
|
from textual.widgets import Markdown
|
|
21
21
|
|
|
22
|
-
from shotgun.agents.models import UserAnswer
|
|
23
22
|
from shotgun.tui.components.vertical_tail import VerticalTail
|
|
24
23
|
from shotgun.tui.screens.chat_screen.hint_message import HintMessage, HintMessageWidget
|
|
25
24
|
|
|
@@ -103,42 +102,8 @@ class ChatHistory(Widget):
|
|
|
103
102
|
self._rendered_count = len(filtered)
|
|
104
103
|
|
|
105
104
|
def filtered_items(self) -> Generator[ModelMessage | HintMessage, None, None]:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if isinstance(prev_item, ModelRequest) and isinstance(
|
|
110
|
-
next_item, ModelResponse
|
|
111
|
-
):
|
|
112
|
-
ask_user_tool_response_part = next(
|
|
113
|
-
(
|
|
114
|
-
part
|
|
115
|
-
for part in prev_item.parts
|
|
116
|
-
if isinstance(part, ToolReturnPart)
|
|
117
|
-
and part.tool_name in ("ask_user", "ask_questions")
|
|
118
|
-
),
|
|
119
|
-
None,
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
ask_user_part = next(
|
|
123
|
-
(
|
|
124
|
-
part
|
|
125
|
-
for part in next_item.parts
|
|
126
|
-
if isinstance(part, ToolCallPart)
|
|
127
|
-
and part.tool_name in ("ask_user", "ask_questions")
|
|
128
|
-
),
|
|
129
|
-
None,
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
if not ask_user_part or not ask_user_tool_response_part:
|
|
133
|
-
yield next_item
|
|
134
|
-
continue
|
|
135
|
-
if (
|
|
136
|
-
ask_user_tool_response_part.tool_call_id
|
|
137
|
-
== ask_user_part.tool_call_id
|
|
138
|
-
):
|
|
139
|
-
continue # don't emit tool call that happens after tool response
|
|
140
|
-
|
|
141
|
-
yield next_item
|
|
105
|
+
# Simply yield all items - no filtering needed now that ask_user/ask_questions are gone
|
|
106
|
+
yield from self.items
|
|
142
107
|
|
|
143
108
|
def update_messages(self, messages: list[ModelMessage | HintMessage]) -> None:
|
|
144
109
|
"""Update the displayed messages using incremental mounting."""
|
|
@@ -167,6 +132,9 @@ class ChatHistory(Widget):
|
|
|
167
132
|
|
|
168
133
|
self._rendered_count = len(filtered)
|
|
169
134
|
|
|
135
|
+
# Scroll to bottom to show newly added messages
|
|
136
|
+
self.vertical_tail.scroll_end(animate=False)
|
|
137
|
+
|
|
170
138
|
|
|
171
139
|
class UserQuestionWidget(Widget):
|
|
172
140
|
def __init__(self, item: ModelRequest | None) -> None:
|
|
@@ -189,13 +157,8 @@ class UserQuestionWidget(Widget):
|
|
|
189
157
|
f"**>** {part.content if isinstance(part.content, str) else ''}\n\n"
|
|
190
158
|
)
|
|
191
159
|
elif isinstance(part, ToolReturnPart):
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
else:
|
|
195
|
-
# acc += " ∟ finished\n\n" # let's not show anything yet
|
|
196
|
-
pass
|
|
197
|
-
elif isinstance(part, UserPromptPart):
|
|
198
|
-
acc += f"**>** {part.content}\n\n"
|
|
160
|
+
# Don't show tool return parts in the UI
|
|
161
|
+
pass
|
|
199
162
|
return acc
|
|
200
163
|
|
|
201
164
|
|
|
@@ -216,23 +179,15 @@ class AgentResponseWidget(Widget):
|
|
|
216
179
|
if self.item is None:
|
|
217
180
|
return ""
|
|
218
181
|
|
|
219
|
-
# Check if there's an ask_user tool call
|
|
220
|
-
has_ask_user = any(
|
|
221
|
-
isinstance(part, ToolCallPart) and part.tool_name == "ask_user"
|
|
222
|
-
for part in self.item.parts
|
|
223
|
-
)
|
|
224
|
-
|
|
225
182
|
for idx, part in enumerate(self.item.parts):
|
|
226
183
|
if isinstance(part, TextPart):
|
|
227
|
-
# Skip ALL text parts if there's an ask_user tool call
|
|
228
|
-
if has_ask_user:
|
|
229
|
-
continue
|
|
230
184
|
# Only show the circle prefix if there's actual content
|
|
231
185
|
if part.content and part.content.strip():
|
|
232
186
|
acc += f"**⏺** {part.content}\n\n"
|
|
233
187
|
elif isinstance(part, ToolCallPart):
|
|
234
188
|
parts_str = self._format_tool_call_part(part)
|
|
235
|
-
|
|
189
|
+
if parts_str: # Only add if there's actual content
|
|
190
|
+
acc += parts_str + "\n\n"
|
|
236
191
|
elif isinstance(part, BuiltinToolCallPart):
|
|
237
192
|
# Format builtin tool calls better
|
|
238
193
|
if part.tool_name and "search" in part.tool_name.lower():
|
|
@@ -286,12 +241,6 @@ class AgentResponseWidget(Widget):
|
|
|
286
241
|
return args if isinstance(args, dict) else {}
|
|
287
242
|
|
|
288
243
|
def _format_tool_call_part(self, part: ToolCallPart) -> str:
|
|
289
|
-
if part.tool_name == "ask_user":
|
|
290
|
-
return self._format_ask_user_part(part)
|
|
291
|
-
|
|
292
|
-
if part.tool_name == "ask_questions":
|
|
293
|
-
return self._format_ask_questions_part(part)
|
|
294
|
-
|
|
295
244
|
# Parse args once (handles both JSON string and dict)
|
|
296
245
|
args = self._parse_args(part.args)
|
|
297
246
|
|
|
@@ -362,10 +311,9 @@ class AgentResponseWidget(Widget):
|
|
|
362
311
|
return f"{part.tool_name}({args['section_title']})"
|
|
363
312
|
return f"{part.tool_name}()"
|
|
364
313
|
|
|
365
|
-
if part.tool_name == "
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
return f"▪ {part.tool_name}()"
|
|
314
|
+
if part.tool_name == "final_result":
|
|
315
|
+
# Hide final_result tool calls completely - they're internal Pydantic AI mechanics
|
|
316
|
+
return ""
|
|
369
317
|
|
|
370
318
|
# Default case for unrecognized tools - format args properly
|
|
371
319
|
args = self._parse_args(part.args)
|
|
@@ -385,27 +333,3 @@ class AgentResponseWidget(Widget):
|
|
|
385
333
|
return f"{part.tool_name}({args_str})"
|
|
386
334
|
else:
|
|
387
335
|
return f"{part.tool_name}()"
|
|
388
|
-
|
|
389
|
-
def _format_ask_user_part(
|
|
390
|
-
self,
|
|
391
|
-
part: ToolCallPart,
|
|
392
|
-
) -> str:
|
|
393
|
-
if isinstance(part.args, str):
|
|
394
|
-
try:
|
|
395
|
-
_args = json.loads(part.args) if part.args.strip() else {}
|
|
396
|
-
except json.JSONDecodeError:
|
|
397
|
-
_args = {}
|
|
398
|
-
else:
|
|
399
|
-
_args = part.args
|
|
400
|
-
|
|
401
|
-
if isinstance(_args, dict) and "question" in _args:
|
|
402
|
-
return f"{_args['question']}"
|
|
403
|
-
else:
|
|
404
|
-
return "❓ "
|
|
405
|
-
|
|
406
|
-
def _format_ask_questions_part(
|
|
407
|
-
self,
|
|
408
|
-
part: ToolCallPart,
|
|
409
|
-
) -> str:
|
|
410
|
-
"""Hide ask_questions tool calls - Q&A shown as HintMessages instead."""
|
|
411
|
-
return ""
|