shotgun-sh 0.1.0.dev13__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 +48 -14
- shotgun/agents/config/models.py +61 -0
- 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 +125 -1
- shotgun/agents/tools/artifact_management.py +56 -24
- shotgun/agents/tools/file_management.py +30 -11
- 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/manager.py +2 -1
- shotgun/artifacts/models.py +6 -4
- shotgun/codebase/core/nl_query.py +4 -4
- shotgun/prompts/agents/partials/artifact_system.j2 +4 -1
- shotgun/prompts/agents/partials/codebase_understanding.j2 +1 -2
- shotgun/prompts/agents/plan.j2 +9 -7
- shotgun/prompts/agents/research.j2 +7 -5
- shotgun/prompts/agents/specify.j2 +8 -7
- 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/history/incremental_summarization.j2 +53 -0
- shotgun/sdk/services.py +14 -0
- shotgun/tui/app.py +1 -1
- shotgun/tui/screens/chat.py +4 -2
- shotgun/utils/file_system_utils.py +6 -1
- {shotgun_sh-0.1.0.dev13.dist-info → shotgun_sh-0.1.0.dev14.dist-info}/METADATA +2 -1
- {shotgun_sh-0.1.0.dev13.dist-info → shotgun_sh-0.1.0.dev14.dist-info}/RECORD +40 -29
- {shotgun_sh-0.1.0.dev13.dist-info → shotgun_sh-0.1.0.dev14.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.1.0.dev13.dist-info → shotgun_sh-0.1.0.dev14.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.1.0.dev13.dist-info → shotgun_sh-0.1.0.dev14.dist-info}/licenses/LICENSE +0 -0
shotgun/agents/models.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from asyncio import Future, Queue
|
|
4
4
|
from collections.abc import Callable
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import Enum
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
from typing import TYPE_CHECKING
|
|
7
9
|
|
|
@@ -11,6 +13,7 @@ from pydantic_ai import RunContext
|
|
|
11
13
|
from .config.models import ModelConfig
|
|
12
14
|
|
|
13
15
|
if TYPE_CHECKING:
|
|
16
|
+
from shotgun.artifacts.service import ArtifactService
|
|
14
17
|
from shotgun.codebase.service import CodebaseService
|
|
15
18
|
|
|
16
19
|
|
|
@@ -74,6 +77,117 @@ class AgentRuntimeOptions(BaseModel):
|
|
|
74
77
|
)
|
|
75
78
|
|
|
76
79
|
|
|
80
|
+
class FileOperationType(str, Enum):
|
|
81
|
+
"""Types of file operations that can be tracked."""
|
|
82
|
+
|
|
83
|
+
CREATED = "created"
|
|
84
|
+
UPDATED = "updated"
|
|
85
|
+
DELETED = "deleted"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class FileOperation(BaseModel):
|
|
89
|
+
"""Single file operation record."""
|
|
90
|
+
|
|
91
|
+
file_path: str = Field(
|
|
92
|
+
description="Full absolute path to the file",
|
|
93
|
+
)
|
|
94
|
+
operation: FileOperationType = Field(
|
|
95
|
+
description="Type of operation performed",
|
|
96
|
+
)
|
|
97
|
+
timestamp: datetime = Field(
|
|
98
|
+
default_factory=datetime.now,
|
|
99
|
+
description="When the operation occurred",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class FileOperationTracker(BaseModel):
|
|
104
|
+
"""Tracks file operations during a single agent run."""
|
|
105
|
+
|
|
106
|
+
operations: list[FileOperation] = Field(
|
|
107
|
+
default_factory=list,
|
|
108
|
+
description="List of file operations performed",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def add_operation(
|
|
112
|
+
self, file_path: Path | str, operation: FileOperationType
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Record a file operation.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
file_path: Path to the file (will be converted to absolute)
|
|
118
|
+
operation: Type of operation performed
|
|
119
|
+
"""
|
|
120
|
+
# Convert to absolute path string
|
|
121
|
+
if isinstance(file_path, Path):
|
|
122
|
+
absolute_path = str(file_path.resolve())
|
|
123
|
+
else:
|
|
124
|
+
absolute_path = str(Path(file_path).resolve())
|
|
125
|
+
|
|
126
|
+
self.operations.append(
|
|
127
|
+
FileOperation(file_path=absolute_path, operation=operation)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def clear(self) -> None:
|
|
131
|
+
"""Clear all tracked operations for a new run."""
|
|
132
|
+
self.operations = []
|
|
133
|
+
|
|
134
|
+
def get_summary(self) -> dict[FileOperationType, list[str]]:
|
|
135
|
+
"""Get operations grouped by type.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Dictionary mapping operation types to lists of file paths
|
|
139
|
+
"""
|
|
140
|
+
summary: dict[FileOperationType, list[str]] = {
|
|
141
|
+
FileOperationType.CREATED: [],
|
|
142
|
+
FileOperationType.UPDATED: [],
|
|
143
|
+
FileOperationType.DELETED: [],
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for op in self.operations:
|
|
147
|
+
summary[op.operation].append(op.file_path)
|
|
148
|
+
|
|
149
|
+
# Remove duplicates while preserving order
|
|
150
|
+
for op_type in summary:
|
|
151
|
+
seen = set()
|
|
152
|
+
unique_paths = []
|
|
153
|
+
for path in summary[op_type]:
|
|
154
|
+
if path not in seen:
|
|
155
|
+
seen.add(path)
|
|
156
|
+
unique_paths.append(path)
|
|
157
|
+
summary[op_type] = unique_paths
|
|
158
|
+
|
|
159
|
+
return summary
|
|
160
|
+
|
|
161
|
+
def format_summary(self) -> str:
|
|
162
|
+
"""Generate human-readable summary for the user.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Formatted string showing files modified during the run
|
|
166
|
+
"""
|
|
167
|
+
if not self.operations:
|
|
168
|
+
return "No files were modified during this run."
|
|
169
|
+
|
|
170
|
+
summary = self.get_summary()
|
|
171
|
+
lines = ["Files modified during this run:"]
|
|
172
|
+
|
|
173
|
+
if summary[FileOperationType.CREATED]:
|
|
174
|
+
lines.append("\nCreated:")
|
|
175
|
+
for path in summary[FileOperationType.CREATED]:
|
|
176
|
+
lines.append(f" - {path}")
|
|
177
|
+
|
|
178
|
+
if summary[FileOperationType.UPDATED]:
|
|
179
|
+
lines.append("\nUpdated:")
|
|
180
|
+
for path in summary[FileOperationType.UPDATED]:
|
|
181
|
+
lines.append(f" - {path}")
|
|
182
|
+
|
|
183
|
+
if summary[FileOperationType.DELETED]:
|
|
184
|
+
lines.append("\nDeleted:")
|
|
185
|
+
for path in summary[FileOperationType.DELETED]:
|
|
186
|
+
lines.append(f" - {path}")
|
|
187
|
+
|
|
188
|
+
return "\n".join(lines)
|
|
189
|
+
|
|
190
|
+
|
|
77
191
|
class AgentDeps(AgentRuntimeOptions):
|
|
78
192
|
"""Dependencies passed to all agents for configuration and runtime behavior."""
|
|
79
193
|
|
|
@@ -85,16 +199,26 @@ class AgentDeps(AgentRuntimeOptions):
|
|
|
85
199
|
description="Codebase service for code analysis tools",
|
|
86
200
|
)
|
|
87
201
|
|
|
202
|
+
artifact_service: "ArtifactService" = Field(
|
|
203
|
+
description="Artifact service for managing structured artifacts",
|
|
204
|
+
)
|
|
205
|
+
|
|
88
206
|
system_prompt_fn: Callable[[RunContext["AgentDeps"]], str] = Field(
|
|
89
207
|
description="Function that generates the system prompt for this agent",
|
|
90
208
|
)
|
|
91
209
|
|
|
210
|
+
file_tracker: FileOperationTracker = Field(
|
|
211
|
+
default_factory=FileOperationTracker,
|
|
212
|
+
description="Tracker for file operations during agent run",
|
|
213
|
+
)
|
|
214
|
+
|
|
92
215
|
|
|
93
216
|
# Rebuild model to resolve forward references after imports are available
|
|
94
217
|
try:
|
|
218
|
+
from shotgun.artifacts.service import ArtifactService
|
|
95
219
|
from shotgun.codebase.service import CodebaseService
|
|
96
220
|
|
|
97
221
|
AgentDeps.model_rebuild()
|
|
98
222
|
except ImportError:
|
|
99
|
-
#
|
|
223
|
+
# Services may not be available in all contexts
|
|
100
224
|
pass
|
|
@@ -4,25 +4,17 @@ These tools provide agents with the ability to create and manage structured
|
|
|
4
4
|
artifacts instead of flat markdown files.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from pydantic_ai import RunContext
|
|
8
|
+
|
|
9
|
+
from shotgun.agents.models import AgentDeps
|
|
8
10
|
from shotgun.artifacts.utils import handle_agent_mode_parsing
|
|
9
11
|
from shotgun.logging_config import setup_logger
|
|
10
12
|
|
|
11
13
|
logger = setup_logger(__name__)
|
|
12
14
|
|
|
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
15
|
|
|
25
|
-
def create_artifact(
|
|
16
|
+
async def create_artifact(
|
|
17
|
+
ctx: RunContext[AgentDeps],
|
|
26
18
|
artifact_id: str,
|
|
27
19
|
agent_mode: str,
|
|
28
20
|
name: str,
|
|
@@ -31,6 +23,7 @@ def create_artifact(
|
|
|
31
23
|
"""Create a new artifact.
|
|
32
24
|
|
|
33
25
|
Args:
|
|
26
|
+
ctx: RunContext containing AgentDeps with artifact service
|
|
34
27
|
artifact_id: Unique identifier for the artifact (slug format)
|
|
35
28
|
agent_mode: Agent mode (research, plan, tasks)
|
|
36
29
|
name: Human-readable name for the artifact
|
|
@@ -55,12 +48,21 @@ def create_artifact(
|
|
|
55
48
|
return "Error: Invalid agent mode"
|
|
56
49
|
|
|
57
50
|
try:
|
|
58
|
-
service =
|
|
51
|
+
service = ctx.deps.artifact_service
|
|
59
52
|
|
|
60
53
|
# Pass template_id if provided and not empty
|
|
61
54
|
template_to_use = template_id.strip() if template_id.strip() else None
|
|
62
55
|
artifact = service.create_artifact(artifact_id, mode, name, template_to_use)
|
|
63
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
|
+
|
|
64
66
|
success_msg = (
|
|
65
67
|
f"Created artifact '{artifact_id}' in {agent_mode} mode with name '{name}'"
|
|
66
68
|
)
|
|
@@ -110,10 +112,15 @@ def create_artifact(
|
|
|
110
112
|
return f"Error: {error_msg}"
|
|
111
113
|
|
|
112
114
|
|
|
113
|
-
def read_artifact(
|
|
115
|
+
async def read_artifact(
|
|
116
|
+
ctx: RunContext[AgentDeps],
|
|
117
|
+
artifact_id: str,
|
|
118
|
+
agent_mode: str,
|
|
119
|
+
) -> str:
|
|
114
120
|
"""Read all sections of an artifact.
|
|
115
121
|
|
|
116
122
|
Args:
|
|
123
|
+
ctx: RunContext containing AgentDeps with artifact service
|
|
117
124
|
artifact_id: Artifact identifier
|
|
118
125
|
agent_mode: Agent mode (research, plan, tasks)
|
|
119
126
|
|
|
@@ -135,7 +142,7 @@ def read_artifact(artifact_id: str, agent_mode: str) -> str:
|
|
|
135
142
|
return "Error: Invalid agent mode"
|
|
136
143
|
|
|
137
144
|
try:
|
|
138
|
-
service =
|
|
145
|
+
service = ctx.deps.artifact_service
|
|
139
146
|
artifact = service.get_artifact(artifact_id, mode, "")
|
|
140
147
|
|
|
141
148
|
if not artifact.sections:
|
|
@@ -207,7 +214,8 @@ def read_artifact(artifact_id: str, agent_mode: str) -> str:
|
|
|
207
214
|
return f"Error: {error_msg}"
|
|
208
215
|
|
|
209
216
|
|
|
210
|
-
def write_artifact_section(
|
|
217
|
+
async def write_artifact_section(
|
|
218
|
+
ctx: RunContext[AgentDeps],
|
|
211
219
|
artifact_id: str,
|
|
212
220
|
agent_mode: str,
|
|
213
221
|
section_number: int,
|
|
@@ -220,6 +228,7 @@ def write_artifact_section(
|
|
|
220
228
|
Creates the artifact and/or section if they don't exist.
|
|
221
229
|
|
|
222
230
|
Args:
|
|
231
|
+
ctx: RunContext containing AgentDeps with artifact service
|
|
223
232
|
artifact_id: Artifact identifier
|
|
224
233
|
agent_mode: Agent mode (research, plan, tasks)
|
|
225
234
|
section_number: Section number (1, 2, 3, etc.)
|
|
@@ -251,13 +260,26 @@ def write_artifact_section(
|
|
|
251
260
|
return "Error: Agent mode validation failed"
|
|
252
261
|
|
|
253
262
|
try:
|
|
254
|
-
service =
|
|
263
|
+
service = ctx.deps.artifact_service
|
|
255
264
|
|
|
256
265
|
# Get or create the section
|
|
257
266
|
section, created = service.get_or_create_section(
|
|
258
267
|
artifact_id, mode, section_number, section_slug, section_title, content
|
|
259
268
|
)
|
|
260
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
|
+
|
|
261
283
|
if created:
|
|
262
284
|
success_msg = (
|
|
263
285
|
f"Created section {section_number} '{section_title}' "
|
|
@@ -283,7 +305,8 @@ def write_artifact_section(
|
|
|
283
305
|
return f"Error: {error_msg}"
|
|
284
306
|
|
|
285
307
|
|
|
286
|
-
def read_artifact_section(
|
|
308
|
+
async def read_artifact_section(
|
|
309
|
+
ctx: RunContext[AgentDeps],
|
|
287
310
|
artifact_id: str,
|
|
288
311
|
agent_mode: str,
|
|
289
312
|
section_number: int,
|
|
@@ -291,6 +314,7 @@ def read_artifact_section(
|
|
|
291
314
|
"""Read content from a specific section of an artifact.
|
|
292
315
|
|
|
293
316
|
Args:
|
|
317
|
+
ctx: RunContext containing AgentDeps with artifact service
|
|
294
318
|
artifact_id: Artifact identifier
|
|
295
319
|
agent_mode: Agent mode (research, plan, tasks)
|
|
296
320
|
section_number: Section number
|
|
@@ -319,7 +343,7 @@ def read_artifact_section(
|
|
|
319
343
|
return "Error: Agent mode validation failed"
|
|
320
344
|
|
|
321
345
|
try:
|
|
322
|
-
service =
|
|
346
|
+
service = ctx.deps.artifact_service
|
|
323
347
|
|
|
324
348
|
section = service.get_section(artifact_id, mode, section_number)
|
|
325
349
|
|
|
@@ -341,10 +365,14 @@ def read_artifact_section(
|
|
|
341
365
|
return f"Error: {error_msg}"
|
|
342
366
|
|
|
343
367
|
|
|
344
|
-
def list_artifacts(
|
|
368
|
+
async def list_artifacts(
|
|
369
|
+
ctx: RunContext[AgentDeps],
|
|
370
|
+
agent_mode: str | None = None,
|
|
371
|
+
) -> str:
|
|
345
372
|
"""List all artifacts, optionally filtered by agent mode.
|
|
346
373
|
|
|
347
374
|
Args:
|
|
375
|
+
ctx: RunContext containing AgentDeps with artifact service
|
|
348
376
|
agent_mode: Optional agent mode filter (research, plan, tasks)
|
|
349
377
|
|
|
350
378
|
Returns:
|
|
@@ -357,7 +385,7 @@ def list_artifacts(agent_mode: str | None = None) -> str:
|
|
|
357
385
|
logger.debug("🔧 Listing artifacts for mode: %s", agent_mode or "all")
|
|
358
386
|
|
|
359
387
|
try:
|
|
360
|
-
service =
|
|
388
|
+
service = ctx.deps.artifact_service
|
|
361
389
|
|
|
362
390
|
mode = None
|
|
363
391
|
if agent_mode:
|
|
@@ -399,10 +427,14 @@ def list_artifacts(agent_mode: str | None = None) -> str:
|
|
|
399
427
|
return f"Error: {error_msg}"
|
|
400
428
|
|
|
401
429
|
|
|
402
|
-
def list_artifact_templates(
|
|
430
|
+
async def list_artifact_templates(
|
|
431
|
+
ctx: RunContext[AgentDeps],
|
|
432
|
+
agent_mode: str | None = None,
|
|
433
|
+
) -> str:
|
|
403
434
|
"""List available artifact templates, optionally filtered by agent mode.
|
|
404
435
|
|
|
405
436
|
Args:
|
|
437
|
+
ctx: RunContext containing AgentDeps with artifact service
|
|
406
438
|
agent_mode: Optional agent mode filter (research, plan, tasks)
|
|
407
439
|
|
|
408
440
|
Returns:
|
|
@@ -415,7 +447,7 @@ def list_artifact_templates(agent_mode: str | None = None) -> str:
|
|
|
415
447
|
logger.debug("🔧 Listing templates for mode: %s", agent_mode or "all")
|
|
416
448
|
|
|
417
449
|
try:
|
|
418
|
-
service =
|
|
450
|
+
service = ctx.deps.artifact_service
|
|
419
451
|
|
|
420
452
|
mode = None
|
|
421
453
|
if agent_mode:
|
|
@@ -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
|
|
|
@@ -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")
|
|
@@ -11,10 +11,10 @@ logger = get_logger(__name__)
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def anthropic_web_search_tool(query: str) -> str:
|
|
14
|
-
"""Perform a web search using Anthropic's Claude API.
|
|
14
|
+
"""Perform a web search using Anthropic's Claude API with streaming.
|
|
15
15
|
|
|
16
16
|
This tool uses Anthropic's web search capabilities to find current information
|
|
17
|
-
about the given query.
|
|
17
|
+
about the given query. Results are streamed for faster response times.
|
|
18
18
|
|
|
19
19
|
Args:
|
|
20
20
|
query: The search query
|
|
@@ -27,7 +27,7 @@ def anthropic_web_search_tool(query: str) -> str:
|
|
|
27
27
|
span = trace.get_current_span()
|
|
28
28
|
span.set_attribute("input.value", f"**Query:** {query}\n")
|
|
29
29
|
|
|
30
|
-
logger.debug("📡 Executing Anthropic web search with prompt: %s", query)
|
|
30
|
+
logger.debug("📡 Executing Anthropic web search with streaming prompt: %s", query)
|
|
31
31
|
|
|
32
32
|
# Get API key from centralized configuration
|
|
33
33
|
try:
|
|
@@ -41,11 +41,13 @@ def anthropic_web_search_tool(query: str) -> str:
|
|
|
41
41
|
|
|
42
42
|
client = anthropic.Anthropic(api_key=api_key)
|
|
43
43
|
|
|
44
|
-
# Use the Messages API with web search tool
|
|
44
|
+
# Use the Messages API with web search tool and streaming
|
|
45
45
|
try:
|
|
46
|
-
|
|
46
|
+
result_text = ""
|
|
47
|
+
|
|
48
|
+
with client.messages.stream(
|
|
47
49
|
model="claude-3-5-sonnet-latest",
|
|
48
|
-
max_tokens=8192, #
|
|
50
|
+
max_tokens=8192, # Maximum for Claude 3.5 Sonnet
|
|
49
51
|
messages=[{"role": "user", "content": f"Search for: {query}"}],
|
|
50
52
|
tools=[
|
|
51
53
|
{
|
|
@@ -54,17 +56,17 @@ def anthropic_web_search_tool(query: str) -> str:
|
|
|
54
56
|
}
|
|
55
57
|
],
|
|
56
58
|
tool_choice={"type": "tool", "name": "web_search"},
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
) as stream:
|
|
60
|
+
logger.debug("🌊 Started streaming Anthropic web search response")
|
|
61
|
+
|
|
62
|
+
for event in stream:
|
|
63
|
+
if event.type == "content_block_delta":
|
|
64
|
+
if hasattr(event.delta, "text"):
|
|
65
|
+
result_text += event.delta.text
|
|
66
|
+
elif event.type == "message_start":
|
|
67
|
+
logger.debug("🚀 Streaming started")
|
|
68
|
+
elif event.type == "message_stop":
|
|
69
|
+
logger.debug("✅ Streaming completed")
|
|
68
70
|
|
|
69
71
|
if not result_text:
|
|
70
72
|
result_text = "No content returned from search"
|
|
@@ -84,3 +86,62 @@ def anthropic_web_search_tool(query: str) -> str:
|
|
|
84
86
|
logger.debug("💥 Full error details: %s", error_msg)
|
|
85
87
|
span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
|
|
86
88
|
return error_msg
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main() -> None:
|
|
92
|
+
"""Main function for testing the Anthropic web search tool."""
|
|
93
|
+
import logging
|
|
94
|
+
import os
|
|
95
|
+
import sys
|
|
96
|
+
|
|
97
|
+
# Set up basic console logging for testing
|
|
98
|
+
logging.basicConfig(
|
|
99
|
+
level=logging.DEBUG,
|
|
100
|
+
format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
|
|
101
|
+
stream=sys.stdout,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if len(sys.argv) < 2:
|
|
105
|
+
print(
|
|
106
|
+
"Usage: python -m shotgun.agents.tools.web_search.anthropic <search_query>"
|
|
107
|
+
)
|
|
108
|
+
print(
|
|
109
|
+
"Example: python -m shotgun.agents.tools.web_search.anthropic 'latest Python updates'"
|
|
110
|
+
)
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
|
|
113
|
+
# Join all arguments as the search query
|
|
114
|
+
query = " ".join(sys.argv[1:])
|
|
115
|
+
|
|
116
|
+
print("🔍 Testing Anthropic Web Search with streaming")
|
|
117
|
+
print(f"📝 Query: {query}")
|
|
118
|
+
print("=" * 60)
|
|
119
|
+
|
|
120
|
+
# Check if API key is available
|
|
121
|
+
if not (
|
|
122
|
+
os.getenv("ANTHROPIC_API_KEY")
|
|
123
|
+
or (
|
|
124
|
+
callable(get_provider_model)
|
|
125
|
+
and get_provider_model(ProviderType.ANTHROPIC).api_key
|
|
126
|
+
)
|
|
127
|
+
):
|
|
128
|
+
print("❌ Error: ANTHROPIC_API_KEY environment variable not set")
|
|
129
|
+
print(" Please set it with: export ANTHROPIC_API_KEY=your_key_here")
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
result = anthropic_web_search_tool(query)
|
|
134
|
+
print(f"✅ Search completed! Result length: {len(result)} characters")
|
|
135
|
+
print("=" * 60)
|
|
136
|
+
print("📄 RESULTS:")
|
|
137
|
+
print("=" * 60)
|
|
138
|
+
print(result)
|
|
139
|
+
except KeyboardInterrupt:
|
|
140
|
+
print("\n⏹️ Search interrupted by user")
|
|
141
|
+
except Exception as e:
|
|
142
|
+
print(f"❌ Error during search: {e}")
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
main()
|
|
@@ -40,17 +40,31 @@ def openai_web_search_tool(query: str) -> str:
|
|
|
40
40
|
span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
|
|
41
41
|
return error_msg
|
|
42
42
|
|
|
43
|
+
prompt = f"""Please provide current and accurate information about the following query:
|
|
44
|
+
|
|
45
|
+
Query: {query}
|
|
46
|
+
|
|
47
|
+
Instructions:
|
|
48
|
+
- Provide comprehensive, factual information
|
|
49
|
+
- Include relevant details and context
|
|
50
|
+
- Focus on current and recent information
|
|
51
|
+
- Be specific and accurate in your response
|
|
52
|
+
- You can't ask the user for details, so assume the most relevant details for the query
|
|
53
|
+
|
|
54
|
+
ALWAYS PROVIDE THE SOURCES (urls) TO BACK UP THE INFORMATION YOU PROVIDE.
|
|
55
|
+
"""
|
|
56
|
+
|
|
43
57
|
client = OpenAI(api_key=api_key)
|
|
44
58
|
response = client.responses.create( # type: ignore[call-overload]
|
|
45
59
|
model="gpt-5-mini",
|
|
46
60
|
input=[
|
|
47
|
-
{"role": "user", "content": [{"type": "input_text", "text":
|
|
61
|
+
{"role": "user", "content": [{"type": "input_text", "text": prompt}]}
|
|
48
62
|
],
|
|
49
63
|
text={
|
|
50
64
|
"format": {"type": "text"},
|
|
51
65
|
"verbosity": "high",
|
|
52
66
|
}, # Increased from medium
|
|
53
|
-
reasoning={"effort": "
|
|
67
|
+
reasoning={"effort": "medium", "summary": "auto"},
|
|
54
68
|
tools=[
|
|
55
69
|
{
|
|
56
70
|
"type": "web_search",
|
shotgun/artifacts/manager.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import Any
|
|
|
7
7
|
import yaml
|
|
8
8
|
|
|
9
9
|
from shotgun.logging_config import setup_logger
|
|
10
|
+
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
10
11
|
|
|
11
12
|
from .exceptions import (
|
|
12
13
|
ArtifactFileSystemError,
|
|
@@ -32,7 +33,7 @@ class ArtifactManager:
|
|
|
32
33
|
base_path: Base path for artifacts. Defaults to .shotgun in current directory.
|
|
33
34
|
"""
|
|
34
35
|
if base_path is None:
|
|
35
|
-
base_path =
|
|
36
|
+
base_path = get_shotgun_base_path()
|
|
36
37
|
elif isinstance(base_path, str):
|
|
37
38
|
base_path = Path(base_path)
|
|
38
39
|
|