shotgun-sh 0.1.0.dev22__py3-none-any.whl → 0.1.0.dev23__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.

Files changed (51) hide show
  1. shotgun/agents/agent_manager.py +95 -15
  2. shotgun/agents/common.py +139 -24
  3. shotgun/agents/conversation_history.py +56 -0
  4. shotgun/agents/conversation_manager.py +105 -0
  5. shotgun/agents/export.py +5 -2
  6. shotgun/agents/models.py +16 -7
  7. shotgun/agents/plan.py +2 -1
  8. shotgun/agents/research.py +2 -1
  9. shotgun/agents/specify.py +2 -1
  10. shotgun/agents/tasks.py +5 -2
  11. shotgun/agents/tools/file_management.py +67 -2
  12. shotgun/main.py +9 -1
  13. shotgun/prompts/agents/export.j2 +14 -11
  14. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +6 -9
  15. shotgun/prompts/agents/plan.j2 +9 -13
  16. shotgun/prompts/agents/research.j2 +11 -14
  17. shotgun/prompts/agents/specify.j2 +9 -12
  18. shotgun/prompts/agents/state/system_state.j2 +27 -5
  19. shotgun/prompts/agents/tasks.j2 +12 -12
  20. shotgun/sdk/services.py +0 -14
  21. shotgun/tui/app.py +9 -4
  22. shotgun/tui/screens/chat.py +57 -8
  23. shotgun/tui/screens/chat_screen/command_providers.py +1 -1
  24. shotgun/tui/screens/chat_screen/history.py +6 -0
  25. shotgun/tui/utils/mode_progress.py +111 -78
  26. {shotgun_sh-0.1.0.dev22.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/METADATA +8 -9
  27. {shotgun_sh-0.1.0.dev22.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/RECORD +30 -49
  28. shotgun/agents/artifact_state.py +0 -58
  29. shotgun/agents/tools/artifact_management.py +0 -481
  30. shotgun/artifacts/__init__.py +0 -17
  31. shotgun/artifacts/exceptions.py +0 -89
  32. shotgun/artifacts/manager.py +0 -530
  33. shotgun/artifacts/models.py +0 -334
  34. shotgun/artifacts/service.py +0 -463
  35. shotgun/artifacts/templates/__init__.py +0 -10
  36. shotgun/artifacts/templates/loader.py +0 -252
  37. shotgun/artifacts/templates/models.py +0 -136
  38. shotgun/artifacts/templates/plan/delivery_and_release_plan.yaml +0 -66
  39. shotgun/artifacts/templates/research/market_research.yaml +0 -585
  40. shotgun/artifacts/templates/research/sdk_comparison.yaml +0 -257
  41. shotgun/artifacts/templates/specify/prd.yaml +0 -331
  42. shotgun/artifacts/templates/specify/product_spec.yaml +0 -301
  43. shotgun/artifacts/utils.py +0 -76
  44. shotgun/prompts/agents/partials/artifact_system.j2 +0 -32
  45. shotgun/prompts/agents/state/artifact_templates_available.j2 +0 -20
  46. shotgun/prompts/agents/state/existing_artifacts_available.j2 +0 -25
  47. shotgun/sdk/artifact_models.py +0 -186
  48. shotgun/sdk/artifacts.py +0 -448
  49. {shotgun_sh-0.1.0.dev22.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/WHEEL +0 -0
  50. {shotgun_sh-0.1.0.dev22.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/entry_points.txt +0 -0
  51. {shotgun_sh-0.1.0.dev22.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/licenses/LICENSE +0 -0
@@ -1,481 +0,0 @@
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 section content (already contains title header from file storage)
351
- logger.debug(
352
- "📄 Read section %d with %d characters",
353
- section_number,
354
- len(section.content),
355
- )
356
- return section.content
357
-
358
- except Exception as e:
359
- error_msg = (
360
- f"Failed to read section {section_number} "
361
- f"from artifact '{artifact_id}': {str(e)}"
362
- )
363
- logger.error("❌ Read artifact section failed: %s", error_msg)
364
- return f"Error: {error_msg}"
365
-
366
-
367
- async def list_artifacts(
368
- ctx: RunContext[AgentDeps],
369
- agent_mode: str | None = None,
370
- ) -> str:
371
- """List all artifacts, optionally filtered by agent mode.
372
-
373
- Args:
374
- ctx: RunContext containing AgentDeps with artifact service
375
- agent_mode: Optional agent mode filter (research, plan, tasks)
376
-
377
- Returns:
378
- Formatted list of artifacts or error message
379
-
380
- Example:
381
- list_artifacts("research")
382
- list_artifacts() # List all artifacts
383
- """
384
- logger.debug("🔧 Listing artifacts for mode: %s", agent_mode or "all")
385
-
386
- try:
387
- service = ctx.deps.artifact_service
388
-
389
- mode = None
390
- if agent_mode:
391
- mode, error_msg = handle_agent_mode_parsing(agent_mode)
392
- if error_msg:
393
- logger.error("❌ List artifacts failed: %s", error_msg)
394
- return f"Error: {error_msg}"
395
-
396
- summaries = service.list_artifacts(mode)
397
-
398
- if not summaries:
399
- mode_text = f" for {agent_mode}" if agent_mode else ""
400
- return f"No artifacts found{mode_text}."
401
-
402
- # Format as table
403
- lines = [
404
- f"{'Agent':<10} {'ID':<25} {'Sections':<8} {'Updated'}",
405
- "-" * 55,
406
- ]
407
-
408
- for summary in summaries:
409
- lines.append(
410
- f"{summary.agent_mode.value:<10} "
411
- f"{summary.artifact_id[:25]:<25} "
412
- f"{summary.section_count:<8} "
413
- f"{summary.updated_at.strftime('%Y-%m-%d')}"
414
- )
415
-
416
- if len(summaries) > 0:
417
- lines.append(f"\nTotal: {len(summaries)} artifacts")
418
-
419
- result = "\n".join(lines)
420
- logger.debug("📄 Listed %d artifacts", len(summaries))
421
- return result
422
-
423
- except Exception as e:
424
- error_msg = f"Failed to list artifacts: {str(e)}"
425
- logger.error("❌ List artifacts failed: %s", error_msg)
426
- return f"Error: {error_msg}"
427
-
428
-
429
- async def list_artifact_templates(
430
- ctx: RunContext[AgentDeps],
431
- agent_mode: str | None = None,
432
- ) -> str:
433
- """List available artifact templates, optionally filtered by agent mode.
434
-
435
- Args:
436
- ctx: RunContext containing AgentDeps with artifact service
437
- agent_mode: Optional agent mode filter (research, plan, tasks)
438
-
439
- Returns:
440
- Formatted list of templates or error message
441
-
442
- Example:
443
- list_artifact_templates("research")
444
- list_artifact_templates() # List all templates
445
- """
446
- logger.debug("🔧 Listing templates for mode: %s", agent_mode or "all")
447
-
448
- try:
449
- service = ctx.deps.artifact_service
450
-
451
- mode = None
452
- if agent_mode:
453
- mode, error_msg = handle_agent_mode_parsing(agent_mode)
454
- if error_msg:
455
- logger.error("❌ List templates failed: %s", error_msg)
456
- return f"Error: {error_msg}"
457
-
458
- templates = service.list_templates(mode)
459
-
460
- if not templates:
461
- mode_text = f" for {agent_mode}" if agent_mode else ""
462
- return f"No templates found{mode_text}."
463
-
464
- # Format as list with template details
465
- lines = ["Available Templates:"]
466
-
467
- for template in templates:
468
- lines.append(f"\n• {template.template_id}")
469
- lines.append(f" Name: {template.name}")
470
- lines.append(f" Mode: {template.agent_mode.value}")
471
- lines.append(f" Purpose: {template.purpose}")
472
- lines.append(f" Sections: {template.section_count}")
473
-
474
- result = "\n".join(lines)
475
- logger.debug("📄 Listed %d templates", len(templates))
476
- return result
477
-
478
- except Exception as e:
479
- error_msg = f"Failed to list templates: {str(e)}"
480
- logger.error("❌ List templates failed: %s", error_msg)
481
- return f"Error: {error_msg}"
@@ -1,17 +0,0 @@
1
- """Artifact system for managing structured content in .shotgun directory."""
2
-
3
- __all__ = [
4
- "ArtifactService",
5
- "ArtifactManager",
6
- "Artifact",
7
- "ArtifactSection",
8
- "ArtifactSummary",
9
- "AgentMode",
10
- "generate_artifact_name",
11
- "parse_agent_mode_string",
12
- ]
13
-
14
- from .manager import ArtifactManager
15
- from .models import AgentMode, Artifact, ArtifactSection, ArtifactSummary
16
- from .service import ArtifactService
17
- from .utils import generate_artifact_name, parse_agent_mode_string
@@ -1,89 +0,0 @@
1
- """Exception classes for the artifact system."""
2
-
3
-
4
- class ArtifactError(Exception):
5
- """Base exception for all artifact-related errors."""
6
-
7
-
8
- class ArtifactNotFoundError(ArtifactError):
9
- """Raised when an artifact is not found."""
10
-
11
- def __init__(self, artifact_id: str, agent_mode: str | None = None) -> None:
12
- if agent_mode:
13
- message = f"Artifact '{artifact_id}' not found in agent mode '{agent_mode}'"
14
- else:
15
- message = f"Artifact '{artifact_id}' not found"
16
- super().__init__(message)
17
- self.artifact_id = artifact_id
18
- self.agent_mode = agent_mode
19
-
20
-
21
- class SectionNotFoundError(ArtifactError):
22
- """Raised when a section is not found within an artifact."""
23
-
24
- def __init__(self, section_identifier: str | int, artifact_id: str) -> None:
25
- message = (
26
- f"Section '{section_identifier}' not found in artifact '{artifact_id}'"
27
- )
28
- super().__init__(message)
29
- self.section_identifier = section_identifier
30
- self.artifact_id = artifact_id
31
-
32
-
33
- class SectionAlreadyExistsError(ArtifactError):
34
- """Raised when trying to create a section that already exists."""
35
-
36
- def __init__(self, section_identifier: str | int, artifact_id: str) -> None:
37
- message = (
38
- f"Section '{section_identifier}' already exists in artifact '{artifact_id}'"
39
- )
40
- super().__init__(message)
41
- self.section_identifier = section_identifier
42
- self.artifact_id = artifact_id
43
-
44
-
45
- class ArtifactAlreadyExistsError(ArtifactError):
46
- """Raised when trying to create an artifact that already exists."""
47
-
48
- def __init__(self, artifact_id: str, agent_mode: str) -> None:
49
- message = (
50
- f"Artifact '{artifact_id}' already exists in agent mode '{agent_mode}'"
51
- )
52
- super().__init__(message)
53
- self.artifact_id = artifact_id
54
- self.agent_mode = agent_mode
55
-
56
-
57
- class InvalidArtifactPathError(ArtifactError):
58
- """Raised when an artifact path is invalid or outside allowed directories."""
59
-
60
- def __init__(self, path: str, reason: str | None = None) -> None:
61
- message = f"Invalid artifact path: {path}"
62
- if reason:
63
- message += f" - {reason}"
64
- super().__init__(message)
65
- self.path = path
66
- self.reason = reason
67
-
68
-
69
- class ArtifactFileSystemError(ArtifactError):
70
- """Raised when file system operations fail."""
71
-
72
- def __init__(self, operation: str, path: str, reason: str) -> None:
73
- message = f"File system error during {operation} on '{path}': {reason}"
74
- super().__init__(message)
75
- self.operation = operation
76
- self.path = path
77
- self.reason = reason
78
-
79
-
80
- class ArtifactValidationError(ArtifactError):
81
- """Raised when artifact data validation fails."""
82
-
83
- def __init__(self, message: str, field: str | None = None) -> None:
84
- if field:
85
- message = f"Validation error for field '{field}': {message}"
86
- else:
87
- message = f"Validation error: {message}"
88
- super().__init__(message)
89
- self.field = field