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