airtrain 0.1.2__py3-none-any.whl → 0.1.4__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.
Files changed (110) hide show
  1. airtrain/__init__.py +148 -2
  2. airtrain/__main__.py +4 -0
  3. airtrain/__pycache__/__init__.cpython-313.pyc +0 -0
  4. airtrain/agents/__init__.py +45 -0
  5. airtrain/agents/example_agent.py +348 -0
  6. airtrain/agents/groq_agent.py +289 -0
  7. airtrain/agents/memory.py +663 -0
  8. airtrain/agents/registry.py +465 -0
  9. airtrain/builder/__init__.py +3 -0
  10. airtrain/builder/agent_builder.py +122 -0
  11. airtrain/cli/__init__.py +0 -0
  12. airtrain/cli/builder.py +23 -0
  13. airtrain/cli/main.py +120 -0
  14. airtrain/contrib/__init__.py +29 -0
  15. airtrain/contrib/travel/__init__.py +35 -0
  16. airtrain/contrib/travel/agents.py +243 -0
  17. airtrain/contrib/travel/models.py +59 -0
  18. airtrain/core/__init__.py +7 -0
  19. airtrain/core/__pycache__/__init__.cpython-313.pyc +0 -0
  20. airtrain/core/__pycache__/schemas.cpython-313.pyc +0 -0
  21. airtrain/core/__pycache__/skills.cpython-313.pyc +0 -0
  22. airtrain/core/credentials.py +171 -0
  23. airtrain/core/schemas.py +237 -0
  24. airtrain/core/skills.py +269 -0
  25. airtrain/integrations/__init__.py +74 -0
  26. airtrain/integrations/anthropic/__init__.py +33 -0
  27. airtrain/integrations/anthropic/credentials.py +32 -0
  28. airtrain/integrations/anthropic/list_models.py +110 -0
  29. airtrain/integrations/anthropic/models_config.py +100 -0
  30. airtrain/integrations/anthropic/skills.py +155 -0
  31. airtrain/integrations/aws/__init__.py +6 -0
  32. airtrain/integrations/aws/credentials.py +36 -0
  33. airtrain/integrations/aws/skills.py +98 -0
  34. airtrain/integrations/cerebras/__init__.py +6 -0
  35. airtrain/integrations/cerebras/credentials.py +19 -0
  36. airtrain/integrations/cerebras/skills.py +127 -0
  37. airtrain/integrations/combined/__init__.py +21 -0
  38. airtrain/integrations/combined/groq_fireworks_skills.py +126 -0
  39. airtrain/integrations/combined/list_models_factory.py +210 -0
  40. airtrain/integrations/fireworks/__init__.py +21 -0
  41. airtrain/integrations/fireworks/completion_skills.py +147 -0
  42. airtrain/integrations/fireworks/conversation_manager.py +109 -0
  43. airtrain/integrations/fireworks/credentials.py +26 -0
  44. airtrain/integrations/fireworks/list_models.py +128 -0
  45. airtrain/integrations/fireworks/models.py +139 -0
  46. airtrain/integrations/fireworks/requests_skills.py +207 -0
  47. airtrain/integrations/fireworks/skills.py +181 -0
  48. airtrain/integrations/fireworks/structured_completion_skills.py +175 -0
  49. airtrain/integrations/fireworks/structured_requests_skills.py +291 -0
  50. airtrain/integrations/fireworks/structured_skills.py +102 -0
  51. airtrain/integrations/google/__init__.py +7 -0
  52. airtrain/integrations/google/credentials.py +58 -0
  53. airtrain/integrations/google/skills.py +122 -0
  54. airtrain/integrations/groq/__init__.py +23 -0
  55. airtrain/integrations/groq/credentials.py +24 -0
  56. airtrain/integrations/groq/models_config.py +162 -0
  57. airtrain/integrations/groq/skills.py +201 -0
  58. airtrain/integrations/ollama/__init__.py +6 -0
  59. airtrain/integrations/ollama/credentials.py +26 -0
  60. airtrain/integrations/ollama/skills.py +41 -0
  61. airtrain/integrations/openai/__init__.py +37 -0
  62. airtrain/integrations/openai/chinese_assistant.py +42 -0
  63. airtrain/integrations/openai/credentials.py +39 -0
  64. airtrain/integrations/openai/list_models.py +112 -0
  65. airtrain/integrations/openai/models_config.py +224 -0
  66. airtrain/integrations/openai/skills.py +342 -0
  67. airtrain/integrations/perplexity/__init__.py +49 -0
  68. airtrain/integrations/perplexity/credentials.py +43 -0
  69. airtrain/integrations/perplexity/list_models.py +112 -0
  70. airtrain/integrations/perplexity/models_config.py +128 -0
  71. airtrain/integrations/perplexity/skills.py +279 -0
  72. airtrain/integrations/sambanova/__init__.py +6 -0
  73. airtrain/integrations/sambanova/credentials.py +20 -0
  74. airtrain/integrations/sambanova/skills.py +129 -0
  75. airtrain/integrations/search/__init__.py +21 -0
  76. airtrain/integrations/search/exa/__init__.py +23 -0
  77. airtrain/integrations/search/exa/credentials.py +30 -0
  78. airtrain/integrations/search/exa/schemas.py +114 -0
  79. airtrain/integrations/search/exa/skills.py +115 -0
  80. airtrain/integrations/together/__init__.py +33 -0
  81. airtrain/integrations/together/audio_models_config.py +34 -0
  82. airtrain/integrations/together/credentials.py +22 -0
  83. airtrain/integrations/together/embedding_models_config.py +92 -0
  84. airtrain/integrations/together/image_models_config.py +69 -0
  85. airtrain/integrations/together/image_skill.py +143 -0
  86. airtrain/integrations/together/list_models.py +76 -0
  87. airtrain/integrations/together/models.py +95 -0
  88. airtrain/integrations/together/models_config.py +399 -0
  89. airtrain/integrations/together/rerank_models_config.py +43 -0
  90. airtrain/integrations/together/rerank_skill.py +49 -0
  91. airtrain/integrations/together/schemas.py +33 -0
  92. airtrain/integrations/together/skills.py +305 -0
  93. airtrain/integrations/together/vision_models_config.py +49 -0
  94. airtrain/telemetry/__init__.py +38 -0
  95. airtrain/telemetry/service.py +167 -0
  96. airtrain/telemetry/views.py +237 -0
  97. airtrain/tools/__init__.py +45 -0
  98. airtrain/tools/command.py +398 -0
  99. airtrain/tools/filesystem.py +166 -0
  100. airtrain/tools/network.py +111 -0
  101. airtrain/tools/registry.py +320 -0
  102. airtrain/tools/search.py +450 -0
  103. airtrain/tools/testing.py +135 -0
  104. airtrain-0.1.4.dist-info/METADATA +222 -0
  105. airtrain-0.1.4.dist-info/RECORD +108 -0
  106. {airtrain-0.1.2.dist-info → airtrain-0.1.4.dist-info}/WHEEL +1 -1
  107. airtrain-0.1.4.dist-info/entry_points.txt +2 -0
  108. airtrain-0.1.2.dist-info/METADATA +0 -106
  109. airtrain-0.1.2.dist-info/RECORD +0 -5
  110. {airtrain-0.1.2.dist-info → airtrain-0.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,237 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import asdict, dataclass
3
+ from typing import Any, Dict, List, Optional, Sequence
4
+ import datetime
5
+ import socket
6
+ import os
7
+
8
+
9
+ @dataclass
10
+ class BaseTelemetryEvent(ABC):
11
+ @property
12
+ @abstractmethod
13
+ def name(self) -> str:
14
+ pass
15
+
16
+ @property
17
+ def properties(self) -> Dict[str, Any]:
18
+ data = asdict(self)
19
+ # Remove name from properties if it exists
20
+ if 'name' in data:
21
+ del data['name']
22
+ # Add the common properties
23
+ data.update({
24
+ 'timestamp': datetime.datetime.now().isoformat(),
25
+ 'ip_address': socket.gethostbyname(socket.gethostname()),
26
+ 'working_directory': os.getcwd(),
27
+ })
28
+ return data
29
+
30
+
31
+ @dataclass
32
+ class AgentRunTelemetryEvent(BaseTelemetryEvent):
33
+ agent_id: str
34
+ task: str
35
+ model_name: str
36
+ model_provider: str
37
+ version: str
38
+ source: str
39
+ environment_variables: Optional[Dict[str, str]] = None
40
+ api_key_hash: Optional[str] = None # Store hash of API key for debugging/tracking
41
+ user_prompt: Optional[str] = None # Store actual prompt text
42
+ name: str = 'agent_run'
43
+
44
+ def __post_init__(self):
45
+ if self.environment_variables is None:
46
+ # Collect relevant environment variables that might affect behavior
47
+ self.environment_variables = {
48
+ k: v for k, v in os.environ.items()
49
+ if any(prefix in k.lower() for prefix in [
50
+ 'python', 'openai', 'anthropic', 'groq', 'airtrain',
51
+ 'api_key', 'path', 'home', 'user'
52
+ ])
53
+ }
54
+
55
+ # If there's an API key for the provider, store a hash for support/debugging
56
+ provider_key_map = {
57
+ 'openai': 'OPENAI_API_KEY',
58
+ 'anthropic': 'ANTHROPIC_API_KEY',
59
+ 'groq': 'GROQ_API_KEY',
60
+ 'together': 'TOGETHER_API_KEY',
61
+ 'fireworks': 'FIREWORKS_API_KEY'
62
+ }
63
+
64
+ key_var = provider_key_map.get(self.model_provider.lower())
65
+ if key_var and key_var in os.environ:
66
+ import hashlib
67
+ self.api_key_hash = hashlib.sha256(os.environ[key_var].encode()).hexdigest()
68
+
69
+
70
+ @dataclass
71
+ class AgentStepTelemetryEvent(BaseTelemetryEvent):
72
+ agent_id: str
73
+ step: int
74
+ step_error: List[str]
75
+ consecutive_failures: int
76
+ actions: List[Dict[str, Any]]
77
+ action_details: Optional[str] = None # Store complete action data including inputs
78
+ thinking: Optional[str] = None # Store agent's reasoning
79
+ memory_state: Optional[Dict[str, Any]] = None # Track memory state changes
80
+ name: str = 'agent_step'
81
+
82
+
83
+ @dataclass
84
+ class AgentEndTelemetryEvent(BaseTelemetryEvent):
85
+ agent_id: str
86
+ steps: int
87
+ is_done: bool
88
+ success: Optional[bool]
89
+ total_tokens: int
90
+ prompt_tokens: int
91
+ completion_tokens: int
92
+ total_duration_seconds: float
93
+ errors: Sequence[Optional[str]]
94
+ full_conversation: Optional[List[Dict[str, Any]]] = None # Complete conversation history
95
+ cpu_usage: Optional[float] = None # CPU usage during execution
96
+ memory_usage: Optional[float] = None # Memory usage during execution
97
+ name: str = 'agent_end'
98
+
99
+ def __post_init__(self):
100
+ # Try to gather resource usage
101
+ try:
102
+ import psutil
103
+ process = psutil.Process(os.getpid())
104
+ self.cpu_usage = process.cpu_percent()
105
+ self.memory_usage = process.memory_info().rss / (1024 * 1024) # MB
106
+ except (ImportError, Exception):
107
+ pass
108
+
109
+
110
+ @dataclass
111
+ class ModelInvocationTelemetryEvent(BaseTelemetryEvent):
112
+ agent_id: str
113
+ model_name: str
114
+ model_provider: str
115
+ tokens: int
116
+ prompt_tokens: int
117
+ completion_tokens: int
118
+ duration_seconds: float
119
+ request_id: Optional[str] = None # Track vendor request ID for debugging
120
+ full_prompt: Optional[str] = None # Full text of the prompt
121
+ full_response: Optional[str] = None # Full text of the response
122
+ parameters: Optional[Dict[str, Any]] = None # Model parameters used
123
+ error: Optional[str] = None
124
+ name: str = 'model_invocation'
125
+
126
+
127
+ @dataclass
128
+ class ErrorTelemetryEvent(BaseTelemetryEvent):
129
+ error_type: str
130
+ error_message: str
131
+ component: str
132
+ agent_id: Optional[str] = None
133
+ stack_trace: Optional[str] = None # Full stack trace
134
+ context: Optional[Dict[str, Any]] = None # Extra context about the error
135
+ name: str = 'error'
136
+
137
+ def __post_init__(self):
138
+ # Try to capture the current stack trace
139
+ if self.stack_trace is None:
140
+ import traceback
141
+ self.stack_trace = ''.join(traceback.format_stack())
142
+
143
+
144
+ @dataclass
145
+ class UserFeedbackTelemetryEvent(BaseTelemetryEvent):
146
+ """New event type to capture user feedback"""
147
+ agent_id: str
148
+ rating: int # User rating (1-5)
149
+ feedback_text: Optional[str] = None # User feedback comments
150
+ interaction_id: Optional[str] = None # Specific interaction ID
151
+ name: str = 'user_feedback'
152
+
153
+
154
+ @dataclass
155
+ class SkillInitTelemetryEvent(BaseTelemetryEvent):
156
+ """Event type to capture skill initialization"""
157
+ skill_id: str
158
+ skill_class: str
159
+ name: str = 'skill_init'
160
+
161
+
162
+ @dataclass
163
+ class SkillProcessTelemetryEvent(BaseTelemetryEvent):
164
+ """Event type to capture skill process method calls"""
165
+ skill_id: str
166
+ skill_class: str
167
+ input_schema: str
168
+ output_schema: str
169
+ # Serialized input data
170
+ input_data: Optional[Dict[str, Any]] = None
171
+ duration_seconds: float = 0.0
172
+ error: Optional[str] = None
173
+ name: str = 'skill_process'
174
+
175
+
176
+ @dataclass
177
+ class PackageInstallTelemetryEvent(BaseTelemetryEvent):
178
+ """Event type to capture package installation"""
179
+ version: str
180
+ python_version: str
181
+ install_method: Optional[str] = None # pip, conda, source, etc.
182
+ platform: Optional[str] = None # Operating system
183
+ dependencies: Optional[Dict[str, str]] = None # Installed dependencies
184
+ name: str = 'package_install'
185
+
186
+ def __post_init__(self):
187
+ # Collect platform info if not provided
188
+ if self.platform is None:
189
+ import platform
190
+ self.platform = platform.platform()
191
+
192
+ # Collect dependency info if not provided
193
+ if self.dependencies is None:
194
+ # Try to get installed package versions for key dependencies
195
+ self.dependencies = {}
196
+ import importlib.metadata
197
+ try:
198
+ for package in ["openai", "anthropic", "groq", "together"]:
199
+ try:
200
+ self.dependencies[package] = importlib.metadata.version(package)
201
+ except importlib.metadata.PackageNotFoundError:
202
+ pass
203
+ except (ImportError, Exception):
204
+ pass
205
+
206
+
207
+ @dataclass
208
+ class PackageImportTelemetryEvent(BaseTelemetryEvent):
209
+ """Event type to capture package import"""
210
+ version: str
211
+ python_version: str
212
+ import_context: Optional[str] = None # Information about what imported the package
213
+ platform: Optional[str] = None # Operating system
214
+ name: str = 'package_import'
215
+
216
+ def __post_init__(self):
217
+ # Collect platform info if not provided
218
+ if self.platform is None:
219
+ import platform
220
+ self.platform = platform.platform()
221
+
222
+ # Try to get import context from traceback
223
+ if self.import_context is None:
224
+ try:
225
+ import inspect
226
+ frames = inspect.stack()
227
+ # Skip the first few frames which are inside our code
228
+ # Look for the first frame that's not in our module
229
+ import_frames = []
230
+ for frame in frames[3:10]: # Skip first 3, take up to 7 more
231
+ module = frame.frame.f_globals.get('__name__', '')
232
+ if not module.startswith('airtrain'):
233
+ import_frames.append(f"{module}:{frame.function}")
234
+ if import_frames:
235
+ self.import_context = " -> ".join(import_frames)
236
+ except Exception:
237
+ pass
@@ -0,0 +1,45 @@
1
+ """
2
+ Tools package for AirTrain.
3
+
4
+ This package provides a registry of tools that can be used by agents.
5
+ """
6
+
7
+ # Import registry components
8
+ from .registry import (
9
+ BaseTool,
10
+ StatelessTool,
11
+ StatefulTool,
12
+ ToolFactory,
13
+ ToolValidationError,
14
+ register_tool,
15
+ execute_tool_call,
16
+ )
17
+
18
+ # Import standard tools
19
+ from .filesystem import ListDirectoryTool, DirectoryTreeTool
20
+ from .network import ApiCallTool
21
+ from .command import ExecuteCommandTool, FindFilesTool, TerminalNavigationTool
22
+ from .search import SearchTermTool, WebSearchTool
23
+ from .testing import RunPytestTool
24
+
25
+ __all__ = [
26
+ # Base classes
27
+ "BaseTool",
28
+ "StatelessTool",
29
+ "StatefulTool",
30
+ # Registry components
31
+ "ToolFactory",
32
+ "ToolValidationError",
33
+ "register_tool",
34
+ "execute_tool_call",
35
+ # Standard tools
36
+ "ListDirectoryTool",
37
+ "DirectoryTreeTool",
38
+ "ApiCallTool",
39
+ "ExecuteCommandTool",
40
+ "FindFilesTool",
41
+ "TerminalNavigationTool",
42
+ "SearchTermTool",
43
+ "WebSearchTool",
44
+ "RunPytestTool",
45
+ ]
@@ -0,0 +1,398 @@
1
+ """
2
+ Command execution tools for AirTrain agents.
3
+
4
+ This module provides tools for executing shell commands in a controlled environment.
5
+ """
6
+
7
+ import os
8
+ import subprocess
9
+ from typing import Dict, Any, List, Optional
10
+
11
+ from .registry import StatelessTool, StatefulTool, register_tool
12
+
13
+
14
+ @register_tool("execute_command")
15
+ class ExecuteCommandTool(StatelessTool):
16
+ """Tool for executing shell commands."""
17
+
18
+ def __init__(self):
19
+ self.name = "execute_command"
20
+ self.description = "Execute a shell command and return its output"
21
+ self.parameters = {
22
+ "type": "object",
23
+ "properties": {
24
+ "command": {"type": "string", "description": "The command to execute"},
25
+ "working_dir": {
26
+ "type": "string",
27
+ "description": "Working directory for the command",
28
+ },
29
+ "timeout": {"type": "number", "description": "Timeout in seconds"},
30
+ "env_vars": {
31
+ "type": "object",
32
+ "description": "Environment variables to set for the command",
33
+ },
34
+ },
35
+ "required": ["command"],
36
+ }
37
+
38
+ # List of disallowed commands for security
39
+ self.disallowed_commands = [
40
+ "rm -rf",
41
+ "sudo",
42
+ "su",
43
+ "chown",
44
+ "chmod",
45
+ "mkfs",
46
+ "dd",
47
+ "shred",
48
+ ">",
49
+ ">>",
50
+ "|",
51
+ "perl -e",
52
+ "python -c",
53
+ "ruby -e",
54
+ ":(){ :|:& };:",
55
+ "eval",
56
+ "exec",
57
+ "`",
58
+ ]
59
+
60
+ def __call__(
61
+ self,
62
+ command: str,
63
+ working_dir: Optional[str] = None,
64
+ timeout: Optional[float] = 30.0,
65
+ env_vars: Optional[Dict[str, str]] = None,
66
+ ) -> Dict[str, Any]:
67
+ """Execute a shell command and return its output."""
68
+ try:
69
+ # Security check
70
+ for disallowed in self.disallowed_commands:
71
+ if disallowed in command:
72
+ return {
73
+ "success": False,
74
+ "error": f"Command contains disallowed pattern: {disallowed}",
75
+ }
76
+
77
+ # Prepare environment
78
+ env = os.environ.copy()
79
+ if env_vars:
80
+ env.update(env_vars)
81
+
82
+ # Execute command
83
+ result = subprocess.run(
84
+ command,
85
+ shell=True,
86
+ capture_output=True,
87
+ text=True,
88
+ cwd=working_dir,
89
+ timeout=timeout,
90
+ env=env,
91
+ )
92
+
93
+ return {
94
+ "success": result.returncode == 0,
95
+ "return_code": result.returncode,
96
+ "stdout": result.stdout,
97
+ "stderr": result.stderr,
98
+ }
99
+ except subprocess.TimeoutExpired:
100
+ return {
101
+ "success": False,
102
+ "error": f"Command timed out after {timeout} seconds",
103
+ }
104
+ except Exception as e:
105
+ return {"success": False, "error": f"Error executing command: {str(e)}"}
106
+
107
+ def to_dict(self):
108
+ """Convert tool to dictionary format for LLM function calling."""
109
+ return {
110
+ "type": "function",
111
+ "function": {
112
+ "name": self.name,
113
+ "description": self.description,
114
+ "parameters": self.parameters,
115
+ },
116
+ }
117
+
118
+
119
+ @register_tool("find_files")
120
+ class FindFilesTool(StatelessTool):
121
+ """Tool for finding files matching patterns."""
122
+
123
+ def __init__(self):
124
+ self.name = "find_files"
125
+ self.description = "Find files matching the specified pattern"
126
+ self.parameters = {
127
+ "type": "object",
128
+ "properties": {
129
+ "directory": {
130
+ "type": "string",
131
+ "description": "Directory to search in",
132
+ },
133
+ "pattern": {
134
+ "type": "string",
135
+ "description": "Glob pattern to match (e.g., *.txt, **/*.py)",
136
+ },
137
+ "max_results": {
138
+ "type": "integer",
139
+ "description": "Maximum number of results to return",
140
+ },
141
+ "show_hidden": {
142
+ "type": "boolean",
143
+ "description": "Whether to include hidden files (starting with .)",
144
+ },
145
+ },
146
+ "required": ["directory", "pattern"],
147
+ }
148
+
149
+ def __call__(
150
+ self,
151
+ directory: str,
152
+ pattern: str,
153
+ max_results: int = 100,
154
+ show_hidden: bool = False,
155
+ ) -> Dict[str, Any]:
156
+ """Find files matching the specified pattern."""
157
+ try:
158
+ import glob
159
+ from pathlib import Path
160
+
161
+ directory = os.path.expanduser(directory)
162
+ if not os.path.exists(directory):
163
+ return {
164
+ "success": False,
165
+ "error": f"Directory '{directory}' does not exist",
166
+ }
167
+
168
+ if not os.path.isdir(directory):
169
+ return {
170
+ "success": False,
171
+ "error": f"Path '{directory}' is not a directory",
172
+ }
173
+
174
+ # Construct search path
175
+ search_path = os.path.join(directory, pattern)
176
+
177
+ # Find matching files
178
+ files = []
179
+ for file_path in glob.glob(search_path, recursive=True):
180
+ if not show_hidden and os.path.basename(file_path).startswith("."):
181
+ continue
182
+
183
+ file_info = {
184
+ "path": file_path,
185
+ "name": os.path.basename(file_path),
186
+ "type": "dir" if os.path.isdir(file_path) else "file",
187
+ "size": (
188
+ os.path.getsize(file_path)
189
+ if os.path.isfile(file_path)
190
+ else None
191
+ ),
192
+ }
193
+ files.append(file_info)
194
+
195
+ if len(files) >= max_results:
196
+ break
197
+
198
+ return {
199
+ "success": True,
200
+ "directory": directory,
201
+ "pattern": pattern,
202
+ "files": files,
203
+ "count": len(files),
204
+ "truncated": len(files) >= max_results,
205
+ }
206
+ except Exception as e:
207
+ return {"success": False, "error": f"Error finding files: {str(e)}"}
208
+
209
+ def to_dict(self):
210
+ """Convert tool to dictionary format for LLM function calling."""
211
+ return {
212
+ "type": "function",
213
+ "function": {
214
+ "name": self.name,
215
+ "description": self.description,
216
+ "parameters": self.parameters,
217
+ },
218
+ }
219
+
220
+
221
+ @register_tool("terminal_navigation")
222
+ class TerminalNavigationTool(StatefulTool):
223
+ """Tool for navigating through the terminal with state memory."""
224
+
225
+ def __init__(self):
226
+ self.name = "terminal_navigation"
227
+ self.description = (
228
+ "Navigate through the terminal with persistent directory state"
229
+ )
230
+ self.parameters = {
231
+ "type": "object",
232
+ "properties": {
233
+ "action": {
234
+ "type": "string",
235
+ "enum": ["cd", "pwd", "pushd", "popd", "dirs"],
236
+ "description": "Navigation action to perform",
237
+ },
238
+ "directory": {
239
+ "type": "string",
240
+ "description": "Target directory for cd and pushd actions",
241
+ },
242
+ },
243
+ "required": ["action"],
244
+ }
245
+ self.reset()
246
+
247
+ @classmethod
248
+ def create_instance(cls):
249
+ """Create a new instance with fresh state."""
250
+ return cls()
251
+
252
+ def reset(self):
253
+ """Reset the terminal navigation state."""
254
+ self.current_dir = os.getcwd()
255
+ self.dir_stack = []
256
+
257
+ def __call__(self, action: str, directory: Optional[str] = None) -> Dict[str, Any]:
258
+ """Execute the terminal navigation action."""
259
+ try:
260
+ # Handle the different navigation actions
261
+ if action == "cd" and directory:
262
+ # Expand user path if present
263
+ target_dir = os.path.expanduser(directory)
264
+
265
+ # Handle relative paths
266
+ if not os.path.isabs(target_dir):
267
+ target_dir = os.path.join(self.current_dir, target_dir)
268
+
269
+ # Normalize the path
270
+ target_dir = os.path.normpath(target_dir)
271
+
272
+ # Check if directory exists
273
+ if not os.path.exists(target_dir):
274
+ return {
275
+ "success": False,
276
+ "error": f"Directory does not exist: {target_dir}",
277
+ "current_dir": self.current_dir,
278
+ }
279
+
280
+ if not os.path.isdir(target_dir):
281
+ return {
282
+ "success": False,
283
+ "error": f"Path is not a directory: {target_dir}",
284
+ "current_dir": self.current_dir,
285
+ }
286
+
287
+ # Change directory
288
+ self.current_dir = target_dir
289
+ return {
290
+ "success": True,
291
+ "action": "cd",
292
+ "previous_dir": self.current_dir,
293
+ "current_dir": self.current_dir,
294
+ }
295
+
296
+ elif action == "pwd":
297
+ # Print working directory
298
+ return {
299
+ "success": True,
300
+ "action": "pwd",
301
+ "current_dir": self.current_dir,
302
+ }
303
+
304
+ elif action == "pushd" and directory:
305
+ # Expand user path if present
306
+ target_dir = os.path.expanduser(directory)
307
+
308
+ # Handle relative paths
309
+ if not os.path.isabs(target_dir):
310
+ target_dir = os.path.join(self.current_dir, target_dir)
311
+
312
+ # Normalize the path
313
+ target_dir = os.path.normpath(target_dir)
314
+
315
+ # Check if directory exists
316
+ if not os.path.exists(target_dir):
317
+ return {
318
+ "success": False,
319
+ "error": f"Directory does not exist: {target_dir}",
320
+ "current_dir": self.current_dir,
321
+ "dir_stack": self.dir_stack,
322
+ }
323
+
324
+ if not os.path.isdir(target_dir):
325
+ return {
326
+ "success": False,
327
+ "error": f"Path is not a directory: {target_dir}",
328
+ "current_dir": self.current_dir,
329
+ "dir_stack": self.dir_stack,
330
+ }
331
+
332
+ # Push current directory onto stack and change to new directory
333
+ self.dir_stack.append(self.current_dir)
334
+ self.current_dir = target_dir
335
+
336
+ return {
337
+ "success": True,
338
+ "action": "pushd",
339
+ "previous_dir": self.dir_stack[-1],
340
+ "current_dir": self.current_dir,
341
+ "dir_stack": self.dir_stack,
342
+ }
343
+
344
+ elif action == "popd":
345
+ # Check if directory stack is empty
346
+ if not self.dir_stack:
347
+ return {
348
+ "success": False,
349
+ "error": "Directory stack is empty",
350
+ "current_dir": self.current_dir,
351
+ "dir_stack": [],
352
+ }
353
+
354
+ # Pop directory from stack and change to it
355
+ previous_dir = self.current_dir
356
+ self.current_dir = self.dir_stack.pop()
357
+
358
+ return {
359
+ "success": True,
360
+ "action": "popd",
361
+ "previous_dir": previous_dir,
362
+ "current_dir": self.current_dir,
363
+ "dir_stack": self.dir_stack,
364
+ }
365
+
366
+ elif action == "dirs":
367
+ # Display directory stack
368
+ return {
369
+ "success": True,
370
+ "action": "dirs",
371
+ "current_dir": self.current_dir,
372
+ "dir_stack": self.dir_stack,
373
+ }
374
+
375
+ else:
376
+ return {
377
+ "success": False,
378
+ "error": f"Invalid action '{action}' or missing required parameters",
379
+ "current_dir": self.current_dir,
380
+ }
381
+
382
+ except Exception as e:
383
+ return {
384
+ "success": False,
385
+ "error": f"Error navigating terminal: {str(e)}",
386
+ "current_dir": self.current_dir,
387
+ }
388
+
389
+ def to_dict(self):
390
+ """Convert tool to dictionary format for LLM function calling."""
391
+ return {
392
+ "type": "function",
393
+ "function": {
394
+ "name": self.name,
395
+ "description": self.description,
396
+ "parameters": self.parameters,
397
+ },
398
+ }