shotgun-sh 0.1.0.dev12__py3-none-any.whl → 0.1.0.dev14__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 +16 -3
- shotgun/agents/artifact_state.py +58 -0
- shotgun/agents/common.py +137 -88
- shotgun/agents/config/constants.py +18 -0
- shotgun/agents/config/manager.py +68 -16
- shotgun/agents/config/models.py +61 -0
- shotgun/agents/config/provider.py +11 -6
- shotgun/agents/history/compaction.py +85 -0
- shotgun/agents/history/constants.py +19 -0
- shotgun/agents/history/context_extraction.py +108 -0
- shotgun/agents/history/history_building.py +104 -0
- shotgun/agents/history/history_processors.py +354 -157
- shotgun/agents/history/message_utils.py +46 -0
- shotgun/agents/history/token_counting.py +429 -0
- shotgun/agents/history/token_estimation.py +138 -0
- shotgun/agents/models.py +131 -1
- shotgun/agents/plan.py +15 -37
- shotgun/agents/research.py +10 -45
- shotgun/agents/specify.py +97 -0
- shotgun/agents/tasks.py +7 -36
- shotgun/agents/tools/artifact_management.py +482 -0
- shotgun/agents/tools/file_management.py +31 -12
- shotgun/agents/tools/web_search/anthropic.py +78 -17
- shotgun/agents/tools/web_search/gemini.py +1 -1
- shotgun/agents/tools/web_search/openai.py +16 -2
- shotgun/artifacts/__init__.py +17 -0
- shotgun/artifacts/exceptions.py +89 -0
- shotgun/artifacts/manager.py +530 -0
- shotgun/artifacts/models.py +334 -0
- shotgun/artifacts/service.py +463 -0
- shotgun/artifacts/templates/__init__.py +10 -0
- shotgun/artifacts/templates/loader.py +252 -0
- shotgun/artifacts/templates/models.py +136 -0
- shotgun/artifacts/templates/plan/delivery_and_release_plan.yaml +66 -0
- shotgun/artifacts/templates/research/market_research.yaml +585 -0
- shotgun/artifacts/templates/research/sdk_comparison.yaml +257 -0
- shotgun/artifacts/templates/specify/prd.yaml +331 -0
- shotgun/artifacts/templates/specify/product_spec.yaml +301 -0
- shotgun/artifacts/utils.py +76 -0
- shotgun/cli/plan.py +1 -4
- shotgun/cli/specify.py +69 -0
- shotgun/cli/tasks.py +0 -4
- shotgun/codebase/core/nl_query.py +4 -4
- shotgun/logging_config.py +23 -7
- shotgun/main.py +7 -6
- shotgun/prompts/agents/partials/artifact_system.j2 +35 -0
- shotgun/prompts/agents/partials/codebase_understanding.j2 +1 -2
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -2
- shotgun/prompts/agents/partials/content_formatting.j2 +65 -0
- shotgun/prompts/agents/partials/interactive_mode.j2 +10 -2
- shotgun/prompts/agents/plan.j2 +33 -32
- shotgun/prompts/agents/research.j2 +39 -29
- shotgun/prompts/agents/specify.j2 +32 -0
- shotgun/prompts/agents/state/artifact_templates_available.j2 +18 -0
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +3 -1
- shotgun/prompts/agents/state/existing_artifacts_available.j2 +23 -0
- shotgun/prompts/agents/state/system_state.j2 +9 -1
- shotgun/prompts/agents/tasks.j2 +27 -12
- shotgun/prompts/history/incremental_summarization.j2 +53 -0
- shotgun/sdk/artifact_models.py +186 -0
- shotgun/sdk/artifacts.py +448 -0
- shotgun/sdk/services.py +14 -0
- shotgun/tui/app.py +26 -7
- shotgun/tui/screens/chat.py +32 -5
- shotgun/tui/screens/directory_setup.py +113 -0
- shotgun/utils/file_system_utils.py +6 -1
- {shotgun_sh-0.1.0.dev12.dist-info → shotgun_sh-0.1.0.dev14.dist-info}/METADATA +3 -2
- shotgun_sh-0.1.0.dev14.dist-info/RECORD +138 -0
- shotgun/prompts/user/research.j2 +0 -5
- shotgun_sh-0.1.0.dev12.dist-info/RECORD +0 -104
- {shotgun_sh-0.1.0.dev12.dist-info → shotgun_sh-0.1.0.dev14.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.1.0.dev12.dist-info → shotgun_sh-0.1.0.dev14.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.1.0.dev12.dist-info → shotgun_sh-0.1.0.dev14.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""Artifact management tools for Pydantic AI agents.
|
|
2
|
+
|
|
3
|
+
These tools provide agents with the ability to create and manage structured
|
|
4
|
+
artifacts instead of flat markdown files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pydantic_ai import RunContext
|
|
8
|
+
|
|
9
|
+
from shotgun.agents.models import AgentDeps
|
|
10
|
+
from shotgun.artifacts.utils import handle_agent_mode_parsing
|
|
11
|
+
from shotgun.logging_config import setup_logger
|
|
12
|
+
|
|
13
|
+
logger = setup_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def create_artifact(
|
|
17
|
+
ctx: RunContext[AgentDeps],
|
|
18
|
+
artifact_id: str,
|
|
19
|
+
agent_mode: str,
|
|
20
|
+
name: str,
|
|
21
|
+
template_id: str = "",
|
|
22
|
+
) -> str:
|
|
23
|
+
"""Create a new artifact.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
ctx: RunContext containing AgentDeps with artifact service
|
|
27
|
+
artifact_id: Unique identifier for the artifact (slug format)
|
|
28
|
+
agent_mode: Agent mode (research, plan, tasks)
|
|
29
|
+
name: Human-readable name for the artifact
|
|
30
|
+
template_id: Optional template ID to use for creating the artifact
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Success message including template content if template was used, or error message
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
create_artifact("market-analysis", "research", "Market Analysis")
|
|
37
|
+
create_artifact("market-study", "research", "Market Study", "research/market_research")
|
|
38
|
+
"""
|
|
39
|
+
logger.debug("🔧 Creating artifact: %s/%s", agent_mode, artifact_id)
|
|
40
|
+
|
|
41
|
+
# Parse and validate agent mode
|
|
42
|
+
mode, error_msg = handle_agent_mode_parsing(agent_mode)
|
|
43
|
+
if error_msg:
|
|
44
|
+
logger.error("❌ Create artifact failed: %s", error_msg)
|
|
45
|
+
return f"Error: {error_msg}"
|
|
46
|
+
# Type checker hint: mode is validated above
|
|
47
|
+
if mode is None:
|
|
48
|
+
return "Error: Invalid agent mode"
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
service = ctx.deps.artifact_service
|
|
52
|
+
|
|
53
|
+
# Pass template_id if provided and not empty
|
|
54
|
+
template_to_use = template_id.strip() if template_id.strip() else None
|
|
55
|
+
artifact = service.create_artifact(artifact_id, mode, name, template_to_use)
|
|
56
|
+
|
|
57
|
+
# Track the artifact file creation
|
|
58
|
+
from shotgun.agents.models import FileOperationType
|
|
59
|
+
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
60
|
+
|
|
61
|
+
artifact_path = (
|
|
62
|
+
get_shotgun_base_path() / mode.value / artifact_id / "artifact.yaml"
|
|
63
|
+
)
|
|
64
|
+
ctx.deps.file_tracker.add_operation(artifact_path, FileOperationType.CREATED)
|
|
65
|
+
|
|
66
|
+
success_msg = (
|
|
67
|
+
f"Created artifact '{artifact_id}' in {agent_mode} mode with name '{name}'"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# If template was used, include template content in the response
|
|
71
|
+
if artifact.has_template():
|
|
72
|
+
template_content = artifact.load_template_from_file()
|
|
73
|
+
if template_content:
|
|
74
|
+
success_msg += f"\n\nUsing template: {template_content.get('name', artifact.get_template_id())}"
|
|
75
|
+
|
|
76
|
+
if "purpose" in template_content:
|
|
77
|
+
success_msg += f"\nPurpose: {template_content['purpose']}"
|
|
78
|
+
if "prompt" in template_content:
|
|
79
|
+
success_msg += f"\nPrompt: {template_content['prompt']}"
|
|
80
|
+
|
|
81
|
+
if "sections" in template_content and isinstance(
|
|
82
|
+
template_content["sections"], dict
|
|
83
|
+
):
|
|
84
|
+
success_msg += "\nSections to complete:"
|
|
85
|
+
|
|
86
|
+
# Sort sections by order if available
|
|
87
|
+
sections_dict = template_content["sections"]
|
|
88
|
+
sorted_sections = sorted(
|
|
89
|
+
sections_dict.items(),
|
|
90
|
+
key=lambda x: x[1].get("order", 999)
|
|
91
|
+
if isinstance(x[1], dict)
|
|
92
|
+
else 999,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
for section_key, section_info in sorted_sections:
|
|
96
|
+
if isinstance(section_info, dict):
|
|
97
|
+
instructions = section_info.get("instructions", "")
|
|
98
|
+
success_msg += f"\n- {section_key}: {instructions}"
|
|
99
|
+
if section_info.get("depends_on"):
|
|
100
|
+
depends_on = section_info["depends_on"]
|
|
101
|
+
if isinstance(depends_on, list):
|
|
102
|
+
success_msg += (
|
|
103
|
+
f" (depends on: {', '.join(depends_on)})"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
logger.debug("✅ %s", success_msg)
|
|
107
|
+
return success_msg
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
error_msg = f"Failed to create artifact '{artifact_id}': {str(e)}"
|
|
111
|
+
logger.error("❌ Create artifact failed: %s", error_msg)
|
|
112
|
+
return f"Error: {error_msg}"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def read_artifact(
|
|
116
|
+
ctx: RunContext[AgentDeps],
|
|
117
|
+
artifact_id: str,
|
|
118
|
+
agent_mode: str,
|
|
119
|
+
) -> str:
|
|
120
|
+
"""Read all sections of an artifact.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
ctx: RunContext containing AgentDeps with artifact service
|
|
124
|
+
artifact_id: Artifact identifier
|
|
125
|
+
agent_mode: Agent mode (research, plan, tasks)
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Combined content of all sections or error message
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
read_artifact("market-analysis", "research")
|
|
132
|
+
"""
|
|
133
|
+
logger.debug("🔧 Reading artifact: %s/%s", agent_mode, artifact_id)
|
|
134
|
+
|
|
135
|
+
# Parse and validate agent mode
|
|
136
|
+
mode, error_msg = handle_agent_mode_parsing(agent_mode)
|
|
137
|
+
if error_msg:
|
|
138
|
+
logger.error("❌ Read artifact failed: %s", error_msg)
|
|
139
|
+
return f"Error: {error_msg}"
|
|
140
|
+
# Type checker hint: mode is validated above
|
|
141
|
+
if mode is None:
|
|
142
|
+
return "Error: Invalid agent mode"
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
service = ctx.deps.artifact_service
|
|
146
|
+
artifact = service.get_artifact(artifact_id, mode, "")
|
|
147
|
+
|
|
148
|
+
if not artifact.sections:
|
|
149
|
+
return f"Artifact '{artifact_id}' exists but has no sections."
|
|
150
|
+
|
|
151
|
+
# Combine all sections with headers
|
|
152
|
+
content_parts = [f"# {artifact.name}\n"]
|
|
153
|
+
|
|
154
|
+
# Include template information if artifact was created from a template
|
|
155
|
+
if artifact.has_template():
|
|
156
|
+
template_content = artifact.load_template_from_file()
|
|
157
|
+
if template_content:
|
|
158
|
+
content_parts.append("\n## Template Information\n")
|
|
159
|
+
content_parts.append(f"**Template ID:** {artifact.get_template_id()}\n")
|
|
160
|
+
|
|
161
|
+
# Extract template info from the loaded template file
|
|
162
|
+
if "name" in template_content:
|
|
163
|
+
content_parts.append(f"**Template:** {template_content['name']}\n")
|
|
164
|
+
if "purpose" in template_content:
|
|
165
|
+
content_parts.append(
|
|
166
|
+
f"**Purpose:** {template_content['purpose']}\n"
|
|
167
|
+
)
|
|
168
|
+
if "prompt" in template_content:
|
|
169
|
+
content_parts.append(f"**Prompt:** {template_content['prompt']}\n")
|
|
170
|
+
|
|
171
|
+
if "sections" in template_content and isinstance(
|
|
172
|
+
template_content["sections"], dict
|
|
173
|
+
):
|
|
174
|
+
content_parts.append("\n### Template Sections:\n")
|
|
175
|
+
|
|
176
|
+
# Sort sections by order if available
|
|
177
|
+
sections_dict = template_content["sections"]
|
|
178
|
+
sorted_sections = sorted(
|
|
179
|
+
sections_dict.items(),
|
|
180
|
+
key=lambda x: x[1].get("order", 999)
|
|
181
|
+
if isinstance(x[1], dict)
|
|
182
|
+
else 999,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
for section_key, section_info in sorted_sections:
|
|
186
|
+
if isinstance(section_info, dict):
|
|
187
|
+
content_parts.append(
|
|
188
|
+
f"- **{section_key}:** {section_info.get('instructions', '')}"
|
|
189
|
+
)
|
|
190
|
+
if section_info.get("depends_on"):
|
|
191
|
+
depends_on = section_info["depends_on"]
|
|
192
|
+
if isinstance(depends_on, list):
|
|
193
|
+
content_parts.append(
|
|
194
|
+
f" *(depends on: {', '.join(depends_on)})*"
|
|
195
|
+
)
|
|
196
|
+
content_parts.append("")
|
|
197
|
+
|
|
198
|
+
for section in artifact.get_ordered_sections():
|
|
199
|
+
content_parts.append(f"\n## {section.title}\n")
|
|
200
|
+
if section.content:
|
|
201
|
+
content_parts.append(f"{section.content}\n")
|
|
202
|
+
|
|
203
|
+
combined_content = "\n".join(content_parts)
|
|
204
|
+
logger.debug(
|
|
205
|
+
"📄 Read artifact with %d sections (%d characters)",
|
|
206
|
+
len(artifact.sections),
|
|
207
|
+
len(combined_content),
|
|
208
|
+
)
|
|
209
|
+
return combined_content
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
error_msg = f"Failed to read artifact '{artifact_id}': {str(e)}"
|
|
213
|
+
logger.error("❌ Read artifact failed: %s", error_msg)
|
|
214
|
+
return f"Error: {error_msg}"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
async def write_artifact_section(
|
|
218
|
+
ctx: RunContext[AgentDeps],
|
|
219
|
+
artifact_id: str,
|
|
220
|
+
agent_mode: str,
|
|
221
|
+
section_number: int,
|
|
222
|
+
section_slug: str,
|
|
223
|
+
section_title: str,
|
|
224
|
+
content: str,
|
|
225
|
+
) -> str:
|
|
226
|
+
"""Write content to a specific section of an artifact.
|
|
227
|
+
|
|
228
|
+
Creates the artifact and/or section if they don't exist.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
ctx: RunContext containing AgentDeps with artifact service
|
|
232
|
+
artifact_id: Artifact identifier
|
|
233
|
+
agent_mode: Agent mode (research, plan, tasks)
|
|
234
|
+
section_number: Section number (1, 2, 3, etc.)
|
|
235
|
+
section_slug: URL-friendly section identifier
|
|
236
|
+
section_title: Human-readable section title
|
|
237
|
+
content: Section content in markdown
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Success message or error message
|
|
241
|
+
|
|
242
|
+
Example:
|
|
243
|
+
write_artifact_section("market-analysis", "research", 1, "overview", "Market Overview", "...")
|
|
244
|
+
"""
|
|
245
|
+
logger.debug(
|
|
246
|
+
"🔧 Writing to artifact section: %s/%s section %d",
|
|
247
|
+
agent_mode,
|
|
248
|
+
artifact_id,
|
|
249
|
+
section_number,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Parse and validate agent mode
|
|
253
|
+
mode, error_msg = handle_agent_mode_parsing(agent_mode)
|
|
254
|
+
if error_msg:
|
|
255
|
+
logger.error("❌ Write artifact section failed: %s", error_msg)
|
|
256
|
+
return f"Error: {error_msg}"
|
|
257
|
+
|
|
258
|
+
# At this point, mode is guaranteed to be not None due to successful validation
|
|
259
|
+
if mode is None:
|
|
260
|
+
return "Error: Agent mode validation failed"
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
service = ctx.deps.artifact_service
|
|
264
|
+
|
|
265
|
+
# Get or create the section
|
|
266
|
+
section, created = service.get_or_create_section(
|
|
267
|
+
artifact_id, mode, section_number, section_slug, section_title, content
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Track the section file operation
|
|
271
|
+
from shotgun.agents.models import FileOperationType
|
|
272
|
+
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
273
|
+
|
|
274
|
+
section_path = (
|
|
275
|
+
get_shotgun_base_path()
|
|
276
|
+
/ mode.value
|
|
277
|
+
/ artifact_id
|
|
278
|
+
/ f"{section_number:02d}_{section_slug}.md"
|
|
279
|
+
)
|
|
280
|
+
operation = FileOperationType.CREATED if created else FileOperationType.UPDATED
|
|
281
|
+
ctx.deps.file_tracker.add_operation(section_path, operation)
|
|
282
|
+
|
|
283
|
+
if created:
|
|
284
|
+
success_msg = (
|
|
285
|
+
f"Created section {section_number} '{section_title}' "
|
|
286
|
+
f"in artifact '{artifact_id}' with {len(content)} characters"
|
|
287
|
+
)
|
|
288
|
+
else:
|
|
289
|
+
# Update existing section content
|
|
290
|
+
service.update_section(artifact_id, mode, section_number, content=content)
|
|
291
|
+
success_msg = (
|
|
292
|
+
f"Updated section {section_number} '{section_title}' "
|
|
293
|
+
f"in artifact '{artifact_id}' with {len(content)} characters"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
logger.debug("✅ %s", success_msg)
|
|
297
|
+
return success_msg
|
|
298
|
+
|
|
299
|
+
except Exception as e:
|
|
300
|
+
error_msg = (
|
|
301
|
+
f"Failed to write section {section_number} "
|
|
302
|
+
f"to artifact '{artifact_id}': {str(e)}"
|
|
303
|
+
)
|
|
304
|
+
logger.error("❌ Write artifact section failed: %s", error_msg)
|
|
305
|
+
return f"Error: {error_msg}"
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
async def read_artifact_section(
|
|
309
|
+
ctx: RunContext[AgentDeps],
|
|
310
|
+
artifact_id: str,
|
|
311
|
+
agent_mode: str,
|
|
312
|
+
section_number: int,
|
|
313
|
+
) -> str:
|
|
314
|
+
"""Read content from a specific section of an artifact.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
ctx: RunContext containing AgentDeps with artifact service
|
|
318
|
+
artifact_id: Artifact identifier
|
|
319
|
+
agent_mode: Agent mode (research, plan, tasks)
|
|
320
|
+
section_number: Section number
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Section content or error message
|
|
324
|
+
|
|
325
|
+
Example:
|
|
326
|
+
read_artifact_section("market-analysis", "research", 1)
|
|
327
|
+
"""
|
|
328
|
+
logger.debug(
|
|
329
|
+
"🔧 Reading artifact section: %s/%s section %d",
|
|
330
|
+
agent_mode,
|
|
331
|
+
artifact_id,
|
|
332
|
+
section_number,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Parse and validate agent mode
|
|
336
|
+
mode, error_msg = handle_agent_mode_parsing(agent_mode)
|
|
337
|
+
if error_msg:
|
|
338
|
+
logger.error("❌ Read artifact section failed: %s", error_msg)
|
|
339
|
+
return f"Error: {error_msg}"
|
|
340
|
+
|
|
341
|
+
# At this point, mode is guaranteed to be not None due to successful validation
|
|
342
|
+
if mode is None:
|
|
343
|
+
return "Error: Agent mode validation failed"
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
service = ctx.deps.artifact_service
|
|
347
|
+
|
|
348
|
+
section = service.get_section(artifact_id, mode, section_number)
|
|
349
|
+
|
|
350
|
+
# Return formatted content with title
|
|
351
|
+
formatted_content = f"# {section.title}\n\n{section.content}"
|
|
352
|
+
logger.debug(
|
|
353
|
+
"📄 Read section %d with %d characters",
|
|
354
|
+
section_number,
|
|
355
|
+
len(section.content),
|
|
356
|
+
)
|
|
357
|
+
return formatted_content
|
|
358
|
+
|
|
359
|
+
except Exception as e:
|
|
360
|
+
error_msg = (
|
|
361
|
+
f"Failed to read section {section_number} "
|
|
362
|
+
f"from artifact '{artifact_id}': {str(e)}"
|
|
363
|
+
)
|
|
364
|
+
logger.error("❌ Read artifact section failed: %s", error_msg)
|
|
365
|
+
return f"Error: {error_msg}"
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
async def list_artifacts(
|
|
369
|
+
ctx: RunContext[AgentDeps],
|
|
370
|
+
agent_mode: str | None = None,
|
|
371
|
+
) -> str:
|
|
372
|
+
"""List all artifacts, optionally filtered by agent mode.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
ctx: RunContext containing AgentDeps with artifact service
|
|
376
|
+
agent_mode: Optional agent mode filter (research, plan, tasks)
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Formatted list of artifacts or error message
|
|
380
|
+
|
|
381
|
+
Example:
|
|
382
|
+
list_artifacts("research")
|
|
383
|
+
list_artifacts() # List all artifacts
|
|
384
|
+
"""
|
|
385
|
+
logger.debug("🔧 Listing artifacts for mode: %s", agent_mode or "all")
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
service = ctx.deps.artifact_service
|
|
389
|
+
|
|
390
|
+
mode = None
|
|
391
|
+
if agent_mode:
|
|
392
|
+
mode, error_msg = handle_agent_mode_parsing(agent_mode)
|
|
393
|
+
if error_msg:
|
|
394
|
+
logger.error("❌ List artifacts failed: %s", error_msg)
|
|
395
|
+
return f"Error: {error_msg}"
|
|
396
|
+
|
|
397
|
+
summaries = service.list_artifacts(mode)
|
|
398
|
+
|
|
399
|
+
if not summaries:
|
|
400
|
+
mode_text = f" for {agent_mode}" if agent_mode else ""
|
|
401
|
+
return f"No artifacts found{mode_text}."
|
|
402
|
+
|
|
403
|
+
# Format as table
|
|
404
|
+
lines = [
|
|
405
|
+
f"{'Agent':<10} {'ID':<25} {'Sections':<8} {'Updated'}",
|
|
406
|
+
"-" * 55,
|
|
407
|
+
]
|
|
408
|
+
|
|
409
|
+
for summary in summaries:
|
|
410
|
+
lines.append(
|
|
411
|
+
f"{summary.agent_mode.value:<10} "
|
|
412
|
+
f"{summary.artifact_id[:25]:<25} "
|
|
413
|
+
f"{summary.section_count:<8} "
|
|
414
|
+
f"{summary.updated_at.strftime('%Y-%m-%d')}"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
if len(summaries) > 0:
|
|
418
|
+
lines.append(f"\nTotal: {len(summaries)} artifacts")
|
|
419
|
+
|
|
420
|
+
result = "\n".join(lines)
|
|
421
|
+
logger.debug("📄 Listed %d artifacts", len(summaries))
|
|
422
|
+
return result
|
|
423
|
+
|
|
424
|
+
except Exception as e:
|
|
425
|
+
error_msg = f"Failed to list artifacts: {str(e)}"
|
|
426
|
+
logger.error("❌ List artifacts failed: %s", error_msg)
|
|
427
|
+
return f"Error: {error_msg}"
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
async def list_artifact_templates(
|
|
431
|
+
ctx: RunContext[AgentDeps],
|
|
432
|
+
agent_mode: str | None = None,
|
|
433
|
+
) -> str:
|
|
434
|
+
"""List available artifact templates, optionally filtered by agent mode.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
ctx: RunContext containing AgentDeps with artifact service
|
|
438
|
+
agent_mode: Optional agent mode filter (research, plan, tasks)
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Formatted list of templates or error message
|
|
442
|
+
|
|
443
|
+
Example:
|
|
444
|
+
list_artifact_templates("research")
|
|
445
|
+
list_artifact_templates() # List all templates
|
|
446
|
+
"""
|
|
447
|
+
logger.debug("🔧 Listing templates for mode: %s", agent_mode or "all")
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
service = ctx.deps.artifact_service
|
|
451
|
+
|
|
452
|
+
mode = None
|
|
453
|
+
if agent_mode:
|
|
454
|
+
mode, error_msg = handle_agent_mode_parsing(agent_mode)
|
|
455
|
+
if error_msg:
|
|
456
|
+
logger.error("❌ List templates failed: %s", error_msg)
|
|
457
|
+
return f"Error: {error_msg}"
|
|
458
|
+
|
|
459
|
+
templates = service.list_templates(mode)
|
|
460
|
+
|
|
461
|
+
if not templates:
|
|
462
|
+
mode_text = f" for {agent_mode}" if agent_mode else ""
|
|
463
|
+
return f"No templates found{mode_text}."
|
|
464
|
+
|
|
465
|
+
# Format as list with template details
|
|
466
|
+
lines = ["Available Templates:"]
|
|
467
|
+
|
|
468
|
+
for template in templates:
|
|
469
|
+
lines.append(f"\n• {template.template_id}")
|
|
470
|
+
lines.append(f" Name: {template.name}")
|
|
471
|
+
lines.append(f" Mode: {template.agent_mode.value}")
|
|
472
|
+
lines.append(f" Purpose: {template.purpose}")
|
|
473
|
+
lines.append(f" Sections: {template.section_count}")
|
|
474
|
+
|
|
475
|
+
result = "\n".join(lines)
|
|
476
|
+
logger.debug("📄 Listed %d templates", len(templates))
|
|
477
|
+
return result
|
|
478
|
+
|
|
479
|
+
except Exception as e:
|
|
480
|
+
error_msg = f"Failed to list templates: {str(e)}"
|
|
481
|
+
logger.error("❌ List templates failed: %s", error_msg)
|
|
482
|
+
return f"Error: {error_msg}"
|
|
@@ -6,16 +6,15 @@ These tools are restricted to the .shotgun directory for security.
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Literal
|
|
8
8
|
|
|
9
|
+
from pydantic_ai import RunContext
|
|
10
|
+
|
|
11
|
+
from shotgun.agents.models import AgentDeps, FileOperationType
|
|
9
12
|
from shotgun.logging_config import get_logger
|
|
13
|
+
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
10
14
|
|
|
11
15
|
logger = get_logger(__name__)
|
|
12
16
|
|
|
13
17
|
|
|
14
|
-
def _get_shotgun_base_path() -> Path:
|
|
15
|
-
"""Get the absolute path to the .shotgun directory."""
|
|
16
|
-
return Path.cwd() / ".shotgun"
|
|
17
|
-
|
|
18
|
-
|
|
19
18
|
def _validate_shotgun_path(filename: str) -> Path:
|
|
20
19
|
"""Validate and resolve a file path within the .shotgun directory.
|
|
21
20
|
|
|
@@ -28,7 +27,7 @@ def _validate_shotgun_path(filename: str) -> Path:
|
|
|
28
27
|
Raises:
|
|
29
28
|
ValueError: If the path attempts to access files outside .shotgun directory
|
|
30
29
|
"""
|
|
31
|
-
base_path =
|
|
30
|
+
base_path = get_shotgun_base_path()
|
|
32
31
|
|
|
33
32
|
# Create the full path
|
|
34
33
|
full_path = (base_path / filename).resolve()
|
|
@@ -44,7 +43,7 @@ def _validate_shotgun_path(filename: str) -> Path:
|
|
|
44
43
|
return full_path
|
|
45
44
|
|
|
46
45
|
|
|
47
|
-
def read_file(filename: str) -> str:
|
|
46
|
+
async def read_file(ctx: RunContext[AgentDeps], filename: str) -> str:
|
|
48
47
|
"""Read a file from the .shotgun directory.
|
|
49
48
|
|
|
50
49
|
Args:
|
|
@@ -75,7 +74,12 @@ def read_file(filename: str) -> str:
|
|
|
75
74
|
return error_msg
|
|
76
75
|
|
|
77
76
|
|
|
78
|
-
def write_file(
|
|
77
|
+
async def write_file(
|
|
78
|
+
ctx: RunContext[AgentDeps],
|
|
79
|
+
filename: str,
|
|
80
|
+
content: str,
|
|
81
|
+
mode: Literal["w", "a"] = "w",
|
|
82
|
+
) -> str:
|
|
79
83
|
"""Write content to a file in the .shotgun directory.
|
|
80
84
|
|
|
81
85
|
Args:
|
|
@@ -97,6 +101,16 @@ def write_file(filename: str, content: str, mode: Literal["w", "a"] = "w") -> st
|
|
|
97
101
|
try:
|
|
98
102
|
file_path = _validate_shotgun_path(filename)
|
|
99
103
|
|
|
104
|
+
# Determine operation type
|
|
105
|
+
if mode == "a":
|
|
106
|
+
operation = FileOperationType.UPDATED
|
|
107
|
+
else:
|
|
108
|
+
operation = (
|
|
109
|
+
FileOperationType.CREATED
|
|
110
|
+
if not file_path.exists()
|
|
111
|
+
else FileOperationType.UPDATED
|
|
112
|
+
)
|
|
113
|
+
|
|
100
114
|
# Ensure parent directory exists
|
|
101
115
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
102
116
|
|
|
@@ -105,11 +119,16 @@ def write_file(filename: str, content: str, mode: Literal["w", "a"] = "w") -> st
|
|
|
105
119
|
with open(file_path, "a", encoding="utf-8") as f:
|
|
106
120
|
f.write(content)
|
|
107
121
|
logger.debug("📄 Appended %d characters to %s", len(content), filename)
|
|
108
|
-
|
|
122
|
+
result = f"Successfully appended {len(content)} characters to {filename}"
|
|
109
123
|
else:
|
|
110
124
|
file_path.write_text(content, encoding="utf-8")
|
|
111
125
|
logger.debug("📄 Wrote %d characters to %s", len(content), filename)
|
|
112
|
-
|
|
126
|
+
result = f"Successfully wrote {len(content)} characters to {filename}"
|
|
127
|
+
|
|
128
|
+
# Track the file operation
|
|
129
|
+
ctx.deps.file_tracker.add_operation(file_path, operation)
|
|
130
|
+
|
|
131
|
+
return result
|
|
113
132
|
|
|
114
133
|
except Exception as e:
|
|
115
134
|
error_msg = f"Error writing file '{filename}': {str(e)}"
|
|
@@ -117,7 +136,7 @@ def write_file(filename: str, content: str, mode: Literal["w", "a"] = "w") -> st
|
|
|
117
136
|
return error_msg
|
|
118
137
|
|
|
119
138
|
|
|
120
|
-
def append_file(filename: str, content: str) -> str:
|
|
139
|
+
async def append_file(ctx: RunContext[AgentDeps], filename: str, content: str) -> str:
|
|
121
140
|
"""Append content to a file in the .shotgun directory.
|
|
122
141
|
|
|
123
142
|
Args:
|
|
@@ -127,4 +146,4 @@ def append_file(filename: str, content: str) -> str:
|
|
|
127
146
|
Returns:
|
|
128
147
|
Success message or error message
|
|
129
148
|
"""
|
|
130
|
-
return write_file(filename, content, mode="a")
|
|
149
|
+
return await write_file(ctx, filename, content, mode="a")
|