shotgun-sh 0.1.0.dev22__py3-none-any.whl → 0.1.0.dev24__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 +95 -15
- shotgun/agents/common.py +143 -25
- shotgun/agents/conversation_history.py +56 -0
- shotgun/agents/conversation_manager.py +105 -0
- shotgun/agents/export.py +5 -2
- shotgun/agents/models.py +16 -7
- shotgun/agents/plan.py +2 -1
- shotgun/agents/research.py +2 -1
- shotgun/agents/specify.py +2 -1
- shotgun/agents/tasks.py +5 -2
- shotgun/agents/tools/file_management.py +67 -2
- shotgun/codebase/core/ingestor.py +1 -1
- shotgun/codebase/core/manager.py +106 -4
- shotgun/codebase/models.py +4 -0
- shotgun/codebase/service.py +60 -2
- shotgun/main.py +9 -1
- shotgun/prompts/agents/export.j2 +14 -11
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +6 -9
- shotgun/prompts/agents/plan.j2 +9 -13
- shotgun/prompts/agents/research.j2 +11 -14
- shotgun/prompts/agents/specify.j2 +9 -12
- shotgun/prompts/agents/state/system_state.j2 +27 -5
- shotgun/prompts/agents/tasks.j2 +12 -12
- shotgun/sdk/codebase.py +26 -2
- shotgun/sdk/services.py +0 -14
- shotgun/tui/app.py +9 -4
- shotgun/tui/screens/chat.py +80 -19
- shotgun/tui/screens/chat_screen/command_providers.py +1 -1
- shotgun/tui/screens/chat_screen/history.py +6 -0
- shotgun/tui/utils/mode_progress.py +111 -78
- {shotgun_sh-0.1.0.dev22.dist-info → shotgun_sh-0.1.0.dev24.dist-info}/METADATA +8 -9
- {shotgun_sh-0.1.0.dev22.dist-info → shotgun_sh-0.1.0.dev24.dist-info}/RECORD +35 -54
- shotgun/agents/artifact_state.py +0 -58
- shotgun/agents/tools/artifact_management.py +0 -481
- shotgun/artifacts/__init__.py +0 -17
- shotgun/artifacts/exceptions.py +0 -89
- shotgun/artifacts/manager.py +0 -530
- shotgun/artifacts/models.py +0 -334
- shotgun/artifacts/service.py +0 -463
- shotgun/artifacts/templates/__init__.py +0 -10
- shotgun/artifacts/templates/loader.py +0 -252
- shotgun/artifacts/templates/models.py +0 -136
- shotgun/artifacts/templates/plan/delivery_and_release_plan.yaml +0 -66
- shotgun/artifacts/templates/research/market_research.yaml +0 -585
- shotgun/artifacts/templates/research/sdk_comparison.yaml +0 -257
- shotgun/artifacts/templates/specify/prd.yaml +0 -331
- shotgun/artifacts/templates/specify/product_spec.yaml +0 -301
- shotgun/artifacts/utils.py +0 -76
- shotgun/prompts/agents/partials/artifact_system.j2 +0 -32
- shotgun/prompts/agents/state/artifact_templates_available.j2 +0 -20
- shotgun/prompts/agents/state/existing_artifacts_available.j2 +0 -25
- shotgun/sdk/artifact_models.py +0 -186
- shotgun/sdk/artifacts.py +0 -448
- {shotgun_sh-0.1.0.dev22.dist-info → shotgun_sh-0.1.0.dev24.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.1.0.dev22.dist-info → shotgun_sh-0.1.0.dev24.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.1.0.dev22.dist-info → shotgun_sh-0.1.0.dev24.dist-info}/licenses/LICENSE +0 -0
shotgun/agents/agent_manager.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""Agent manager for coordinating multiple AI agents with shared message history."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from collections.abc import AsyncIterable
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
|
-
from
|
|
7
|
-
|
|
4
|
+
from collections.abc import AsyncIterable, Sequence
|
|
5
|
+
from dataclasses import dataclass, field, is_dataclass, replace
|
|
6
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from shotgun.agents.conversation_history import ConversationState
|
|
8
10
|
|
|
9
11
|
from pydantic_ai import (
|
|
10
12
|
Agent,
|
|
@@ -21,6 +23,7 @@ from pydantic_ai.messages import (
|
|
|
21
23
|
FunctionToolResultEvent,
|
|
22
24
|
ModelMessage,
|
|
23
25
|
ModelRequest,
|
|
26
|
+
ModelRequestPart,
|
|
24
27
|
ModelResponse,
|
|
25
28
|
ModelResponsePart,
|
|
26
29
|
PartDeltaEvent,
|
|
@@ -33,10 +36,11 @@ from textual.message import Message
|
|
|
33
36
|
from textual.widget import Widget
|
|
34
37
|
|
|
35
38
|
from shotgun.agents.common import add_system_prompt_message, add_system_status_message
|
|
39
|
+
from shotgun.agents.models import AgentType, FileOperation
|
|
36
40
|
|
|
37
41
|
from .export import create_export_agent
|
|
38
42
|
from .history.compaction import apply_persistent_compaction
|
|
39
|
-
from .models import AgentDeps, AgentRuntimeOptions
|
|
43
|
+
from .models import AgentDeps, AgentRuntimeOptions
|
|
40
44
|
from .plan import create_plan_agent
|
|
41
45
|
from .research import create_research_agent
|
|
42
46
|
from .specify import create_specify_agent
|
|
@@ -45,16 +49,6 @@ from .tasks import create_tasks_agent
|
|
|
45
49
|
logger = logging.getLogger(__name__)
|
|
46
50
|
|
|
47
51
|
|
|
48
|
-
class AgentType(Enum):
|
|
49
|
-
"""Enumeration for available agent types (for Python < 3.11)."""
|
|
50
|
-
|
|
51
|
-
RESEARCH = "research"
|
|
52
|
-
PLAN = "plan"
|
|
53
|
-
TASKS = "tasks"
|
|
54
|
-
SPECIFY = "specify"
|
|
55
|
-
EXPORT = "export"
|
|
56
|
-
|
|
57
|
-
|
|
58
52
|
class MessageHistoryUpdated(Message):
|
|
59
53
|
"""Event posted when the message history is updated."""
|
|
60
54
|
|
|
@@ -281,6 +275,8 @@ class AgentManager(Widget):
|
|
|
281
275
|
# Start with persistent message history
|
|
282
276
|
message_history = self.message_history
|
|
283
277
|
|
|
278
|
+
deps.agent_mode = self._current_agent_type
|
|
279
|
+
|
|
284
280
|
# Add a system status message so the agent knows whats going on
|
|
285
281
|
message_history = await add_system_status_message(deps, message_history)
|
|
286
282
|
|
|
@@ -463,3 +459,87 @@ class AgentManager(Widget):
|
|
|
463
459
|
file_operations=file_operations,
|
|
464
460
|
)
|
|
465
461
|
)
|
|
462
|
+
|
|
463
|
+
def _filter_system_prompts(
|
|
464
|
+
self, messages: list[ModelMessage]
|
|
465
|
+
) -> list[ModelMessage]:
|
|
466
|
+
"""Filter out system prompts from messages for UI display.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
messages: List of messages that may contain system prompts
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
List of messages without system prompt parts
|
|
473
|
+
"""
|
|
474
|
+
from pydantic_ai.messages import SystemPromptPart
|
|
475
|
+
|
|
476
|
+
filtered_messages: list[ModelMessage] = []
|
|
477
|
+
for msg in messages:
|
|
478
|
+
parts: Sequence[ModelRequestPart] | Sequence[ModelResponsePart] | None = (
|
|
479
|
+
msg.parts if hasattr(msg, "parts") else None
|
|
480
|
+
)
|
|
481
|
+
if not parts:
|
|
482
|
+
filtered_messages.append(msg)
|
|
483
|
+
continue
|
|
484
|
+
|
|
485
|
+
non_system_parts = [
|
|
486
|
+
part for part in parts if not isinstance(part, SystemPromptPart)
|
|
487
|
+
]
|
|
488
|
+
|
|
489
|
+
if not non_system_parts:
|
|
490
|
+
# Skip messages made up entirely of system prompt parts (e.g. system message)
|
|
491
|
+
continue
|
|
492
|
+
|
|
493
|
+
if len(non_system_parts) == len(parts):
|
|
494
|
+
# Nothing was filtered – keep original message
|
|
495
|
+
filtered_messages.append(msg)
|
|
496
|
+
continue
|
|
497
|
+
|
|
498
|
+
if is_dataclass(msg):
|
|
499
|
+
filtered_messages.append(
|
|
500
|
+
# ignore types because of the convoluted Request | Response types
|
|
501
|
+
replace(msg, parts=cast(Any, non_system_parts))
|
|
502
|
+
)
|
|
503
|
+
else:
|
|
504
|
+
filtered_messages.append(msg)
|
|
505
|
+
return filtered_messages
|
|
506
|
+
|
|
507
|
+
def get_conversation_state(self) -> "ConversationState":
|
|
508
|
+
"""Get the current conversation state.
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
ConversationState object containing UI and agent messages and current type
|
|
512
|
+
"""
|
|
513
|
+
from shotgun.agents.conversation_history import ConversationState
|
|
514
|
+
|
|
515
|
+
return ConversationState(
|
|
516
|
+
agent_messages=self.message_history.copy(),
|
|
517
|
+
agent_type=self._current_agent_type.value,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
def restore_conversation_state(self, state: "ConversationState") -> None:
|
|
521
|
+
"""Restore conversation state from a saved state.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
state: ConversationState object to restore
|
|
525
|
+
"""
|
|
526
|
+
# Restore message history for agents (includes system prompts)
|
|
527
|
+
self.message_history = state.agent_messages.copy()
|
|
528
|
+
|
|
529
|
+
# Filter out system prompts for UI display
|
|
530
|
+
self.ui_message_history = self._filter_system_prompts(state.agent_messages)
|
|
531
|
+
|
|
532
|
+
# Restore agent type
|
|
533
|
+
self._current_agent_type = AgentType(state.agent_type)
|
|
534
|
+
|
|
535
|
+
# Notify listeners about the restored messages
|
|
536
|
+
self._post_messages_updated()
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
# Re-export AgentType for backward compatibility
|
|
540
|
+
__all__ = [
|
|
541
|
+
"AgentManager",
|
|
542
|
+
"AgentType",
|
|
543
|
+
"MessageHistoryUpdated",
|
|
544
|
+
"PartialResponseMessage",
|
|
545
|
+
]
|
shotgun/agents/common.py
CHANGED
|
@@ -19,10 +19,12 @@ from pydantic_ai.messages import (
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
from shotgun.agents.config import ProviderType, get_config_manager, get_provider_model
|
|
22
|
+
from shotgun.agents.models import AgentType
|
|
22
23
|
from shotgun.logging_config import get_logger
|
|
23
24
|
from shotgun.prompts import PromptLoader
|
|
24
|
-
from shotgun.sdk.services import
|
|
25
|
+
from shotgun.sdk.services import get_codebase_service
|
|
25
26
|
from shotgun.utils import ensure_shotgun_directory_exists
|
|
27
|
+
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
26
28
|
|
|
27
29
|
from .history import token_limit_compactor
|
|
28
30
|
from .history.compaction import apply_persistent_compaction
|
|
@@ -38,14 +40,7 @@ from .tools import (
|
|
|
38
40
|
retrieve_code,
|
|
39
41
|
write_file,
|
|
40
42
|
)
|
|
41
|
-
from .tools.
|
|
42
|
-
create_artifact,
|
|
43
|
-
list_artifact_templates,
|
|
44
|
-
list_artifacts,
|
|
45
|
-
read_artifact,
|
|
46
|
-
read_artifact_section,
|
|
47
|
-
write_artifact_section,
|
|
48
|
-
)
|
|
43
|
+
from .tools.file_management import AGENT_DIRECTORIES
|
|
49
44
|
|
|
50
45
|
logger = get_logger(__name__)
|
|
51
46
|
|
|
@@ -67,18 +62,23 @@ async def add_system_status_message(
|
|
|
67
62
|
Updated message history with system status message prepended
|
|
68
63
|
"""
|
|
69
64
|
message_history = message_history or []
|
|
70
|
-
|
|
65
|
+
# Only show graphs for the current working directory
|
|
66
|
+
codebase_understanding_graphs = (
|
|
67
|
+
await deps.codebase_service.list_graphs_for_directory()
|
|
68
|
+
)
|
|
71
69
|
|
|
72
|
-
#
|
|
73
|
-
|
|
70
|
+
# Get existing files for the agent
|
|
71
|
+
existing_files = get_agent_existing_files(deps.agent_mode)
|
|
74
72
|
|
|
75
|
-
|
|
73
|
+
# Extract table of contents from the agent's markdown file
|
|
74
|
+
markdown_toc = extract_markdown_toc(deps.agent_mode)
|
|
76
75
|
|
|
77
76
|
system_state = prompt_loader.render(
|
|
78
77
|
"agents/state/system_state.j2",
|
|
79
78
|
codebase_understanding_graphs=codebase_understanding_graphs,
|
|
80
79
|
is_tui_context=deps.is_tui_context,
|
|
81
|
-
|
|
80
|
+
existing_files=existing_files,
|
|
81
|
+
markdown_toc=markdown_toc,
|
|
82
82
|
)
|
|
83
83
|
|
|
84
84
|
message_history.append(
|
|
@@ -97,14 +97,17 @@ def create_base_agent(
|
|
|
97
97
|
load_codebase_understanding_tools: bool = True,
|
|
98
98
|
additional_tools: list[Any] | None = None,
|
|
99
99
|
provider: ProviderType | None = None,
|
|
100
|
+
agent_mode: AgentType | None = None,
|
|
100
101
|
) -> tuple[Agent[AgentDeps, str | DeferredToolRequests], AgentDeps]:
|
|
101
102
|
"""Create a base agent with common configuration.
|
|
102
103
|
|
|
103
104
|
Args:
|
|
104
105
|
system_prompt_fn: Function that will be decorated as system_prompt
|
|
105
106
|
agent_runtime_options: Agent runtime options for the agent
|
|
107
|
+
load_codebase_understanding_tools: Whether to load codebase understanding tools
|
|
106
108
|
additional_tools: Optional list of additional tools
|
|
107
109
|
provider: Optional provider override. If None, uses configured default
|
|
110
|
+
agent_mode: The mode of the agent (research, plan, tasks, specify, export)
|
|
108
111
|
|
|
109
112
|
Returns:
|
|
110
113
|
Tuple of (Configured Pydantic AI agent, Agent dependencies)
|
|
@@ -126,13 +129,12 @@ def create_base_agent(
|
|
|
126
129
|
|
|
127
130
|
# Create deps with model config and services
|
|
128
131
|
codebase_service = get_codebase_service()
|
|
129
|
-
artifact_service = get_artifact_service()
|
|
130
132
|
deps = AgentDeps(
|
|
131
133
|
**agent_runtime_options.model_dump(),
|
|
132
134
|
llm_model=model_config,
|
|
133
135
|
codebase_service=codebase_service,
|
|
134
|
-
artifact_service=artifact_service,
|
|
135
136
|
system_prompt_fn=system_prompt_fn,
|
|
137
|
+
agent_mode=agent_mode,
|
|
136
138
|
)
|
|
137
139
|
|
|
138
140
|
except Exception as e:
|
|
@@ -180,14 +182,6 @@ def create_base_agent(
|
|
|
180
182
|
agent.tool(append_file)
|
|
181
183
|
agent.tool(read_file)
|
|
182
184
|
|
|
183
|
-
# Register artifact management tools (always available)
|
|
184
|
-
agent.tool(create_artifact)
|
|
185
|
-
agent.tool(list_artifacts)
|
|
186
|
-
agent.tool(list_artifact_templates)
|
|
187
|
-
agent.tool(read_artifact)
|
|
188
|
-
agent.tool(read_artifact_section)
|
|
189
|
-
agent.tool(write_artifact_section)
|
|
190
|
-
|
|
191
185
|
# Register codebase understanding tools (conditional)
|
|
192
186
|
if load_codebase_understanding_tools:
|
|
193
187
|
agent.tool(query_graph)
|
|
@@ -199,10 +193,134 @@ def create_base_agent(
|
|
|
199
193
|
else:
|
|
200
194
|
logger.debug("🚫🧠 Codebase understanding tools not registered")
|
|
201
195
|
|
|
202
|
-
logger.debug("✅ Agent creation complete with
|
|
196
|
+
logger.debug("✅ Agent creation complete with codebase tools")
|
|
203
197
|
return agent, deps
|
|
204
198
|
|
|
205
199
|
|
|
200
|
+
def extract_markdown_toc(agent_mode: AgentType | None) -> str | None:
|
|
201
|
+
"""Extract table of contents from agent's markdown file.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
agent_mode: The agent mode to extract TOC for
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Formatted TOC string (up to 2000 chars) or None if not applicable
|
|
208
|
+
"""
|
|
209
|
+
# Skip for EXPORT mode or no mode
|
|
210
|
+
if (
|
|
211
|
+
not agent_mode
|
|
212
|
+
or agent_mode == AgentType.EXPORT
|
|
213
|
+
or agent_mode not in AGENT_DIRECTORIES
|
|
214
|
+
):
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
base_path = get_shotgun_base_path()
|
|
218
|
+
md_file = AGENT_DIRECTORIES[agent_mode]
|
|
219
|
+
md_path = base_path / md_file
|
|
220
|
+
|
|
221
|
+
# Check if the markdown file exists
|
|
222
|
+
if not md_path.exists():
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
content = md_path.read_text(encoding="utf-8")
|
|
227
|
+
lines = content.split("\n")
|
|
228
|
+
|
|
229
|
+
# Extract headings
|
|
230
|
+
toc_lines = []
|
|
231
|
+
for line in lines:
|
|
232
|
+
stripped = line.strip()
|
|
233
|
+
if stripped.startswith("#"):
|
|
234
|
+
# Count the heading level
|
|
235
|
+
level = 0
|
|
236
|
+
for char in stripped:
|
|
237
|
+
if char == "#":
|
|
238
|
+
level += 1
|
|
239
|
+
else:
|
|
240
|
+
break
|
|
241
|
+
|
|
242
|
+
# Get the heading text (remove the # symbols and clean up)
|
|
243
|
+
heading_text = stripped[level:].strip()
|
|
244
|
+
if heading_text:
|
|
245
|
+
# Add indentation based on level
|
|
246
|
+
indent = " " * (level - 1)
|
|
247
|
+
toc_lines.append(f"{indent}{'#' * level} {heading_text}")
|
|
248
|
+
|
|
249
|
+
if not toc_lines:
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
# Join and truncate to 2000 characters
|
|
253
|
+
toc = "\n".join(toc_lines)
|
|
254
|
+
if len(toc) > 2000:
|
|
255
|
+
toc = toc[:1997] + "..."
|
|
256
|
+
|
|
257
|
+
return toc
|
|
258
|
+
|
|
259
|
+
except Exception as e:
|
|
260
|
+
logger.debug(f"Failed to extract TOC from {md_file}: {e}")
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def get_agent_existing_files(agent_mode: AgentType | None = None) -> list[str]:
|
|
265
|
+
"""Get list of existing files for the given agent mode.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
agent_mode: The agent mode to check files for. If None, lists all files.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
List of existing file paths relative to .shotgun directory
|
|
272
|
+
"""
|
|
273
|
+
base_path = get_shotgun_base_path()
|
|
274
|
+
existing_files = []
|
|
275
|
+
|
|
276
|
+
# If no agent mode, list all files in base path and first level subdirectories
|
|
277
|
+
if agent_mode is None:
|
|
278
|
+
# List files in the root .shotgun directory
|
|
279
|
+
for item in base_path.iterdir():
|
|
280
|
+
if item.is_file():
|
|
281
|
+
existing_files.append(item.name)
|
|
282
|
+
elif item.is_dir():
|
|
283
|
+
# List files in first-level subdirectories
|
|
284
|
+
for subitem in item.iterdir():
|
|
285
|
+
if subitem.is_file():
|
|
286
|
+
relative_path = subitem.relative_to(base_path)
|
|
287
|
+
existing_files.append(str(relative_path))
|
|
288
|
+
return existing_files
|
|
289
|
+
|
|
290
|
+
# Handle specific agent modes
|
|
291
|
+
if agent_mode not in AGENT_DIRECTORIES:
|
|
292
|
+
return []
|
|
293
|
+
|
|
294
|
+
if agent_mode == AgentType.EXPORT:
|
|
295
|
+
# For export agent, list all files in exports directory
|
|
296
|
+
exports_dir = base_path / "exports"
|
|
297
|
+
if exports_dir.exists():
|
|
298
|
+
for file_path in exports_dir.rglob("*"):
|
|
299
|
+
if file_path.is_file():
|
|
300
|
+
relative_path = file_path.relative_to(base_path)
|
|
301
|
+
existing_files.append(str(relative_path))
|
|
302
|
+
else:
|
|
303
|
+
# For other agents, check both .md file and directory with same name
|
|
304
|
+
allowed_file = AGENT_DIRECTORIES[agent_mode]
|
|
305
|
+
|
|
306
|
+
# Check for the .md file
|
|
307
|
+
md_file_path = base_path / allowed_file
|
|
308
|
+
if md_file_path.exists():
|
|
309
|
+
existing_files.append(allowed_file)
|
|
310
|
+
|
|
311
|
+
# Check for directory with same base name (e.g., research/ for research.md)
|
|
312
|
+
base_name = allowed_file.replace(".md", "")
|
|
313
|
+
dir_path = base_path / base_name
|
|
314
|
+
if dir_path.exists() and dir_path.is_dir():
|
|
315
|
+
# List all files in the directory
|
|
316
|
+
for file_path in dir_path.rglob("*"):
|
|
317
|
+
if file_path.is_file():
|
|
318
|
+
relative_path = file_path.relative_to(base_path)
|
|
319
|
+
existing_files.append(str(relative_path))
|
|
320
|
+
|
|
321
|
+
return existing_files
|
|
322
|
+
|
|
323
|
+
|
|
206
324
|
def build_agent_system_prompt(
|
|
207
325
|
agent_type: str,
|
|
208
326
|
ctx: RunContext[AgentDeps],
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Models and utilities for persisting TUI conversation history."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
from pydantic_ai.messages import (
|
|
8
|
+
ModelMessage,
|
|
9
|
+
ModelMessagesTypeAdapter,
|
|
10
|
+
)
|
|
11
|
+
from pydantic_core import to_jsonable_python
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConversationState(BaseModel):
|
|
15
|
+
"""Represents the complete state of a conversation in memory."""
|
|
16
|
+
|
|
17
|
+
agent_messages: list[ModelMessage]
|
|
18
|
+
agent_type: str # Will store AgentType.value
|
|
19
|
+
|
|
20
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConversationHistory(BaseModel):
|
|
24
|
+
"""Persistent conversation history for TUI sessions."""
|
|
25
|
+
|
|
26
|
+
version: int = 1
|
|
27
|
+
agent_history: list[dict[str, Any]] = Field(
|
|
28
|
+
default_factory=list
|
|
29
|
+
) # Will store serialized ModelMessage objects
|
|
30
|
+
last_agent_model: str = "research"
|
|
31
|
+
updated_at: datetime = Field(default_factory=datetime.now)
|
|
32
|
+
|
|
33
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
34
|
+
|
|
35
|
+
def set_agent_messages(self, messages: list[ModelMessage]) -> None:
|
|
36
|
+
"""Set agent_history from a list of ModelMessage objects.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
messages: List of ModelMessage objects to serialize and store
|
|
40
|
+
"""
|
|
41
|
+
# Serialize ModelMessage list to JSON-serializable format
|
|
42
|
+
self.agent_history = to_jsonable_python(
|
|
43
|
+
messages, fallback=lambda x: str(x), exclude_none=True
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def get_agent_messages(self) -> list[ModelMessage]:
|
|
47
|
+
"""Get agent_history as a list of ModelMessage objects.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
List of deserialized ModelMessage objects
|
|
51
|
+
"""
|
|
52
|
+
if not self.agent_history:
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
# Deserialize from JSON format back to ModelMessage objects
|
|
56
|
+
return ModelMessagesTypeAdapter.validate_python(self.agent_history)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Manager for handling conversation persistence operations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from shotgun.logging_config import get_logger
|
|
7
|
+
from shotgun.utils import get_shotgun_home
|
|
8
|
+
|
|
9
|
+
from .conversation_history import ConversationHistory
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConversationManager:
|
|
15
|
+
"""Handles saving and loading conversation history."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, conversation_path: Path | None = None):
|
|
18
|
+
"""Initialize ConversationManager.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
conversation_path: Path to conversation file.
|
|
22
|
+
If None, uses default ~/.shotgun-sh/conversation.json
|
|
23
|
+
"""
|
|
24
|
+
if conversation_path is None:
|
|
25
|
+
self.conversation_path = get_shotgun_home() / "conversation.json"
|
|
26
|
+
else:
|
|
27
|
+
self.conversation_path = conversation_path
|
|
28
|
+
|
|
29
|
+
def save(self, conversation: ConversationHistory) -> None:
|
|
30
|
+
"""Save conversation history to file.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
conversation: ConversationHistory to save
|
|
34
|
+
"""
|
|
35
|
+
# Ensure directory exists
|
|
36
|
+
self.conversation_path.parent.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
# Update timestamp
|
|
40
|
+
from datetime import datetime
|
|
41
|
+
|
|
42
|
+
conversation.updated_at = datetime.now()
|
|
43
|
+
|
|
44
|
+
# Serialize to JSON using Pydantic's model_dump
|
|
45
|
+
data = conversation.model_dump(mode="json")
|
|
46
|
+
|
|
47
|
+
with open(self.conversation_path, "w", encoding="utf-8") as f:
|
|
48
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
49
|
+
|
|
50
|
+
logger.debug("Conversation saved to %s", self.conversation_path)
|
|
51
|
+
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.error(
|
|
54
|
+
"Failed to save conversation to %s: %s", self.conversation_path, e
|
|
55
|
+
)
|
|
56
|
+
# Don't raise - we don't want to interrupt the user's session
|
|
57
|
+
|
|
58
|
+
def load(self) -> ConversationHistory | None:
|
|
59
|
+
"""Load conversation history from file.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
ConversationHistory if file exists and is valid, None otherwise
|
|
63
|
+
"""
|
|
64
|
+
if not self.conversation_path.exists():
|
|
65
|
+
logger.debug("No conversation history found at %s", self.conversation_path)
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
with open(self.conversation_path, encoding="utf-8") as f:
|
|
70
|
+
data = json.load(f)
|
|
71
|
+
|
|
72
|
+
conversation = ConversationHistory.model_validate(data)
|
|
73
|
+
logger.debug(
|
|
74
|
+
"Conversation loaded from %s with %d agent messages",
|
|
75
|
+
self.conversation_path,
|
|
76
|
+
len(conversation.agent_history),
|
|
77
|
+
)
|
|
78
|
+
return conversation
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.error(
|
|
82
|
+
"Failed to load conversation from %s: %s", self.conversation_path, e
|
|
83
|
+
)
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def clear(self) -> None:
|
|
87
|
+
"""Delete the conversation history file."""
|
|
88
|
+
if self.conversation_path.exists():
|
|
89
|
+
try:
|
|
90
|
+
self.conversation_path.unlink()
|
|
91
|
+
logger.debug(
|
|
92
|
+
"Conversation history cleared at %s", self.conversation_path
|
|
93
|
+
)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error(
|
|
96
|
+
"Failed to clear conversation at %s: %s", self.conversation_path, e
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def exists(self) -> bool:
|
|
100
|
+
"""Check if a conversation history file exists.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if conversation file exists, False otherwise
|
|
104
|
+
"""
|
|
105
|
+
return self.conversation_path.exists()
|
shotgun/agents/export.py
CHANGED
|
@@ -19,7 +19,7 @@ from .common import (
|
|
|
19
19
|
create_usage_limits,
|
|
20
20
|
run_agent,
|
|
21
21
|
)
|
|
22
|
-
from .models import AgentDeps, AgentRuntimeOptions
|
|
22
|
+
from .models import AgentDeps, AgentRuntimeOptions, AgentType
|
|
23
23
|
|
|
24
24
|
logger = get_logger(__name__)
|
|
25
25
|
|
|
@@ -41,7 +41,10 @@ def create_export_agent(
|
|
|
41
41
|
system_prompt_fn = partial(build_agent_system_prompt, "export")
|
|
42
42
|
|
|
43
43
|
agent, deps = create_base_agent(
|
|
44
|
-
system_prompt_fn,
|
|
44
|
+
system_prompt_fn,
|
|
45
|
+
agent_runtime_options,
|
|
46
|
+
provider=provider,
|
|
47
|
+
agent_mode=AgentType.EXPORT,
|
|
45
48
|
)
|
|
46
49
|
return agent, deps
|
|
47
50
|
|
shotgun/agents/models.py
CHANGED
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
from asyncio import Future, Queue
|
|
5
5
|
from collections.abc import Callable
|
|
6
6
|
from datetime import datetime
|
|
7
|
-
from enum import Enum
|
|
7
|
+
from enum import Enum, StrEnum
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
|
|
@@ -14,10 +14,19 @@ from pydantic_ai import RunContext
|
|
|
14
14
|
from .config.models import ModelConfig
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
|
-
from shotgun.artifacts.service import ArtifactService
|
|
18
17
|
from shotgun.codebase.service import CodebaseService
|
|
19
18
|
|
|
20
19
|
|
|
20
|
+
class AgentType(StrEnum):
|
|
21
|
+
"""Enumeration for available agent types."""
|
|
22
|
+
|
|
23
|
+
RESEARCH = "research"
|
|
24
|
+
SPECIFY = "specify"
|
|
25
|
+
PLAN = "plan"
|
|
26
|
+
TASKS = "tasks"
|
|
27
|
+
EXPORT = "export"
|
|
28
|
+
|
|
29
|
+
|
|
21
30
|
class UserAnswer(BaseModel):
|
|
22
31
|
"""A answer from the user."""
|
|
23
32
|
|
|
@@ -224,10 +233,6 @@ class AgentDeps(AgentRuntimeOptions):
|
|
|
224
233
|
description="Codebase service for code analysis tools",
|
|
225
234
|
)
|
|
226
235
|
|
|
227
|
-
artifact_service: "ArtifactService" = Field(
|
|
228
|
-
description="Artifact service for managing structured artifacts",
|
|
229
|
-
)
|
|
230
|
-
|
|
231
236
|
system_prompt_fn: Callable[[RunContext["AgentDeps"]], str] = Field(
|
|
232
237
|
description="Function that generates the system prompt for this agent",
|
|
233
238
|
)
|
|
@@ -237,10 +242,14 @@ class AgentDeps(AgentRuntimeOptions):
|
|
|
237
242
|
description="Tracker for file operations during agent run",
|
|
238
243
|
)
|
|
239
244
|
|
|
245
|
+
agent_mode: AgentType | None = Field(
|
|
246
|
+
default=None,
|
|
247
|
+
description="Current agent mode for file scoping",
|
|
248
|
+
)
|
|
249
|
+
|
|
240
250
|
|
|
241
251
|
# Rebuild model to resolve forward references after imports are available
|
|
242
252
|
try:
|
|
243
|
-
from shotgun.artifacts.service import ArtifactService
|
|
244
253
|
from shotgun.codebase.service import CodebaseService
|
|
245
254
|
|
|
246
255
|
AgentDeps.model_rebuild()
|
shotgun/agents/plan.py
CHANGED
|
@@ -19,7 +19,7 @@ from .common import (
|
|
|
19
19
|
create_usage_limits,
|
|
20
20
|
run_agent,
|
|
21
21
|
)
|
|
22
|
-
from .models import AgentDeps, AgentRuntimeOptions
|
|
22
|
+
from .models import AgentDeps, AgentRuntimeOptions, AgentType
|
|
23
23
|
|
|
24
24
|
logger = get_logger(__name__)
|
|
25
25
|
|
|
@@ -46,6 +46,7 @@ def create_plan_agent(
|
|
|
46
46
|
load_codebase_understanding_tools=True,
|
|
47
47
|
additional_tools=None,
|
|
48
48
|
provider=provider,
|
|
49
|
+
agent_mode=AgentType.PLAN,
|
|
49
50
|
)
|
|
50
51
|
return agent, deps
|
|
51
52
|
|
shotgun/agents/research.py
CHANGED
|
@@ -21,7 +21,7 @@ from .common import (
|
|
|
21
21
|
create_usage_limits,
|
|
22
22
|
run_agent,
|
|
23
23
|
)
|
|
24
|
-
from .models import AgentDeps, AgentRuntimeOptions
|
|
24
|
+
from .models import AgentDeps, AgentRuntimeOptions, AgentType
|
|
25
25
|
from .tools import get_available_web_search_tools
|
|
26
26
|
|
|
27
27
|
logger = get_logger(__name__)
|
|
@@ -60,6 +60,7 @@ def create_research_agent(
|
|
|
60
60
|
load_codebase_understanding_tools=True,
|
|
61
61
|
additional_tools=web_search_tools,
|
|
62
62
|
provider=provider,
|
|
63
|
+
agent_mode=AgentType.RESEARCH,
|
|
63
64
|
)
|
|
64
65
|
return agent, deps
|
|
65
66
|
|
shotgun/agents/specify.py
CHANGED
|
@@ -19,7 +19,7 @@ from .common import (
|
|
|
19
19
|
create_usage_limits,
|
|
20
20
|
run_agent,
|
|
21
21
|
)
|
|
22
|
-
from .models import AgentDeps, AgentRuntimeOptions
|
|
22
|
+
from .models import AgentDeps, AgentRuntimeOptions, AgentType
|
|
23
23
|
|
|
24
24
|
logger = get_logger(__name__)
|
|
25
25
|
|
|
@@ -46,6 +46,7 @@ def create_specify_agent(
|
|
|
46
46
|
load_codebase_understanding_tools=True,
|
|
47
47
|
additional_tools=None,
|
|
48
48
|
provider=provider,
|
|
49
|
+
agent_mode=AgentType.SPECIFY,
|
|
49
50
|
)
|
|
50
51
|
return agent, deps
|
|
51
52
|
|