shotgun-sh 0.2.6.dev1__py3-none-any.whl → 0.2.17__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.
- shotgun/agents/agent_manager.py +694 -73
- shotgun/agents/common.py +69 -70
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +70 -35
- shotgun/agents/config/models.py +41 -1
- shotgun/agents/config/provider.py +33 -5
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +471 -0
- shotgun/agents/context_analyzer/constants.py +9 -0
- shotgun/agents/context_analyzer/formatter.py +115 -0
- shotgun/agents/context_analyzer/models.py +212 -0
- shotgun/agents/conversation_history.py +125 -2
- shotgun/agents/conversation_manager.py +57 -19
- shotgun/agents/export.py +6 -7
- shotgun/agents/history/compaction.py +9 -4
- shotgun/agents/history/context_extraction.py +93 -6
- shotgun/agents/history/history_processors.py +113 -5
- shotgun/agents/history/token_counting/anthropic.py +39 -3
- shotgun/agents/history/token_counting/base.py +14 -3
- shotgun/agents/history/token_counting/openai.py +11 -1
- shotgun/agents/history/token_counting/sentencepiece_counter.py +8 -0
- shotgun/agents/history/token_counting/tokenizer_cache.py +3 -1
- shotgun/agents/history/token_counting/utils.py +0 -3
- shotgun/agents/models.py +50 -2
- shotgun/agents/plan.py +6 -7
- shotgun/agents/research.py +7 -8
- shotgun/agents/specify.py +6 -7
- shotgun/agents/tasks.py +6 -7
- shotgun/agents/tools/__init__.py +0 -2
- shotgun/agents/tools/codebase/codebase_shell.py +6 -0
- shotgun/agents/tools/codebase/directory_lister.py +6 -0
- shotgun/agents/tools/codebase/file_read.py +11 -2
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +82 -16
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +8 -8
- shotgun/agents/tools/web_search/anthropic.py +8 -2
- shotgun/agents/tools/web_search/gemini.py +7 -1
- shotgun/agents/tools/web_search/openai.py +7 -1
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +16 -11
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +3 -3
- shotgun/cli/clear.py +53 -0
- shotgun/cli/compact.py +186 -0
- shotgun/cli/config.py +8 -5
- shotgun/cli/context.py +111 -0
- shotgun/cli/export.py +1 -1
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +1 -0
- shotgun/cli/plan.py +1 -1
- shotgun/cli/research.py +1 -1
- shotgun/cli/specify.py +1 -1
- shotgun/cli/tasks.py +1 -1
- shotgun/cli/update.py +16 -2
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +10 -8
- shotgun/codebase/core/manager.py +13 -4
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/exceptions.py +32 -0
- shotgun/logging_config.py +18 -27
- shotgun/main.py +73 -11
- shotgun/posthog_telemetry.py +37 -28
- shotgun/prompts/agents/export.j2 +18 -1
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
- shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
- shotgun/prompts/agents/plan.j2 +1 -1
- shotgun/prompts/agents/research.j2 +1 -1
- shotgun/prompts/agents/specify.j2 +270 -3
- shotgun/prompts/agents/tasks.j2 +1 -1
- shotgun/sentry_telemetry.py +163 -16
- shotgun/settings.py +238 -0
- shotgun/telemetry.py +18 -33
- shotgun/tui/app.py +243 -43
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +179 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +91 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/protocols.py +45 -0
- shotgun/tui/screens/chat/__init__.py +5 -0
- shotgun/tui/screens/chat/chat.tcss +54 -0
- shotgun/tui/screens/chat/chat_screen.py +1254 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +40 -0
- shotgun/tui/screens/chat/prompt_history.py +48 -0
- shotgun/tui/screens/chat.tcss +11 -0
- shotgun/tui/screens/chat_screen/command_providers.py +78 -2
- shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
- shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +115 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
- shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
- shotgun/tui/screens/confirmation_dialog.py +151 -0
- shotgun/tui/screens/feedback.py +4 -4
- shotgun/tui/screens/github_issue.py +102 -0
- shotgun/tui/screens/model_picker.py +49 -24
- shotgun/tui/screens/onboarding.py +431 -0
- shotgun/tui/screens/pipx_migration.py +153 -0
- shotgun/tui/screens/provider_config.py +50 -27
- shotgun/tui/screens/shotgun_auth.py +2 -2
- shotgun/tui/screens/welcome.py +23 -12
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +184 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/utils/mode_progress.py +14 -7
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +263 -0
- shotgun/utils/file_system_utils.py +22 -2
- shotgun/utils/marketing.py +110 -0
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.2.17.dist-info/METADATA +465 -0
- shotgun_sh-0.2.17.dist-info/RECORD +194 -0
- {shotgun_sh-0.2.6.dev1.dist-info → shotgun_sh-0.2.17.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.6.dev1.dist-info → shotgun_sh-0.2.17.dist-info}/licenses/LICENSE +1 -1
- shotgun/agents/tools/user_interaction.py +0 -37
- shotgun/tui/screens/chat.py +0 -804
- shotgun/tui/screens/chat_screen/history.py +0 -401
- shotgun_sh-0.2.6.dev1.dist-info/METADATA +0 -467
- shotgun_sh-0.2.6.dev1.dist-info/RECORD +0 -156
- {shotgun_sh-0.2.6.dev1.dist-info → shotgun_sh-0.2.17.dist-info}/WHEEL +0 -0
shotgun/agents/plan.py
CHANGED
|
@@ -4,7 +4,6 @@ from functools import partial
|
|
|
4
4
|
|
|
5
5
|
from pydantic_ai import (
|
|
6
6
|
Agent,
|
|
7
|
-
DeferredToolRequests,
|
|
8
7
|
)
|
|
9
8
|
from pydantic_ai.agent import AgentRunResult
|
|
10
9
|
from pydantic_ai.messages import ModelMessage
|
|
@@ -19,14 +18,14 @@ from .common import (
|
|
|
19
18
|
create_usage_limits,
|
|
20
19
|
run_agent,
|
|
21
20
|
)
|
|
22
|
-
from .models import AgentDeps, AgentRuntimeOptions, AgentType
|
|
21
|
+
from .models import AgentDeps, AgentResponse, AgentRuntimeOptions, AgentType
|
|
23
22
|
|
|
24
23
|
logger = get_logger(__name__)
|
|
25
24
|
|
|
26
25
|
|
|
27
|
-
def create_plan_agent(
|
|
26
|
+
async def create_plan_agent(
|
|
28
27
|
agent_runtime_options: AgentRuntimeOptions, provider: ProviderType | None = None
|
|
29
|
-
) -> tuple[Agent[AgentDeps,
|
|
28
|
+
) -> tuple[Agent[AgentDeps, AgentResponse], AgentDeps]:
|
|
30
29
|
"""Create a plan agent with artifact management capabilities.
|
|
31
30
|
|
|
32
31
|
Args:
|
|
@@ -40,7 +39,7 @@ def create_plan_agent(
|
|
|
40
39
|
# Use partial to create system prompt function for plan agent
|
|
41
40
|
system_prompt_fn = partial(build_agent_system_prompt, "plan")
|
|
42
41
|
|
|
43
|
-
agent, deps = create_base_agent(
|
|
42
|
+
agent, deps = await create_base_agent(
|
|
44
43
|
system_prompt_fn,
|
|
45
44
|
agent_runtime_options,
|
|
46
45
|
load_codebase_understanding_tools=True,
|
|
@@ -52,11 +51,11 @@ def create_plan_agent(
|
|
|
52
51
|
|
|
53
52
|
|
|
54
53
|
async def run_plan_agent(
|
|
55
|
-
agent: Agent[AgentDeps,
|
|
54
|
+
agent: Agent[AgentDeps, AgentResponse],
|
|
56
55
|
goal: str,
|
|
57
56
|
deps: AgentDeps,
|
|
58
57
|
message_history: list[ModelMessage] | None = None,
|
|
59
|
-
) -> AgentRunResult[
|
|
58
|
+
) -> AgentRunResult[AgentResponse]:
|
|
60
59
|
"""Create or update a plan based on the given goal using artifacts.
|
|
61
60
|
|
|
62
61
|
Args:
|
shotgun/agents/research.py
CHANGED
|
@@ -4,7 +4,6 @@ from functools import partial
|
|
|
4
4
|
|
|
5
5
|
from pydantic_ai import (
|
|
6
6
|
Agent,
|
|
7
|
-
DeferredToolRequests,
|
|
8
7
|
)
|
|
9
8
|
from pydantic_ai.agent import AgentRunResult
|
|
10
9
|
from pydantic_ai.messages import (
|
|
@@ -21,15 +20,15 @@ from .common import (
|
|
|
21
20
|
create_usage_limits,
|
|
22
21
|
run_agent,
|
|
23
22
|
)
|
|
24
|
-
from .models import AgentDeps, AgentRuntimeOptions, AgentType
|
|
23
|
+
from .models import AgentDeps, AgentResponse, AgentRuntimeOptions, AgentType
|
|
25
24
|
from .tools import get_available_web_search_tools
|
|
26
25
|
|
|
27
26
|
logger = get_logger(__name__)
|
|
28
27
|
|
|
29
28
|
|
|
30
|
-
def create_research_agent(
|
|
29
|
+
async def create_research_agent(
|
|
31
30
|
agent_runtime_options: AgentRuntimeOptions, provider: ProviderType | None = None
|
|
32
|
-
) -> tuple[Agent[AgentDeps,
|
|
31
|
+
) -> tuple[Agent[AgentDeps, AgentResponse], AgentDeps]:
|
|
33
32
|
"""Create a research agent with web search and artifact management capabilities.
|
|
34
33
|
|
|
35
34
|
Args:
|
|
@@ -42,7 +41,7 @@ def create_research_agent(
|
|
|
42
41
|
logger.debug("Initializing research agent")
|
|
43
42
|
|
|
44
43
|
# Get available web search tools based on configured API keys
|
|
45
|
-
web_search_tools = get_available_web_search_tools()
|
|
44
|
+
web_search_tools = await get_available_web_search_tools()
|
|
46
45
|
if web_search_tools:
|
|
47
46
|
logger.info(
|
|
48
47
|
"Research agent configured with %d web search tool(s)",
|
|
@@ -54,7 +53,7 @@ def create_research_agent(
|
|
|
54
53
|
# Use partial to create system prompt function for research agent
|
|
55
54
|
system_prompt_fn = partial(build_agent_system_prompt, "research")
|
|
56
55
|
|
|
57
|
-
agent, deps = create_base_agent(
|
|
56
|
+
agent, deps = await create_base_agent(
|
|
58
57
|
system_prompt_fn,
|
|
59
58
|
agent_runtime_options,
|
|
60
59
|
load_codebase_understanding_tools=True,
|
|
@@ -66,11 +65,11 @@ def create_research_agent(
|
|
|
66
65
|
|
|
67
66
|
|
|
68
67
|
async def run_research_agent(
|
|
69
|
-
agent: Agent[AgentDeps,
|
|
68
|
+
agent: Agent[AgentDeps, AgentResponse],
|
|
70
69
|
query: str,
|
|
71
70
|
deps: AgentDeps,
|
|
72
71
|
message_history: list[ModelMessage] | None = None,
|
|
73
|
-
) -> AgentRunResult[
|
|
72
|
+
) -> AgentRunResult[AgentResponse]:
|
|
74
73
|
"""Perform research on the given query and update research artifacts.
|
|
75
74
|
|
|
76
75
|
Args:
|
shotgun/agents/specify.py
CHANGED
|
@@ -4,7 +4,6 @@ from functools import partial
|
|
|
4
4
|
|
|
5
5
|
from pydantic_ai import (
|
|
6
6
|
Agent,
|
|
7
|
-
DeferredToolRequests,
|
|
8
7
|
)
|
|
9
8
|
from pydantic_ai.agent import AgentRunResult
|
|
10
9
|
from pydantic_ai.messages import ModelMessage
|
|
@@ -19,14 +18,14 @@ from .common import (
|
|
|
19
18
|
create_usage_limits,
|
|
20
19
|
run_agent,
|
|
21
20
|
)
|
|
22
|
-
from .models import AgentDeps, AgentRuntimeOptions, AgentType
|
|
21
|
+
from .models import AgentDeps, AgentResponse, AgentRuntimeOptions, AgentType
|
|
23
22
|
|
|
24
23
|
logger = get_logger(__name__)
|
|
25
24
|
|
|
26
25
|
|
|
27
|
-
def create_specify_agent(
|
|
26
|
+
async def create_specify_agent(
|
|
28
27
|
agent_runtime_options: AgentRuntimeOptions, provider: ProviderType | None = None
|
|
29
|
-
) -> tuple[Agent[AgentDeps,
|
|
28
|
+
) -> tuple[Agent[AgentDeps, AgentResponse], AgentDeps]:
|
|
30
29
|
"""Create a specify agent with artifact management capabilities.
|
|
31
30
|
|
|
32
31
|
Args:
|
|
@@ -40,7 +39,7 @@ def create_specify_agent(
|
|
|
40
39
|
# Use partial to create system prompt function for specify agent
|
|
41
40
|
system_prompt_fn = partial(build_agent_system_prompt, "specify")
|
|
42
41
|
|
|
43
|
-
agent, deps = create_base_agent(
|
|
42
|
+
agent, deps = await create_base_agent(
|
|
44
43
|
system_prompt_fn,
|
|
45
44
|
agent_runtime_options,
|
|
46
45
|
load_codebase_understanding_tools=True,
|
|
@@ -52,11 +51,11 @@ def create_specify_agent(
|
|
|
52
51
|
|
|
53
52
|
|
|
54
53
|
async def run_specify_agent(
|
|
55
|
-
agent: Agent[AgentDeps,
|
|
54
|
+
agent: Agent[AgentDeps, AgentResponse],
|
|
56
55
|
requirement: str,
|
|
57
56
|
deps: AgentDeps,
|
|
58
57
|
message_history: list[ModelMessage] | None = None,
|
|
59
|
-
) -> AgentRunResult[
|
|
58
|
+
) -> AgentRunResult[AgentResponse]:
|
|
60
59
|
"""Create or update specifications based on the given requirement.
|
|
61
60
|
|
|
62
61
|
Args:
|
shotgun/agents/tasks.py
CHANGED
|
@@ -4,7 +4,6 @@ from functools import partial
|
|
|
4
4
|
|
|
5
5
|
from pydantic_ai import (
|
|
6
6
|
Agent,
|
|
7
|
-
DeferredToolRequests,
|
|
8
7
|
)
|
|
9
8
|
from pydantic_ai.agent import AgentRunResult
|
|
10
9
|
from pydantic_ai.messages import ModelMessage
|
|
@@ -19,14 +18,14 @@ from .common import (
|
|
|
19
18
|
create_usage_limits,
|
|
20
19
|
run_agent,
|
|
21
20
|
)
|
|
22
|
-
from .models import AgentDeps, AgentRuntimeOptions, AgentType
|
|
21
|
+
from .models import AgentDeps, AgentResponse, AgentRuntimeOptions, AgentType
|
|
23
22
|
|
|
24
23
|
logger = get_logger(__name__)
|
|
25
24
|
|
|
26
25
|
|
|
27
|
-
def create_tasks_agent(
|
|
26
|
+
async def create_tasks_agent(
|
|
28
27
|
agent_runtime_options: AgentRuntimeOptions, provider: ProviderType | None = None
|
|
29
|
-
) -> tuple[Agent[AgentDeps,
|
|
28
|
+
) -> tuple[Agent[AgentDeps, AgentResponse], AgentDeps]:
|
|
30
29
|
"""Create a tasks agent with file management capabilities.
|
|
31
30
|
|
|
32
31
|
Args:
|
|
@@ -40,7 +39,7 @@ def create_tasks_agent(
|
|
|
40
39
|
# Use partial to create system prompt function for tasks agent
|
|
41
40
|
system_prompt_fn = partial(build_agent_system_prompt, "tasks")
|
|
42
41
|
|
|
43
|
-
agent, deps = create_base_agent(
|
|
42
|
+
agent, deps = await create_base_agent(
|
|
44
43
|
system_prompt_fn,
|
|
45
44
|
agent_runtime_options,
|
|
46
45
|
provider=provider,
|
|
@@ -50,11 +49,11 @@ def create_tasks_agent(
|
|
|
50
49
|
|
|
51
50
|
|
|
52
51
|
async def run_tasks_agent(
|
|
53
|
-
agent: Agent[AgentDeps,
|
|
52
|
+
agent: Agent[AgentDeps, AgentResponse],
|
|
54
53
|
instruction: str,
|
|
55
54
|
deps: AgentDeps,
|
|
56
55
|
message_history: list[ModelMessage] | None = None,
|
|
57
|
-
) -> AgentRunResult[
|
|
56
|
+
) -> AgentRunResult[AgentResponse]:
|
|
58
57
|
"""Create or update tasks based on the given instruction.
|
|
59
58
|
|
|
60
59
|
Args:
|
shotgun/agents/tools/__init__.py
CHANGED
|
@@ -8,7 +8,6 @@ from .codebase import (
|
|
|
8
8
|
retrieve_code,
|
|
9
9
|
)
|
|
10
10
|
from .file_management import append_file, read_file, write_file
|
|
11
|
-
from .user_interaction import ask_user
|
|
12
11
|
from .web_search import (
|
|
13
12
|
anthropic_web_search_tool,
|
|
14
13
|
gemini_web_search_tool,
|
|
@@ -21,7 +20,6 @@ __all__ = [
|
|
|
21
20
|
"anthropic_web_search_tool",
|
|
22
21
|
"gemini_web_search_tool",
|
|
23
22
|
"get_available_web_search_tools",
|
|
24
|
-
"ask_user",
|
|
25
23
|
"read_file",
|
|
26
24
|
"write_file",
|
|
27
25
|
"append_file",
|
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
from pydantic_ai import RunContext
|
|
9
9
|
|
|
10
10
|
from shotgun.agents.models import AgentDeps
|
|
11
|
+
from shotgun.agents.tools.registry import ToolCategory, register_tool
|
|
11
12
|
from shotgun.logging_config import get_logger
|
|
12
13
|
|
|
13
14
|
from .models import ShellCommandResult
|
|
@@ -48,6 +49,11 @@ DANGEROUS_PATTERNS = [
|
|
|
48
49
|
]
|
|
49
50
|
|
|
50
51
|
|
|
52
|
+
@register_tool(
|
|
53
|
+
category=ToolCategory.CODEBASE_UNDERSTANDING,
|
|
54
|
+
display_text="Running shell",
|
|
55
|
+
key_arg="command",
|
|
56
|
+
)
|
|
51
57
|
async def codebase_shell(
|
|
52
58
|
ctx: RunContext[AgentDeps],
|
|
53
59
|
command: str,
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from pydantic_ai import RunContext
|
|
6
6
|
|
|
7
7
|
from shotgun.agents.models import AgentDeps
|
|
8
|
+
from shotgun.agents.tools.registry import ToolCategory, register_tool
|
|
8
9
|
from shotgun.logging_config import get_logger
|
|
9
10
|
|
|
10
11
|
from .models import DirectoryListResult
|
|
@@ -12,6 +13,11 @@ from .models import DirectoryListResult
|
|
|
12
13
|
logger = get_logger(__name__)
|
|
13
14
|
|
|
14
15
|
|
|
16
|
+
@register_tool(
|
|
17
|
+
category=ToolCategory.CODEBASE_UNDERSTANDING,
|
|
18
|
+
display_text="Listing directory",
|
|
19
|
+
key_arg="directory",
|
|
20
|
+
)
|
|
15
21
|
async def directory_lister(
|
|
16
22
|
ctx: RunContext[AgentDeps], graph_id: str, directory: str = "."
|
|
17
23
|
) -> DirectoryListResult:
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
+
import aiofiles
|
|
5
6
|
from pydantic_ai import RunContext
|
|
6
7
|
|
|
7
8
|
from shotgun.agents.models import AgentDeps
|
|
9
|
+
from shotgun.agents.tools.registry import ToolCategory, register_tool
|
|
8
10
|
from shotgun.codebase.core.language_config import get_language_config
|
|
9
11
|
from shotgun.logging_config import get_logger
|
|
10
12
|
|
|
@@ -13,6 +15,11 @@ from .models import FileReadResult
|
|
|
13
15
|
logger = get_logger(__name__)
|
|
14
16
|
|
|
15
17
|
|
|
18
|
+
@register_tool(
|
|
19
|
+
category=ToolCategory.CODEBASE_UNDERSTANDING,
|
|
20
|
+
display_text="Reading file",
|
|
21
|
+
key_arg="file_path",
|
|
22
|
+
)
|
|
16
23
|
async def file_read(
|
|
17
24
|
ctx: RunContext[AgentDeps], graph_id: str, file_path: str
|
|
18
25
|
) -> FileReadResult:
|
|
@@ -87,7 +94,8 @@ async def file_read(
|
|
|
87
94
|
# Read file contents
|
|
88
95
|
encoding_used = "utf-8"
|
|
89
96
|
try:
|
|
90
|
-
|
|
97
|
+
async with aiofiles.open(full_file_path, encoding="utf-8") as f:
|
|
98
|
+
content = await f.read()
|
|
91
99
|
size_bytes = full_file_path.stat().st_size
|
|
92
100
|
|
|
93
101
|
logger.debug(
|
|
@@ -113,7 +121,8 @@ async def file_read(
|
|
|
113
121
|
try:
|
|
114
122
|
# Try with different encoding
|
|
115
123
|
encoding_used = "latin-1"
|
|
116
|
-
|
|
124
|
+
async with aiofiles.open(full_file_path, encoding="latin-1") as f:
|
|
125
|
+
content = await f.read()
|
|
117
126
|
size_bytes = full_file_path.stat().st_size
|
|
118
127
|
|
|
119
128
|
# Detect language from file extension
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from pydantic_ai import RunContext
|
|
4
4
|
|
|
5
5
|
from shotgun.agents.models import AgentDeps
|
|
6
|
+
from shotgun.agents.tools.registry import ToolCategory, register_tool
|
|
6
7
|
from shotgun.codebase.models import QueryType
|
|
7
8
|
from shotgun.logging_config import get_logger
|
|
8
9
|
|
|
@@ -11,6 +12,11 @@ from .models import QueryGraphResult
|
|
|
11
12
|
logger = get_logger(__name__)
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
@register_tool(
|
|
16
|
+
category=ToolCategory.CODEBASE_UNDERSTANDING,
|
|
17
|
+
display_text="Querying code",
|
|
18
|
+
key_arg="query",
|
|
19
|
+
)
|
|
14
20
|
async def query_graph(
|
|
15
21
|
ctx: RunContext[AgentDeps], graph_id: str, query: str
|
|
16
22
|
) -> QueryGraphResult:
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from pydantic_ai import RunContext
|
|
6
6
|
|
|
7
7
|
from shotgun.agents.models import AgentDeps
|
|
8
|
+
from shotgun.agents.tools.registry import ToolCategory, register_tool
|
|
8
9
|
from shotgun.codebase.core.code_retrieval import retrieve_code_by_qualified_name
|
|
9
10
|
from shotgun.codebase.core.language_config import get_language_config
|
|
10
11
|
from shotgun.logging_config import get_logger
|
|
@@ -14,6 +15,11 @@ from .models import CodeSnippetResult
|
|
|
14
15
|
logger = get_logger(__name__)
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
@register_tool(
|
|
19
|
+
category=ToolCategory.CODEBASE_UNDERSTANDING,
|
|
20
|
+
display_text="Retrieving code",
|
|
21
|
+
key_arg="qualified_name",
|
|
22
|
+
)
|
|
17
23
|
async def retrieve_code(
|
|
18
24
|
ctx: RunContext[AgentDeps], graph_id: str, qualified_name: str
|
|
19
25
|
) -> CodeSnippetResult:
|
|
@@ -6,20 +6,30 @@ 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
|
+
import aiofiles
|
|
10
|
+
import aiofiles.os
|
|
9
11
|
from pydantic_ai import RunContext
|
|
10
12
|
|
|
11
13
|
from shotgun.agents.models import AgentDeps, AgentType, FileOperationType
|
|
14
|
+
from shotgun.agents.tools.registry import ToolCategory, register_tool
|
|
12
15
|
from shotgun.logging_config import get_logger
|
|
13
16
|
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
14
17
|
|
|
15
18
|
logger = get_logger(__name__)
|
|
16
19
|
|
|
17
20
|
# Map agent modes to their allowed directories/files (in workflow order)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
# Values can be:
|
|
22
|
+
# - A Path: exact file (e.g., Path("research.md"))
|
|
23
|
+
# - A list of Paths: multiple allowed files/directories (e.g., [Path("specification.md"), Path("contracts")])
|
|
24
|
+
# - "*": any file except protected files (for export agent)
|
|
25
|
+
AGENT_DIRECTORIES: dict[AgentType, str | Path | list[Path]] = {
|
|
26
|
+
AgentType.RESEARCH: Path("research.md"),
|
|
27
|
+
AgentType.SPECIFY: [
|
|
28
|
+
Path("specification.md"),
|
|
29
|
+
Path("contracts"),
|
|
30
|
+
], # Specify can write specs and contract files
|
|
31
|
+
AgentType.PLAN: Path("plan.md"),
|
|
32
|
+
AgentType.TASKS: Path("tasks.md"),
|
|
23
33
|
AgentType.EXPORT: "*", # Export agent can write anywhere except protected files
|
|
24
34
|
}
|
|
25
35
|
|
|
@@ -60,13 +70,52 @@ def _validate_agent_scoped_path(filename: str, agent_mode: AgentType | None) ->
|
|
|
60
70
|
# Allow writing anywhere else in .shotgun directory
|
|
61
71
|
full_path = (base_path / filename).resolve()
|
|
62
72
|
else:
|
|
63
|
-
# For other agents,
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
# For other agents, check if they have access to the requested file
|
|
74
|
+
allowed_paths_raw = AGENT_DIRECTORIES[agent_mode]
|
|
75
|
+
|
|
76
|
+
# Convert single Path/string to list of Paths for uniform handling
|
|
77
|
+
if isinstance(allowed_paths_raw, str):
|
|
78
|
+
# Special case: "*" means export agent
|
|
79
|
+
allowed_paths = (
|
|
80
|
+
[Path(allowed_paths_raw)] if allowed_paths_raw != "*" else []
|
|
81
|
+
)
|
|
82
|
+
elif isinstance(allowed_paths_raw, Path):
|
|
83
|
+
allowed_paths = [allowed_paths_raw]
|
|
84
|
+
else:
|
|
85
|
+
# Already a list
|
|
86
|
+
allowed_paths = allowed_paths_raw
|
|
87
|
+
|
|
88
|
+
# Check if filename matches any allowed path
|
|
89
|
+
is_allowed = False
|
|
90
|
+
for allowed_path in allowed_paths:
|
|
91
|
+
allowed_str = str(allowed_path)
|
|
92
|
+
|
|
93
|
+
# Check if it's a directory (no .md extension or suffix)
|
|
94
|
+
# Directories: Path("contracts") has no suffix, files: Path("spec.md") has .md suffix
|
|
95
|
+
if not allowed_path.suffix or (
|
|
96
|
+
allowed_path.suffix and not allowed_str.endswith(".md")
|
|
97
|
+
):
|
|
98
|
+
# Directory - allow any file within this directory
|
|
99
|
+
# Check both "contracts/file.py" and "contracts" prefix
|
|
100
|
+
if (
|
|
101
|
+
filename.startswith(allowed_str + "/")
|
|
102
|
+
or filename == allowed_str
|
|
103
|
+
):
|
|
104
|
+
is_allowed = True
|
|
105
|
+
break
|
|
106
|
+
else:
|
|
107
|
+
# Exact file match
|
|
108
|
+
if filename == allowed_str:
|
|
109
|
+
is_allowed = True
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
if not is_allowed:
|
|
113
|
+
allowed_display = ", ".join(f"'{p}'" for p in allowed_paths)
|
|
66
114
|
raise ValueError(
|
|
67
|
-
f"{agent_mode.value.capitalize()} agent can only write to
|
|
115
|
+
f"{agent_mode.value.capitalize()} agent can only write to {allowed_display}. "
|
|
68
116
|
f"Attempted to write to '{filename}'"
|
|
69
117
|
)
|
|
118
|
+
|
|
70
119
|
full_path = (base_path / filename).resolve()
|
|
71
120
|
else:
|
|
72
121
|
# No agent mode specified, fall back to old validation
|
|
@@ -111,6 +160,11 @@ def _validate_shotgun_path(filename: str) -> Path:
|
|
|
111
160
|
return full_path
|
|
112
161
|
|
|
113
162
|
|
|
163
|
+
@register_tool(
|
|
164
|
+
category=ToolCategory.ARTIFACT_MANAGEMENT,
|
|
165
|
+
display_text="Reading file",
|
|
166
|
+
key_arg="filename",
|
|
167
|
+
)
|
|
114
168
|
async def read_file(ctx: RunContext[AgentDeps], filename: str) -> str:
|
|
115
169
|
"""Read a file from the .shotgun directory.
|
|
116
170
|
|
|
@@ -129,10 +183,11 @@ async def read_file(ctx: RunContext[AgentDeps], filename: str) -> str:
|
|
|
129
183
|
try:
|
|
130
184
|
file_path = _validate_shotgun_path(filename)
|
|
131
185
|
|
|
132
|
-
if not
|
|
186
|
+
if not await aiofiles.os.path.exists(file_path):
|
|
133
187
|
raise FileNotFoundError(f"File not found: {filename}")
|
|
134
188
|
|
|
135
|
-
|
|
189
|
+
async with aiofiles.open(file_path, encoding="utf-8") as f:
|
|
190
|
+
content = await f.read()
|
|
136
191
|
logger.debug("📄 Read %d characters from %s", len(content), filename)
|
|
137
192
|
return content
|
|
138
193
|
|
|
@@ -142,6 +197,11 @@ async def read_file(ctx: RunContext[AgentDeps], filename: str) -> str:
|
|
|
142
197
|
return error_msg
|
|
143
198
|
|
|
144
199
|
|
|
200
|
+
@register_tool(
|
|
201
|
+
category=ToolCategory.ARTIFACT_MANAGEMENT,
|
|
202
|
+
display_text="Writing file",
|
|
203
|
+
key_arg="filename",
|
|
204
|
+
)
|
|
145
205
|
async def write_file(
|
|
146
206
|
ctx: RunContext[AgentDeps],
|
|
147
207
|
filename: str,
|
|
@@ -176,21 +236,22 @@ async def write_file(
|
|
|
176
236
|
else:
|
|
177
237
|
operation = (
|
|
178
238
|
FileOperationType.CREATED
|
|
179
|
-
if not
|
|
239
|
+
if not await aiofiles.os.path.exists(file_path)
|
|
180
240
|
else FileOperationType.UPDATED
|
|
181
241
|
)
|
|
182
242
|
|
|
183
243
|
# Ensure parent directory exists
|
|
184
|
-
file_path.parent
|
|
244
|
+
await aiofiles.os.makedirs(file_path.parent, exist_ok=True)
|
|
185
245
|
|
|
186
246
|
# Write content
|
|
187
247
|
if mode == "a":
|
|
188
|
-
with open(file_path, "a", encoding="utf-8") as f:
|
|
189
|
-
f.write(content)
|
|
248
|
+
async with aiofiles.open(file_path, "a", encoding="utf-8") as f:
|
|
249
|
+
await f.write(content)
|
|
190
250
|
logger.debug("📄 Appended %d characters to %s", len(content), filename)
|
|
191
251
|
result = f"Successfully appended {len(content)} characters to {filename}"
|
|
192
252
|
else:
|
|
193
|
-
|
|
253
|
+
async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
|
|
254
|
+
await f.write(content)
|
|
194
255
|
logger.debug("📄 Wrote %d characters to %s", len(content), filename)
|
|
195
256
|
result = f"Successfully wrote {len(content)} characters to {filename}"
|
|
196
257
|
|
|
@@ -205,6 +266,11 @@ async def write_file(
|
|
|
205
266
|
return error_msg
|
|
206
267
|
|
|
207
268
|
|
|
269
|
+
@register_tool(
|
|
270
|
+
category=ToolCategory.ARTIFACT_MANAGEMENT,
|
|
271
|
+
display_text="Appending to file",
|
|
272
|
+
key_arg="filename",
|
|
273
|
+
)
|
|
208
274
|
async def append_file(ctx: RunContext[AgentDeps], filename: str, content: str) -> str:
|
|
209
275
|
"""Append content to a file in the .shotgun directory.
|
|
210
276
|
|