kagent-adk 0.7.11__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.
@@ -0,0 +1,74 @@
1
+ """Simplified bash tool for executing shell commands in skills context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Any, Dict
8
+
9
+ from google.adk.tools import BaseTool, ToolContext
10
+ from google.genai import types
11
+
12
+ from kagent.skills import execute_command, get_bash_description, get_session_path
13
+
14
+ logger = logging.getLogger("kagent_adk." + __name__)
15
+
16
+
17
+ class BashTool(BaseTool):
18
+ """Execute bash commands safely in the skills environment.
19
+
20
+ This tool uses the Anthropic Sandbox Runtime (srt) to execute commands with:
21
+ - Filesystem restrictions (controlled read/write access)
22
+ - Network restrictions (controlled domain access)
23
+ - Process isolation at the OS level
24
+
25
+ Use it for command-line operations like running scripts, installing packages, etc.
26
+ For file operations (read/write/edit), use the dedicated file tools instead.
27
+ """
28
+
29
+ def __init__(self, skills_directory: str | Path):
30
+ super().__init__(
31
+ name="bash",
32
+ description=get_bash_description(),
33
+ )
34
+ self.skills_directory = Path(skills_directory).resolve()
35
+ if not self.skills_directory.exists():
36
+ raise ValueError(f"Skills directory does not exist: {self.skills_directory}")
37
+
38
+ def _get_declaration(self) -> types.FunctionDeclaration:
39
+ return types.FunctionDeclaration(
40
+ name=self.name,
41
+ description=self.description,
42
+ parameters=types.Schema(
43
+ type=types.Type.OBJECT,
44
+ properties={
45
+ "command": types.Schema(
46
+ type=types.Type.STRING,
47
+ description="Bash command to execute. Use && to chain commands.",
48
+ ),
49
+ "description": types.Schema(
50
+ type=types.Type.STRING,
51
+ description="Clear, concise description of what this command does (5-10 words)",
52
+ ),
53
+ },
54
+ required=["command"],
55
+ ),
56
+ )
57
+
58
+ async def run_async(self, *, args: Dict[str, Any], tool_context: ToolContext) -> str:
59
+ """Execute a bash command safely using the Anthropic Sandbox Runtime."""
60
+ command = args.get("command", "").strip()
61
+ description = args.get("description", "")
62
+
63
+ if not command:
64
+ return "Error: No command provided"
65
+
66
+ try:
67
+ working_dir = get_session_path(session_id=tool_context.session.id)
68
+ result = await execute_command(command, working_dir)
69
+ logger.info(f"Executed bash command: {command}, description: {description}")
70
+ return result
71
+ except Exception as e:
72
+ error_msg = f"Error executing command '{command}': {e}"
73
+ logger.error(error_msg)
74
+ return error_msg
@@ -0,0 +1,192 @@
1
+ """File operation tools for agent skills.
2
+
3
+ This module provides Read, Write, and Edit tools that agents can use to work with
4
+ files on the filesystem within the sandbox environment.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any, Dict
12
+
13
+ from google.adk.tools import BaseTool, ToolContext
14
+ from google.genai import types
15
+
16
+ from kagent.skills import (
17
+ edit_file_content,
18
+ get_edit_file_description,
19
+ get_read_file_description,
20
+ get_session_path,
21
+ get_write_file_description,
22
+ read_file_content,
23
+ write_file_content,
24
+ )
25
+
26
+ logger = logging.getLogger("kagent_adk." + __name__)
27
+
28
+
29
+ class ReadFileTool(BaseTool):
30
+ """Read files with line numbers for precise editing."""
31
+
32
+ def __init__(self):
33
+ super().__init__(
34
+ name="read_file",
35
+ description=get_read_file_description(),
36
+ )
37
+
38
+ def _get_declaration(self) -> types.FunctionDeclaration:
39
+ return types.FunctionDeclaration(
40
+ name=self.name,
41
+ description=self.description,
42
+ parameters=types.Schema(
43
+ type=types.Type.OBJECT,
44
+ properties={
45
+ "file_path": types.Schema(
46
+ type=types.Type.STRING,
47
+ description="Path to the file to read (absolute or relative to working directory)",
48
+ ),
49
+ "offset": types.Schema(
50
+ type=types.Type.INTEGER,
51
+ description="Optional line number to start reading from (1-indexed)",
52
+ ),
53
+ "limit": types.Schema(
54
+ type=types.Type.INTEGER,
55
+ description="Optional number of lines to read",
56
+ ),
57
+ },
58
+ required=["file_path"],
59
+ ),
60
+ )
61
+
62
+ async def run_async(self, *, args: Dict[str, Any], tool_context: ToolContext) -> str:
63
+ """Read a file with line numbers."""
64
+ file_path_str = args.get("file_path", "").strip()
65
+ offset = args.get("offset")
66
+ limit = args.get("limit")
67
+
68
+ if not file_path_str:
69
+ return "Error: No file path provided"
70
+
71
+ try:
72
+ working_dir = get_session_path(session_id=tool_context.session.id)
73
+ path = Path(file_path_str)
74
+ if not path.is_absolute():
75
+ path = working_dir / path
76
+ path = path.resolve()
77
+
78
+ return read_file_content(path, offset, limit)
79
+ except (FileNotFoundError, IsADirectoryError, IOError) as e:
80
+ return f"Error reading file {file_path_str}: {e}"
81
+
82
+
83
+ class WriteFileTool(BaseTool):
84
+ """Write content to files (overwrites existing files)."""
85
+
86
+ def __init__(self):
87
+ super().__init__(
88
+ name="write_file",
89
+ description=get_write_file_description(),
90
+ )
91
+
92
+ def _get_declaration(self) -> types.FunctionDeclaration:
93
+ return types.FunctionDeclaration(
94
+ name=self.name,
95
+ description=self.description,
96
+ parameters=types.Schema(
97
+ type=types.Type.OBJECT,
98
+ properties={
99
+ "file_path": types.Schema(
100
+ type=types.Type.STRING,
101
+ description="Path to the file to write (absolute or relative to working directory)",
102
+ ),
103
+ "content": types.Schema(
104
+ type=types.Type.STRING,
105
+ description="Content to write to the file",
106
+ ),
107
+ },
108
+ required=["file_path", "content"],
109
+ ),
110
+ )
111
+
112
+ async def run_async(self, *, args: Dict[str, Any], tool_context: ToolContext) -> str:
113
+ """Write content to a file."""
114
+ file_path_str = args.get("file_path", "").strip()
115
+ content = args.get("content", "")
116
+
117
+ if not file_path_str:
118
+ return "Error: No file path provided"
119
+
120
+ try:
121
+ working_dir = get_session_path(session_id=tool_context.session.id)
122
+ path = Path(file_path_str)
123
+ if not path.is_absolute():
124
+ path = working_dir / path
125
+ path = path.resolve()
126
+
127
+ return write_file_content(path, content)
128
+ except IOError as e:
129
+ error_msg = f"Error writing file {file_path_str}: {e}"
130
+ logger.error(error_msg)
131
+ return error_msg
132
+
133
+
134
+ class EditFileTool(BaseTool):
135
+ """Edit files by replacing exact string matches."""
136
+
137
+ def __init__(self):
138
+ super().__init__(
139
+ name="edit_file",
140
+ description=get_edit_file_description(),
141
+ )
142
+
143
+ def _get_declaration(self) -> types.FunctionDeclaration:
144
+ return types.FunctionDeclaration(
145
+ name=self.name,
146
+ description=self.description,
147
+ parameters=types.Schema(
148
+ type=types.Type.OBJECT,
149
+ properties={
150
+ "file_path": types.Schema(
151
+ type=types.Type.STRING,
152
+ description="Path to the file to edit (absolute or relative to working directory)",
153
+ ),
154
+ "old_string": types.Schema(
155
+ type=types.Type.STRING,
156
+ description="The exact text to replace (must exist in file)",
157
+ ),
158
+ "new_string": types.Schema(
159
+ type=types.Type.STRING,
160
+ description="The text to replace it with (must be different from old_string)",
161
+ ),
162
+ "replace_all": types.Schema(
163
+ type=types.Type.BOOLEAN,
164
+ description="Replace all occurrences (default: false, only replaces first occurrence)",
165
+ ),
166
+ },
167
+ required=["file_path", "old_string", "new_string"],
168
+ ),
169
+ )
170
+
171
+ async def run_async(self, *, args: Dict[str, Any], tool_context: ToolContext) -> str:
172
+ """Edit a file by replacing old_string with new_string."""
173
+ file_path_str = args.get("file_path", "").strip()
174
+ old_string = args.get("old_string", "")
175
+ new_string = args.get("new_string", "")
176
+ replace_all = args.get("replace_all", False)
177
+
178
+ if not file_path_str:
179
+ return "Error: No file path provided"
180
+
181
+ try:
182
+ working_dir = get_session_path(session_id=tool_context.session.id)
183
+ path = Path(file_path_str)
184
+ if not path.is_absolute():
185
+ path = working_dir / path
186
+ path = path.resolve()
187
+
188
+ return edit_file_content(path, old_string, new_string, replace_all)
189
+ except (FileNotFoundError, IsADirectoryError, ValueError, IOError) as e:
190
+ error_msg = f"Error editing file {file_path_str}: {e}"
191
+ logger.error(error_msg)
192
+ return error_msg
@@ -0,0 +1,104 @@
1
+ """Tool for discovering and loading skills."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Any, Dict
8
+
9
+ from google.adk.tools import BaseTool, ToolContext
10
+ from google.genai import types
11
+
12
+ from kagent.skills import (
13
+ discover_skills,
14
+ generate_skills_tool_description,
15
+ load_skill_content,
16
+ )
17
+
18
+ logger = logging.getLogger("kagent_adk." + __name__)
19
+
20
+
21
+ class SkillsTool(BaseTool):
22
+ """Discover and load skill instructions.
23
+
24
+ This tool dynamically discovers available skills and embeds their metadata in the
25
+ tool description. Agent invokes a skill by name to load its full instructions.
26
+ """
27
+
28
+ def __init__(self, skills_directory: str | Path):
29
+ self.skills_directory = Path(skills_directory).resolve()
30
+ if not self.skills_directory.exists():
31
+ raise ValueError(f"Skills directory does not exist: {self.skills_directory}")
32
+
33
+ self._skill_cache: Dict[str, str] = {}
34
+
35
+ # Generate description with available skills embedded
36
+ description = self._generate_description_with_skills()
37
+
38
+ super().__init__(
39
+ name="skills",
40
+ description=description,
41
+ )
42
+
43
+ def _generate_description_with_skills(self) -> str:
44
+ """Generate tool description with available skills embedded."""
45
+ skills = discover_skills(self.skills_directory)
46
+ return generate_skills_tool_description(skills)
47
+
48
+ def _get_declaration(self) -> types.FunctionDeclaration:
49
+ return types.FunctionDeclaration(
50
+ name=self.name,
51
+ description=self.description,
52
+ parameters=types.Schema(
53
+ type=types.Type.OBJECT,
54
+ properties={
55
+ "command": types.Schema(
56
+ type=types.Type.STRING,
57
+ description='The skill name (no arguments). E.g., "data-analysis" or "pdf-processing"',
58
+ ),
59
+ },
60
+ required=["command"],
61
+ ),
62
+ )
63
+
64
+ async def run_async(self, *, args: Dict[str, Any], tool_context: ToolContext) -> str:
65
+ """Execute skill loading by name."""
66
+ skill_name = args.get("command", "").strip()
67
+
68
+ if not skill_name:
69
+ return "Error: No skill name provided"
70
+
71
+ return self._invoke_skill(skill_name)
72
+
73
+ def _invoke_skill(self, skill_name: str) -> str:
74
+ """Load and return the full content of a skill."""
75
+ # Check cache first
76
+ if skill_name in self._skill_cache:
77
+ return self._skill_cache[skill_name]
78
+
79
+ try:
80
+ content = load_skill_content(self.skills_directory, skill_name)
81
+ formatted_content = self._format_skill_content(skill_name, content)
82
+
83
+ # Cache the formatted content
84
+ self._skill_cache[skill_name] = formatted_content
85
+
86
+ return formatted_content
87
+ except (FileNotFoundError, IOError) as e:
88
+ logger.error(f"Failed to load skill {skill_name}: {e}")
89
+ return f"Error loading skill '{skill_name}': {e}"
90
+ except Exception as e:
91
+ logger.error(f"An unexpected error occurred while loading skill {skill_name}: {e}")
92
+ return f"An unexpected error occurred while loading skill '{skill_name}': {e}"
93
+
94
+ def _format_skill_content(self, skill_name: str, content: str) -> str:
95
+ """Format skill content for display to the agent."""
96
+ header = (
97
+ f'<command-message>The "{skill_name}" skill is loading</command-message>\n\n'
98
+ f"Base directory for this skill: {self.skills_directory}/{skill_name}\n\n"
99
+ )
100
+ footer = (
101
+ "\n\n---\n"
102
+ "The skill has been loaded. Follow the instructions above and use the bash tool to execute commands."
103
+ )
104
+ return header + content + footer
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from google.adk.agents import BaseAgent, LlmAgent
8
+
9
+ from ..tools import BashTool, EditFileTool, ReadFileTool, WriteFileTool
10
+ from .skill_tool import SkillsTool
11
+
12
+ logger = logging.getLogger("kagent_adk." + __name__)
13
+
14
+
15
+ def add_skills_tool_to_agent(skills_directory: str | Path, agent: BaseAgent) -> None:
16
+ """Utility function to add Skills and Bash tools to a given agent.
17
+
18
+ Args:
19
+ agent: The LlmAgent instance to which the tools will be added.
20
+ skills_directory: Path to directory containing skill folders.
21
+ """
22
+
23
+ if not isinstance(agent, LlmAgent):
24
+ return
25
+
26
+ skills_directory = Path(skills_directory)
27
+ existing_tool_names = {getattr(t, "name", None) for t in agent.tools}
28
+
29
+ # Add SkillsTool if not already present
30
+ if "skills" not in existing_tool_names:
31
+ agent.tools.append(SkillsTool(skills_directory))
32
+ logger.debug(f"Added skills invoke tool to agent: {agent.name}")
33
+
34
+ # Add BashTool if not already present
35
+ if "bash" not in existing_tool_names:
36
+ agent.tools.append(BashTool(skills_directory))
37
+ logger.debug(f"Added bash tool to agent: {agent.name}")
38
+
39
+ if "read_file" not in existing_tool_names:
40
+ agent.tools.append(ReadFileTool())
41
+ logger.debug(f"Added read file tool to agent: {agent.name}")
42
+
43
+ if "write_file" not in existing_tool_names:
44
+ agent.tools.append(WriteFileTool())
45
+ logger.debug(f"Added write file tool to agent: {agent.name}")
46
+
47
+ if "edit_file" not in existing_tool_names:
48
+ agent.tools.append(EditFileTool())
49
+ logger.debug(f"Added edit file tool to agent: {agent.name}")
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+
7
+ try:
8
+ from typing_extensions import override
9
+ except ImportError:
10
+ from typing import override
11
+
12
+ from google.adk.agents.readonly_context import ReadonlyContext
13
+ from google.adk.tools import BaseTool
14
+ from google.adk.tools.base_toolset import BaseToolset
15
+
16
+ from ..tools import BashTool, EditFileTool, ReadFileTool, WriteFileTool
17
+ from .skill_tool import SkillsTool
18
+
19
+ logger = logging.getLogger("kagent_adk." + __name__)
20
+
21
+
22
+ class SkillsToolset(BaseToolset):
23
+ """Toolset that provides Skills functionality for domain expertise execution.
24
+
25
+ This toolset provides skills access through specialized tools:
26
+ 1. SkillsTool - Discover and load skill instructions
27
+ 2. ReadFileTool - Read files with line numbers
28
+ 3. WriteFileTool - Write/create files
29
+ 4. EditFileTool - Edit files with precise replacements
30
+ 5. BashTool - Execute shell commands
31
+
32
+ Skills provide specialized domain knowledge and scripts that the agent can use
33
+ to solve complex tasks. The toolset enables discovery of available skills,
34
+ file manipulation, and command execution.
35
+
36
+ Note: For file upload/download, use the ArtifactsToolset separately.
37
+ """
38
+
39
+ def __init__(self, skills_directory: str | Path):
40
+ """Initialize the skills toolset.
41
+
42
+ Args:
43
+ skills_directory: Path to directory containing skill folders.
44
+ """
45
+ super().__init__()
46
+ self.skills_directory = Path(skills_directory)
47
+
48
+ # Create skills tools
49
+ self.skills_tool = SkillsTool(skills_directory)
50
+ self.read_file_tool = ReadFileTool()
51
+ self.write_file_tool = WriteFileTool()
52
+ self.edit_file_tool = EditFileTool()
53
+ self.bash_tool = BashTool(skills_directory)
54
+
55
+ @override
56
+ async def get_tools(self, readonly_context: Optional[ReadonlyContext] = None) -> List[BaseTool]:
57
+ """Get all skills tools.
58
+
59
+ Returns:
60
+ List containing all skills tools: skills, read, write, edit, and bash.
61
+ """
62
+ return [
63
+ self.skills_tool,
64
+ self.read_file_tool,
65
+ self.write_file_tool,
66
+ self.edit_file_tool,
67
+ self.bash_tool,
68
+ ]