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,186 @@
|
|
|
1
|
+
"""Result models for SDK artifact operations."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from shotgun.artifacts.models import AgentMode, Artifact, ArtifactSummary
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ArtifactListResult(BaseModel):
|
|
9
|
+
"""Result for artifact list command."""
|
|
10
|
+
|
|
11
|
+
artifacts: list[ArtifactSummary]
|
|
12
|
+
agent_mode: AgentMode | None = None
|
|
13
|
+
|
|
14
|
+
def __str__(self) -> str:
|
|
15
|
+
"""Format list result as plain text table."""
|
|
16
|
+
if not self.artifacts:
|
|
17
|
+
mode_text = f" for {self.agent_mode.value}" if self.agent_mode else ""
|
|
18
|
+
return f"No artifacts found{mode_text}."
|
|
19
|
+
|
|
20
|
+
lines = [
|
|
21
|
+
f"{'Agent':<10} {'ID':<25} {'Name':<30} {'Sections':<8} {'Updated'}",
|
|
22
|
+
"-" * 85,
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
for artifact in self.artifacts:
|
|
26
|
+
lines.append(
|
|
27
|
+
f"{artifact.agent_mode.value:<10} "
|
|
28
|
+
f"{artifact.artifact_id[:25]:<25} "
|
|
29
|
+
f"{artifact.name[:30]:<30} "
|
|
30
|
+
f"{artifact.section_count:<8} "
|
|
31
|
+
f"{artifact.updated_at.strftime('%Y-%m-%d')}"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return "\n".join(lines)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ArtifactCreateResult(BaseModel):
|
|
38
|
+
"""Result for artifact create command."""
|
|
39
|
+
|
|
40
|
+
artifact_id: str
|
|
41
|
+
agent_mode: AgentMode
|
|
42
|
+
name: str
|
|
43
|
+
created: bool = True
|
|
44
|
+
|
|
45
|
+
def __str__(self) -> str:
|
|
46
|
+
"""Format create result as success message."""
|
|
47
|
+
return f"Created artifact '{self.artifact_id}' in {self.agent_mode.value} mode"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ArtifactDeleteResult(BaseModel):
|
|
51
|
+
"""Result for artifact delete command."""
|
|
52
|
+
|
|
53
|
+
artifact_id: str
|
|
54
|
+
agent_mode: AgentMode
|
|
55
|
+
deleted: bool = True
|
|
56
|
+
cancelled: bool = False
|
|
57
|
+
|
|
58
|
+
def __str__(self) -> str:
|
|
59
|
+
"""Format delete result message."""
|
|
60
|
+
if self.cancelled:
|
|
61
|
+
return "Deletion cancelled."
|
|
62
|
+
elif self.deleted:
|
|
63
|
+
return f"Deleted artifact '{self.artifact_id}' from {self.agent_mode.value} mode"
|
|
64
|
+
else:
|
|
65
|
+
return f"Failed to delete artifact '{self.artifact_id}'"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ArtifactInfoResult(BaseModel):
|
|
69
|
+
"""Result for artifact info command."""
|
|
70
|
+
|
|
71
|
+
artifact: Artifact
|
|
72
|
+
|
|
73
|
+
def __str__(self) -> str:
|
|
74
|
+
"""Format detailed artifact information."""
|
|
75
|
+
artifact = self.artifact
|
|
76
|
+
lines = [
|
|
77
|
+
f"Artifact ID: {artifact.artifact_id}",
|
|
78
|
+
f"Name: {artifact.name}",
|
|
79
|
+
f"Agent Mode: {artifact.agent_mode.value}",
|
|
80
|
+
f"Created: {artifact.get_created_at()}",
|
|
81
|
+
f"Updated: {artifact.get_updated_at()}",
|
|
82
|
+
f"Sections: {artifact.get_section_count()}",
|
|
83
|
+
f"Total Content Length: {artifact.get_total_content_length()} characters",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
if artifact.sections:
|
|
87
|
+
lines.append("\nSections:")
|
|
88
|
+
for section in artifact.get_ordered_sections():
|
|
89
|
+
content_preview = (
|
|
90
|
+
section.content[:50] + "..."
|
|
91
|
+
if len(section.content) > 50
|
|
92
|
+
else section.content
|
|
93
|
+
).replace("\n", " ")
|
|
94
|
+
lines.append(f" {section.number:03d}. {section.title}")
|
|
95
|
+
if content_preview:
|
|
96
|
+
lines.append(f" {content_preview}")
|
|
97
|
+
|
|
98
|
+
return "\n".join(lines)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class SectionCreateResult(BaseModel):
|
|
102
|
+
"""Result for section create command."""
|
|
103
|
+
|
|
104
|
+
artifact_id: str
|
|
105
|
+
agent_mode: AgentMode
|
|
106
|
+
section_number: int
|
|
107
|
+
section_title: str
|
|
108
|
+
created: bool = True
|
|
109
|
+
|
|
110
|
+
def __str__(self) -> str:
|
|
111
|
+
"""Format section create result."""
|
|
112
|
+
return (
|
|
113
|
+
f"Created section {self.section_number} '{self.section_title}' "
|
|
114
|
+
f"in artifact '{self.artifact_id}'"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class SectionUpdateResult(BaseModel):
|
|
119
|
+
"""Result for section update command."""
|
|
120
|
+
|
|
121
|
+
artifact_id: str
|
|
122
|
+
agent_mode: AgentMode
|
|
123
|
+
section_number: int
|
|
124
|
+
updated_fields: list[str]
|
|
125
|
+
|
|
126
|
+
def __str__(self) -> str:
|
|
127
|
+
"""Format section update result."""
|
|
128
|
+
fields_text = ", ".join(self.updated_fields)
|
|
129
|
+
return (
|
|
130
|
+
f"Updated section {self.section_number} in artifact '{self.artifact_id}' "
|
|
131
|
+
f"(fields: {fields_text})"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class SectionDeleteResult(BaseModel):
|
|
136
|
+
"""Result for section delete command."""
|
|
137
|
+
|
|
138
|
+
artifact_id: str
|
|
139
|
+
agent_mode: AgentMode
|
|
140
|
+
section_number: int
|
|
141
|
+
deleted: bool = True
|
|
142
|
+
|
|
143
|
+
def __str__(self) -> str:
|
|
144
|
+
"""Format section delete result."""
|
|
145
|
+
if self.deleted:
|
|
146
|
+
return f"Deleted section {self.section_number} from artifact '{self.artifact_id}'"
|
|
147
|
+
else:
|
|
148
|
+
return f"Failed to delete section {self.section_number}"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class SectionContentResult(BaseModel):
|
|
152
|
+
"""Result for section content read command."""
|
|
153
|
+
|
|
154
|
+
artifact_id: str
|
|
155
|
+
agent_mode: AgentMode
|
|
156
|
+
section_number: int
|
|
157
|
+
content: str
|
|
158
|
+
|
|
159
|
+
def __str__(self) -> str:
|
|
160
|
+
"""Format section content."""
|
|
161
|
+
return self.content
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class ArtifactErrorResult(BaseModel):
|
|
165
|
+
"""Result for error cases in artifact operations."""
|
|
166
|
+
|
|
167
|
+
error_message: str
|
|
168
|
+
artifact_id: str | None = None
|
|
169
|
+
agent_mode: AgentMode | None = None
|
|
170
|
+
section_number: int | None = None
|
|
171
|
+
details: str | None = None
|
|
172
|
+
|
|
173
|
+
def __str__(self) -> str:
|
|
174
|
+
"""Format error message."""
|
|
175
|
+
parts = [f"Error: {self.error_message}"]
|
|
176
|
+
|
|
177
|
+
if self.artifact_id:
|
|
178
|
+
parts.append(f"Artifact: {self.artifact_id}")
|
|
179
|
+
if self.agent_mode:
|
|
180
|
+
parts.append(f"Mode: {self.agent_mode.value}")
|
|
181
|
+
if self.section_number:
|
|
182
|
+
parts.append(f"Section: {self.section_number}")
|
|
183
|
+
if self.details:
|
|
184
|
+
parts.append(f"Details: {self.details}")
|
|
185
|
+
|
|
186
|
+
return " | ".join(parts)
|
shotgun/sdk/artifacts.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""Artifact SDK for framework-agnostic business logic."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from shotgun.artifacts.exceptions import (
|
|
8
|
+
ArtifactAlreadyExistsError,
|
|
9
|
+
ArtifactError,
|
|
10
|
+
ArtifactNotFoundError,
|
|
11
|
+
SectionAlreadyExistsError,
|
|
12
|
+
SectionNotFoundError,
|
|
13
|
+
)
|
|
14
|
+
from shotgun.artifacts.models import AgentMode, ArtifactSection
|
|
15
|
+
from shotgun.artifacts.service import ArtifactService
|
|
16
|
+
from shotgun.artifacts.utils import generate_artifact_name
|
|
17
|
+
|
|
18
|
+
from .artifact_models import (
|
|
19
|
+
ArtifactCreateResult,
|
|
20
|
+
ArtifactDeleteResult,
|
|
21
|
+
ArtifactErrorResult,
|
|
22
|
+
ArtifactInfoResult,
|
|
23
|
+
ArtifactListResult,
|
|
24
|
+
SectionContentResult,
|
|
25
|
+
SectionCreateResult,
|
|
26
|
+
SectionDeleteResult,
|
|
27
|
+
SectionUpdateResult,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ArtifactSDK:
|
|
32
|
+
"""Framework-agnostic SDK for artifact operations.
|
|
33
|
+
|
|
34
|
+
This SDK provides business logic for artifact management that can be
|
|
35
|
+
used by both CLI and TUI implementations without framework dependencies.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, base_path: Path | None = None):
|
|
39
|
+
"""Initialize SDK with optional base path.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
base_path: Optional custom base path for artifacts.
|
|
43
|
+
Defaults to .shotgun in current directory.
|
|
44
|
+
"""
|
|
45
|
+
self.service = ArtifactService(base_path)
|
|
46
|
+
|
|
47
|
+
# Artifact operations
|
|
48
|
+
|
|
49
|
+
def list_artifacts(
|
|
50
|
+
self, agent_mode: AgentMode | None = None
|
|
51
|
+
) -> ArtifactListResult | ArtifactErrorResult:
|
|
52
|
+
"""List all artifacts, optionally filtered by agent mode.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
agent_mode: Optional agent mode filter
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
ArtifactListResult containing list of artifacts or ArtifactErrorResult
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
artifacts = self.service.list_artifacts(agent_mode)
|
|
62
|
+
return ArtifactListResult(artifacts=artifacts, agent_mode=agent_mode)
|
|
63
|
+
except ArtifactError as e:
|
|
64
|
+
return ArtifactErrorResult(error_message=str(e), agent_mode=agent_mode)
|
|
65
|
+
|
|
66
|
+
def create_artifact(
|
|
67
|
+
self,
|
|
68
|
+
artifact_id: str,
|
|
69
|
+
agent_mode: AgentMode,
|
|
70
|
+
name: str,
|
|
71
|
+
template_id: str | None = None,
|
|
72
|
+
) -> ArtifactCreateResult | ArtifactErrorResult:
|
|
73
|
+
"""Create a new artifact.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
artifact_id: Unique identifier for the artifact
|
|
77
|
+
agent_mode: Agent mode this artifact belongs to
|
|
78
|
+
name: Human-readable name for the artifact
|
|
79
|
+
template_id: Optional template ID to use for creating the artifact
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
ArtifactCreateResult or ArtifactErrorResult
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
self.service.create_artifact(artifact_id, agent_mode, name, template_id)
|
|
86
|
+
return ArtifactCreateResult(
|
|
87
|
+
artifact_id=artifact_id,
|
|
88
|
+
agent_mode=agent_mode,
|
|
89
|
+
name=name,
|
|
90
|
+
)
|
|
91
|
+
except ArtifactAlreadyExistsError as e:
|
|
92
|
+
return ArtifactErrorResult(
|
|
93
|
+
error_message="Artifact already exists",
|
|
94
|
+
artifact_id=artifact_id,
|
|
95
|
+
agent_mode=agent_mode,
|
|
96
|
+
details=str(e),
|
|
97
|
+
)
|
|
98
|
+
except ArtifactError as e:
|
|
99
|
+
return ArtifactErrorResult(
|
|
100
|
+
error_message=str(e),
|
|
101
|
+
artifact_id=artifact_id,
|
|
102
|
+
agent_mode=agent_mode,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def delete_artifact(
|
|
106
|
+
self,
|
|
107
|
+
artifact_id: str,
|
|
108
|
+
agent_mode: AgentMode,
|
|
109
|
+
confirm_callback: Callable[[str, AgentMode], bool] | None = None,
|
|
110
|
+
) -> ArtifactDeleteResult | ArtifactErrorResult:
|
|
111
|
+
"""Delete an artifact with optional confirmation.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
artifact_id: ID of the artifact to delete
|
|
115
|
+
agent_mode: Agent mode
|
|
116
|
+
confirm_callback: Optional callback for confirmation that receives
|
|
117
|
+
artifact_id and agent_mode and returns boolean.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
ArtifactDeleteResult or ArtifactErrorResult
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
# Handle confirmation callback if provided
|
|
124
|
+
if confirm_callback and not confirm_callback(artifact_id, agent_mode):
|
|
125
|
+
return ArtifactDeleteResult(
|
|
126
|
+
artifact_id=artifact_id,
|
|
127
|
+
agent_mode=agent_mode,
|
|
128
|
+
deleted=False,
|
|
129
|
+
cancelled=True,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
self.service.delete_artifact(artifact_id, agent_mode)
|
|
133
|
+
return ArtifactDeleteResult(
|
|
134
|
+
artifact_id=artifact_id,
|
|
135
|
+
agent_mode=agent_mode,
|
|
136
|
+
deleted=True,
|
|
137
|
+
)
|
|
138
|
+
except ArtifactNotFoundError as e:
|
|
139
|
+
return ArtifactErrorResult(
|
|
140
|
+
error_message="Artifact not found",
|
|
141
|
+
artifact_id=artifact_id,
|
|
142
|
+
agent_mode=agent_mode,
|
|
143
|
+
details=str(e),
|
|
144
|
+
)
|
|
145
|
+
except ArtifactError as e:
|
|
146
|
+
return ArtifactErrorResult(
|
|
147
|
+
error_message=str(e),
|
|
148
|
+
artifact_id=artifact_id,
|
|
149
|
+
agent_mode=agent_mode,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def get_artifact_info(
|
|
153
|
+
self, artifact_id: str, agent_mode: AgentMode
|
|
154
|
+
) -> ArtifactInfoResult | ArtifactErrorResult:
|
|
155
|
+
"""Get detailed information about an artifact.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
artifact_id: ID of the artifact to get info for
|
|
159
|
+
agent_mode: Agent mode
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
ArtifactInfoResult or ArtifactErrorResult
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
artifact = self.service.get_artifact(artifact_id, agent_mode, "")
|
|
166
|
+
return ArtifactInfoResult(artifact=artifact)
|
|
167
|
+
except ArtifactNotFoundError as e:
|
|
168
|
+
return ArtifactErrorResult(
|
|
169
|
+
error_message="Artifact not found",
|
|
170
|
+
artifact_id=artifact_id,
|
|
171
|
+
agent_mode=agent_mode,
|
|
172
|
+
details=str(e),
|
|
173
|
+
)
|
|
174
|
+
except ArtifactError as e:
|
|
175
|
+
return ArtifactErrorResult(
|
|
176
|
+
error_message=str(e),
|
|
177
|
+
artifact_id=artifact_id,
|
|
178
|
+
agent_mode=agent_mode,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Section operations
|
|
182
|
+
|
|
183
|
+
def create_section(
|
|
184
|
+
self,
|
|
185
|
+
artifact_id: str,
|
|
186
|
+
agent_mode: AgentMode,
|
|
187
|
+
section_number: int,
|
|
188
|
+
section_slug: str,
|
|
189
|
+
section_title: str,
|
|
190
|
+
content: str = "",
|
|
191
|
+
) -> SectionCreateResult | ArtifactErrorResult:
|
|
192
|
+
"""Create a new section in an artifact.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
artifact_id: Artifact identifier
|
|
196
|
+
agent_mode: Agent mode
|
|
197
|
+
section_number: Section number
|
|
198
|
+
section_slug: Section slug
|
|
199
|
+
section_title: Section title
|
|
200
|
+
content: Section content
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
SectionCreateResult or ArtifactErrorResult
|
|
204
|
+
"""
|
|
205
|
+
try:
|
|
206
|
+
section = ArtifactSection(
|
|
207
|
+
number=section_number,
|
|
208
|
+
slug=section_slug,
|
|
209
|
+
title=section_title,
|
|
210
|
+
content=content,
|
|
211
|
+
)
|
|
212
|
+
self.service.add_section(artifact_id, agent_mode, section)
|
|
213
|
+
return SectionCreateResult(
|
|
214
|
+
artifact_id=artifact_id,
|
|
215
|
+
agent_mode=agent_mode,
|
|
216
|
+
section_number=section_number,
|
|
217
|
+
section_title=section_title,
|
|
218
|
+
)
|
|
219
|
+
except (SectionAlreadyExistsError, ArtifactNotFoundError) as e:
|
|
220
|
+
return ArtifactErrorResult(
|
|
221
|
+
error_message=str(e),
|
|
222
|
+
artifact_id=artifact_id,
|
|
223
|
+
agent_mode=agent_mode,
|
|
224
|
+
section_number=section_number,
|
|
225
|
+
)
|
|
226
|
+
except ArtifactError as e:
|
|
227
|
+
return ArtifactErrorResult(
|
|
228
|
+
error_message=str(e),
|
|
229
|
+
artifact_id=artifact_id,
|
|
230
|
+
agent_mode=agent_mode,
|
|
231
|
+
section_number=section_number,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def update_section(
|
|
235
|
+
self,
|
|
236
|
+
artifact_id: str,
|
|
237
|
+
agent_mode: AgentMode,
|
|
238
|
+
section_number: int,
|
|
239
|
+
**kwargs: Any,
|
|
240
|
+
) -> SectionUpdateResult | ArtifactErrorResult:
|
|
241
|
+
"""Update a section in an artifact.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
artifact_id: Artifact identifier
|
|
245
|
+
agent_mode: Agent mode
|
|
246
|
+
section_number: Section number to update
|
|
247
|
+
**kwargs: Fields to update
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
SectionUpdateResult or ArtifactErrorResult
|
|
251
|
+
"""
|
|
252
|
+
try:
|
|
253
|
+
self.service.update_section(
|
|
254
|
+
artifact_id, agent_mode, section_number, **kwargs
|
|
255
|
+
)
|
|
256
|
+
return SectionUpdateResult(
|
|
257
|
+
artifact_id=artifact_id,
|
|
258
|
+
agent_mode=agent_mode,
|
|
259
|
+
section_number=section_number,
|
|
260
|
+
updated_fields=list(kwargs.keys()),
|
|
261
|
+
)
|
|
262
|
+
except (SectionNotFoundError, ArtifactNotFoundError) as e:
|
|
263
|
+
return ArtifactErrorResult(
|
|
264
|
+
error_message=str(e),
|
|
265
|
+
artifact_id=artifact_id,
|
|
266
|
+
agent_mode=agent_mode,
|
|
267
|
+
section_number=section_number,
|
|
268
|
+
)
|
|
269
|
+
except ArtifactError as e:
|
|
270
|
+
return ArtifactErrorResult(
|
|
271
|
+
error_message=str(e),
|
|
272
|
+
artifact_id=artifact_id,
|
|
273
|
+
agent_mode=agent_mode,
|
|
274
|
+
section_number=section_number,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def delete_section(
|
|
278
|
+
self,
|
|
279
|
+
artifact_id: str,
|
|
280
|
+
agent_mode: AgentMode,
|
|
281
|
+
section_number: int,
|
|
282
|
+
) -> SectionDeleteResult | ArtifactErrorResult:
|
|
283
|
+
"""Delete a section from an artifact.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
artifact_id: Artifact identifier
|
|
287
|
+
agent_mode: Agent mode
|
|
288
|
+
section_number: Section number to delete
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
SectionDeleteResult or ArtifactErrorResult
|
|
292
|
+
"""
|
|
293
|
+
try:
|
|
294
|
+
self.service.delete_section(artifact_id, agent_mode, section_number)
|
|
295
|
+
return SectionDeleteResult(
|
|
296
|
+
artifact_id=artifact_id,
|
|
297
|
+
agent_mode=agent_mode,
|
|
298
|
+
section_number=section_number,
|
|
299
|
+
)
|
|
300
|
+
except (SectionNotFoundError, ArtifactNotFoundError) as e:
|
|
301
|
+
return ArtifactErrorResult(
|
|
302
|
+
error_message=str(e),
|
|
303
|
+
artifact_id=artifact_id,
|
|
304
|
+
agent_mode=agent_mode,
|
|
305
|
+
section_number=section_number,
|
|
306
|
+
)
|
|
307
|
+
except ArtifactError as e:
|
|
308
|
+
return ArtifactErrorResult(
|
|
309
|
+
error_message=str(e),
|
|
310
|
+
artifact_id=artifact_id,
|
|
311
|
+
agent_mode=agent_mode,
|
|
312
|
+
section_number=section_number,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def get_section_content(
|
|
316
|
+
self,
|
|
317
|
+
artifact_id: str,
|
|
318
|
+
agent_mode: AgentMode,
|
|
319
|
+
section_number: int,
|
|
320
|
+
) -> SectionContentResult | ArtifactErrorResult:
|
|
321
|
+
"""Get the content of a section.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
artifact_id: Artifact identifier
|
|
325
|
+
agent_mode: Agent mode
|
|
326
|
+
section_number: Section number
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
SectionContentResult or ArtifactErrorResult
|
|
330
|
+
"""
|
|
331
|
+
try:
|
|
332
|
+
content = self.service.get_section_content(
|
|
333
|
+
artifact_id, agent_mode, section_number
|
|
334
|
+
)
|
|
335
|
+
return SectionContentResult(
|
|
336
|
+
artifact_id=artifact_id,
|
|
337
|
+
agent_mode=agent_mode,
|
|
338
|
+
section_number=section_number,
|
|
339
|
+
content=content,
|
|
340
|
+
)
|
|
341
|
+
except (SectionNotFoundError, ArtifactNotFoundError) as e:
|
|
342
|
+
return ArtifactErrorResult(
|
|
343
|
+
error_message=str(e),
|
|
344
|
+
artifact_id=artifact_id,
|
|
345
|
+
agent_mode=agent_mode,
|
|
346
|
+
section_number=section_number,
|
|
347
|
+
)
|
|
348
|
+
except ArtifactError as e:
|
|
349
|
+
return ArtifactErrorResult(
|
|
350
|
+
error_message=str(e),
|
|
351
|
+
artifact_id=artifact_id,
|
|
352
|
+
agent_mode=agent_mode,
|
|
353
|
+
section_number=section_number,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Template operations
|
|
357
|
+
|
|
358
|
+
def list_templates(
|
|
359
|
+
self, agent_mode: AgentMode | None = None
|
|
360
|
+
) -> list[Any] | ArtifactErrorResult:
|
|
361
|
+
"""List available artifact templates.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
agent_mode: Optional agent mode filter
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
List of template summaries or ArtifactErrorResult
|
|
368
|
+
"""
|
|
369
|
+
try:
|
|
370
|
+
return self.service.list_templates(agent_mode)
|
|
371
|
+
except Exception as e:
|
|
372
|
+
return ArtifactErrorResult(
|
|
373
|
+
error_message=f"Failed to list templates: {str(e)}",
|
|
374
|
+
agent_mode=agent_mode,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# Convenience methods
|
|
378
|
+
|
|
379
|
+
def ensure_artifact_exists(
|
|
380
|
+
self,
|
|
381
|
+
artifact_id: str,
|
|
382
|
+
agent_mode: AgentMode,
|
|
383
|
+
name: str | None = None,
|
|
384
|
+
) -> ArtifactCreateResult | ArtifactInfoResult | ArtifactErrorResult:
|
|
385
|
+
"""Ensure an artifact exists, creating it if necessary.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
artifact_id: Artifact identifier
|
|
389
|
+
agent_mode: Agent mode
|
|
390
|
+
name: Optional name (defaults to formatted artifact_id)
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
ArtifactCreateResult if created, ArtifactInfoResult if already existed, ArtifactErrorResult on error
|
|
394
|
+
"""
|
|
395
|
+
if name is None:
|
|
396
|
+
name = generate_artifact_name(artifact_id)
|
|
397
|
+
|
|
398
|
+
# Try to get existing artifact
|
|
399
|
+
info_result = self.get_artifact_info(artifact_id, agent_mode)
|
|
400
|
+
if isinstance(info_result, ArtifactInfoResult):
|
|
401
|
+
return info_result
|
|
402
|
+
|
|
403
|
+
# Create new artifact
|
|
404
|
+
create_result = self.create_artifact(artifact_id, agent_mode, name)
|
|
405
|
+
return create_result
|
|
406
|
+
|
|
407
|
+
def ensure_section_exists(
|
|
408
|
+
self,
|
|
409
|
+
artifact_id: str,
|
|
410
|
+
agent_mode: AgentMode,
|
|
411
|
+
section_number: int,
|
|
412
|
+
section_slug: str,
|
|
413
|
+
section_title: str,
|
|
414
|
+
initial_content: str = "",
|
|
415
|
+
) -> SectionCreateResult | SectionContentResult | ArtifactErrorResult:
|
|
416
|
+
"""Ensure a section exists, creating it if necessary.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
artifact_id: Artifact identifier
|
|
420
|
+
agent_mode: Agent mode
|
|
421
|
+
section_number: Section number
|
|
422
|
+
section_slug: Section slug
|
|
423
|
+
section_title: Section title
|
|
424
|
+
initial_content: Initial content for new sections
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
SectionCreateResult if created, SectionContentResult if already existed, ArtifactErrorResult on error
|
|
428
|
+
"""
|
|
429
|
+
# Try to get existing section
|
|
430
|
+
content_result = self.get_section_content(
|
|
431
|
+
artifact_id, agent_mode, section_number
|
|
432
|
+
)
|
|
433
|
+
if isinstance(content_result, SectionContentResult):
|
|
434
|
+
return content_result
|
|
435
|
+
|
|
436
|
+
# Ensure artifact exists first
|
|
437
|
+
self.ensure_artifact_exists(artifact_id, agent_mode)
|
|
438
|
+
|
|
439
|
+
# Create new section
|
|
440
|
+
create_result = self.create_section(
|
|
441
|
+
artifact_id,
|
|
442
|
+
agent_mode,
|
|
443
|
+
section_number,
|
|
444
|
+
section_slug,
|
|
445
|
+
section_title,
|
|
446
|
+
initial_content,
|
|
447
|
+
)
|
|
448
|
+
return create_result
|
shotgun/sdk/services.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
+
from shotgun.artifacts.service import ArtifactService
|
|
5
6
|
from shotgun.codebase.service import CodebaseService
|
|
6
7
|
from shotgun.utils import get_shotgun_home
|
|
7
8
|
|
|
@@ -21,3 +22,16 @@ def get_codebase_service(storage_dir: Path | str | None = None) -> CodebaseServi
|
|
|
21
22
|
elif isinstance(storage_dir, str):
|
|
22
23
|
storage_dir = Path(storage_dir)
|
|
23
24
|
return CodebaseService(storage_dir)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_artifact_service(base_path: Path | None = None) -> ArtifactService:
|
|
28
|
+
"""Get ArtifactService instance with configurable base path.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
base_path: Optional base path for artifacts.
|
|
32
|
+
Defaults to .shotgun in current directory.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Configured ArtifactService instance
|
|
36
|
+
"""
|
|
37
|
+
return ArtifactService(base_path)
|