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.
- airtrain/__init__.py +148 -2
- airtrain/__main__.py +4 -0
- airtrain/__pycache__/__init__.cpython-313.pyc +0 -0
- airtrain/agents/__init__.py +45 -0
- airtrain/agents/example_agent.py +348 -0
- airtrain/agents/groq_agent.py +289 -0
- airtrain/agents/memory.py +663 -0
- airtrain/agents/registry.py +465 -0
- airtrain/builder/__init__.py +3 -0
- airtrain/builder/agent_builder.py +122 -0
- airtrain/cli/__init__.py +0 -0
- airtrain/cli/builder.py +23 -0
- airtrain/cli/main.py +120 -0
- airtrain/contrib/__init__.py +29 -0
- airtrain/contrib/travel/__init__.py +35 -0
- airtrain/contrib/travel/agents.py +243 -0
- airtrain/contrib/travel/models.py +59 -0
- airtrain/core/__init__.py +7 -0
- airtrain/core/__pycache__/__init__.cpython-313.pyc +0 -0
- airtrain/core/__pycache__/schemas.cpython-313.pyc +0 -0
- airtrain/core/__pycache__/skills.cpython-313.pyc +0 -0
- airtrain/core/credentials.py +171 -0
- airtrain/core/schemas.py +237 -0
- airtrain/core/skills.py +269 -0
- airtrain/integrations/__init__.py +74 -0
- airtrain/integrations/anthropic/__init__.py +33 -0
- airtrain/integrations/anthropic/credentials.py +32 -0
- airtrain/integrations/anthropic/list_models.py +110 -0
- airtrain/integrations/anthropic/models_config.py +100 -0
- airtrain/integrations/anthropic/skills.py +155 -0
- airtrain/integrations/aws/__init__.py +6 -0
- airtrain/integrations/aws/credentials.py +36 -0
- airtrain/integrations/aws/skills.py +98 -0
- airtrain/integrations/cerebras/__init__.py +6 -0
- airtrain/integrations/cerebras/credentials.py +19 -0
- airtrain/integrations/cerebras/skills.py +127 -0
- airtrain/integrations/combined/__init__.py +21 -0
- airtrain/integrations/combined/groq_fireworks_skills.py +126 -0
- airtrain/integrations/combined/list_models_factory.py +210 -0
- airtrain/integrations/fireworks/__init__.py +21 -0
- airtrain/integrations/fireworks/completion_skills.py +147 -0
- airtrain/integrations/fireworks/conversation_manager.py +109 -0
- airtrain/integrations/fireworks/credentials.py +26 -0
- airtrain/integrations/fireworks/list_models.py +128 -0
- airtrain/integrations/fireworks/models.py +139 -0
- airtrain/integrations/fireworks/requests_skills.py +207 -0
- airtrain/integrations/fireworks/skills.py +181 -0
- airtrain/integrations/fireworks/structured_completion_skills.py +175 -0
- airtrain/integrations/fireworks/structured_requests_skills.py +291 -0
- airtrain/integrations/fireworks/structured_skills.py +102 -0
- airtrain/integrations/google/__init__.py +7 -0
- airtrain/integrations/google/credentials.py +58 -0
- airtrain/integrations/google/skills.py +122 -0
- airtrain/integrations/groq/__init__.py +23 -0
- airtrain/integrations/groq/credentials.py +24 -0
- airtrain/integrations/groq/models_config.py +162 -0
- airtrain/integrations/groq/skills.py +201 -0
- airtrain/integrations/ollama/__init__.py +6 -0
- airtrain/integrations/ollama/credentials.py +26 -0
- airtrain/integrations/ollama/skills.py +41 -0
- airtrain/integrations/openai/__init__.py +37 -0
- airtrain/integrations/openai/chinese_assistant.py +42 -0
- airtrain/integrations/openai/credentials.py +39 -0
- airtrain/integrations/openai/list_models.py +112 -0
- airtrain/integrations/openai/models_config.py +224 -0
- airtrain/integrations/openai/skills.py +342 -0
- airtrain/integrations/perplexity/__init__.py +49 -0
- airtrain/integrations/perplexity/credentials.py +43 -0
- airtrain/integrations/perplexity/list_models.py +112 -0
- airtrain/integrations/perplexity/models_config.py +128 -0
- airtrain/integrations/perplexity/skills.py +279 -0
- airtrain/integrations/sambanova/__init__.py +6 -0
- airtrain/integrations/sambanova/credentials.py +20 -0
- airtrain/integrations/sambanova/skills.py +129 -0
- airtrain/integrations/search/__init__.py +21 -0
- airtrain/integrations/search/exa/__init__.py +23 -0
- airtrain/integrations/search/exa/credentials.py +30 -0
- airtrain/integrations/search/exa/schemas.py +114 -0
- airtrain/integrations/search/exa/skills.py +115 -0
- airtrain/integrations/together/__init__.py +33 -0
- airtrain/integrations/together/audio_models_config.py +34 -0
- airtrain/integrations/together/credentials.py +22 -0
- airtrain/integrations/together/embedding_models_config.py +92 -0
- airtrain/integrations/together/image_models_config.py +69 -0
- airtrain/integrations/together/image_skill.py +143 -0
- airtrain/integrations/together/list_models.py +76 -0
- airtrain/integrations/together/models.py +95 -0
- airtrain/integrations/together/models_config.py +399 -0
- airtrain/integrations/together/rerank_models_config.py +43 -0
- airtrain/integrations/together/rerank_skill.py +49 -0
- airtrain/integrations/together/schemas.py +33 -0
- airtrain/integrations/together/skills.py +305 -0
- airtrain/integrations/together/vision_models_config.py +49 -0
- airtrain/telemetry/__init__.py +38 -0
- airtrain/telemetry/service.py +167 -0
- airtrain/telemetry/views.py +237 -0
- airtrain/tools/__init__.py +45 -0
- airtrain/tools/command.py +398 -0
- airtrain/tools/filesystem.py +166 -0
- airtrain/tools/network.py +111 -0
- airtrain/tools/registry.py +320 -0
- airtrain/tools/search.py +450 -0
- airtrain/tools/testing.py +135 -0
- airtrain-0.1.4.dist-info/METADATA +222 -0
- airtrain-0.1.4.dist-info/RECORD +108 -0
- {airtrain-0.1.2.dist-info → airtrain-0.1.4.dist-info}/WHEEL +1 -1
- airtrain-0.1.4.dist-info/entry_points.txt +2 -0
- airtrain-0.1.2.dist-info/METADATA +0 -106
- airtrain-0.1.2.dist-info/RECORD +0 -5
- {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
|
+
}
|