Update Modules/Agent_Terminal.py
Browse files- Modules/Agent_Terminal.py +172 -56
Modules/Agent_Terminal.py
CHANGED
|
@@ -4,8 +4,10 @@ import os
|
|
| 4 |
import sys
|
| 5 |
import types
|
| 6 |
import ast
|
|
|
|
|
|
|
| 7 |
from io import StringIO
|
| 8 |
-
from typing import Annotated
|
| 9 |
import importlib.metadata
|
| 10 |
|
| 11 |
import gradio as gr
|
|
@@ -24,6 +26,145 @@ from .Code_Interpreter import Code_Interpreter
|
|
| 24 |
|
| 25 |
from app import _log_call_end, _log_call_start, _truncate_for_log
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
def search_packages(query: str = "") -> str:
|
| 28 |
"""Search for installed Python packages by name. If query is empty, lists all."""
|
| 29 |
packages = []
|
|
@@ -38,7 +179,8 @@ def search_packages(query: str = "") -> str:
|
|
| 38 |
return "\n".join(packages)
|
| 39 |
|
| 40 |
def _get_tools_map():
|
| 41 |
-
return
|
|
|
|
| 42 |
"Web_Fetch": Web_Fetch,
|
| 43 |
"Web_Search": Web_Search,
|
| 44 |
"Memory_Manager": Memory_Manager,
|
|
@@ -53,10 +195,7 @@ def _get_tools_map():
|
|
| 53 |
"Shell_Command": Shell_Command,
|
| 54 |
"Code_Interpreter": Code_Interpreter,
|
| 55 |
}
|
| 56 |
-
|
| 57 |
-
def list_tools() -> list[str]:
|
| 58 |
-
"""List all available tools in the Code Interpreter environment."""
|
| 59 |
-
return list(_get_tools_map().keys())
|
| 60 |
|
| 61 |
def search_tools(query: str) -> str:
|
| 62 |
"""Search for tools by name or description. Returns usage info for matches."""
|
|
@@ -64,7 +203,9 @@ def search_tools(query: str) -> str:
|
|
| 64 |
matches = []
|
| 65 |
tools = _get_tools_map()
|
| 66 |
for name, func in tools.items():
|
| 67 |
-
|
|
|
|
|
|
|
| 68 |
if query in name.lower() or query in doc:
|
| 69 |
matches.append((name, func))
|
| 70 |
|
|
@@ -73,35 +214,25 @@ def search_tools(query: str) -> str:
|
|
| 73 |
|
| 74 |
output = []
|
| 75 |
for name, func in matches:
|
| 76 |
-
output.append(
|
| 77 |
-
output.append(func.__doc__ or "No documentation available.")
|
| 78 |
output.append("")
|
| 79 |
return "\n".join(output)
|
| 80 |
|
| 81 |
-
def usage(tool_name: str) -> str:
|
| 82 |
-
"""Get detailed usage information for a specific tool."""
|
| 83 |
-
tools = _get_tools_map()
|
| 84 |
-
if tool_name not in tools:
|
| 85 |
-
return f"Tool '{tool_name}' not found. Available tools: {', '.join(tools.keys())}"
|
| 86 |
-
func = tools[tool_name]
|
| 87 |
-
return f"--- {tool_name} ---\n{func.__doc__ or 'No documentation available.'}"
|
| 88 |
-
|
| 89 |
def _initialize_mock_modules():
|
| 90 |
"""
|
| 91 |
Registers a mock 'functions' module in sys.modules so that LLMs
|
| 92 |
can do 'from functions import ...' without error.
|
|
|
|
| 93 |
"""
|
| 94 |
mock_module = types.ModuleType("functions")
|
| 95 |
|
| 96 |
-
# Add tools
|
| 97 |
for name, tool in _get_tools_map().items():
|
| 98 |
setattr(mock_module, name, tool)
|
| 99 |
|
| 100 |
# Add helpers
|
| 101 |
helpers = {
|
| 102 |
-
"list_tools": list_tools,
|
| 103 |
"search_tools": search_tools,
|
| 104 |
-
"usage": usage,
|
| 105 |
"search_packages": search_packages,
|
| 106 |
}
|
| 107 |
for name, func in helpers.items():
|
|
@@ -114,8 +245,7 @@ _initialize_mock_modules()
|
|
| 114 |
# Single source of truth for the LLM-facing tool description
|
| 115 |
TOOL_SUMMARY = (
|
| 116 |
"Executes Python code as the unified interface for the entire tools ecosystem. "
|
| 117 |
-
"
|
| 118 |
-
"Use Agent Terminal repeatedly whenever you need to chain or combine tool operations. "
|
| 119 |
"Available tools: `Web_Fetch`, `Web_Search`, `Code_Interpreter`, `Shell_Command`, `File_System`, `Obsidian_Vault`, `Memory_Manager`, `Generate_Speech`, `Generate_Image`, `Generate_Video`, `Deep_Research`."
|
| 120 |
)
|
| 121 |
|
|
@@ -126,10 +256,9 @@ TOOL_SUMMARY = (
|
|
| 126 |
)
|
| 127 |
def Agent_Terminal(input: Annotated[str, (
|
| 128 |
"Python source code to run; stdout is captured and returned. "
|
| 129 |
-
"
|
| 130 |
-
"
|
| 131 |
-
"`
|
| 132 |
-
"`usage('ToolName')` to inspect a tool’s expected input parameters; "
|
| 133 |
"`search_packages('query')` to search for installed Python libraries."
|
| 134 |
)]) -> str:
|
| 135 |
_log_call_start("Agent_Terminal", input=_truncate_for_log(input or "", 300))
|
|
@@ -141,24 +270,13 @@ def Agent_Terminal(input: Annotated[str, (
|
|
| 141 |
old_cwd = os.getcwd()
|
| 142 |
redirected_output = sys.stdout = StringIO()
|
| 143 |
|
|
|
|
|
|
|
|
|
|
| 144 |
# Prepare the execution environment with all tools
|
| 145 |
tools_env = {
|
| 146 |
-
|
| 147 |
-
"Web_Search": Web_Search,
|
| 148 |
-
"Memory_Manager": Memory_Manager,
|
| 149 |
-
"Generate_Speech": Generate_Speech,
|
| 150 |
-
"List_Kokoro_Voices": List_Kokoro_Voices,
|
| 151 |
-
"List_Supertonic_Voices": List_Supertonic_Voices,
|
| 152 |
-
"Generate_Image": Generate_Image,
|
| 153 |
-
"Generate_Video": Generate_Video,
|
| 154 |
-
"Deep_Research": Deep_Research,
|
| 155 |
-
"File_System": File_System,
|
| 156 |
-
"Obsidian_Vault": Obsidian_Vault,
|
| 157 |
-
"Shell_Command": Shell_Command,
|
| 158 |
-
"Code_Interpreter": Code_Interpreter,
|
| 159 |
-
"list_tools": list_tools,
|
| 160 |
"search_tools": search_tools,
|
| 161 |
-
"usage": usage,
|
| 162 |
"search_packages": search_packages,
|
| 163 |
"print": print, # Ensure print is available
|
| 164 |
"__builtins__": __builtins__,
|
|
@@ -167,22 +285,20 @@ def Agent_Terminal(input: Annotated[str, (
|
|
| 167 |
try:
|
| 168 |
os.chdir(ROOT_DIR)
|
| 169 |
|
| 170 |
-
# Parse code
|
| 171 |
tree = ast.parse(input)
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
else:
|
| 185 |
-
exec(input, tools_env)
|
| 186 |
|
| 187 |
result = redirected_output.getvalue()
|
| 188 |
except Exception as exc: # pylint: disable=broad-except
|
|
|
|
| 4 |
import sys
|
| 5 |
import types
|
| 6 |
import ast
|
| 7 |
+
import inspect
|
| 8 |
+
import functools
|
| 9 |
from io import StringIO
|
| 10 |
+
from typing import Annotated, get_type_hints, get_origin, get_args
|
| 11 |
import importlib.metadata
|
| 12 |
|
| 13 |
import gradio as gr
|
|
|
|
| 26 |
|
| 27 |
from app import _log_call_end, _log_call_start, _truncate_for_log
|
| 28 |
|
| 29 |
+
|
| 30 |
+
# Example usages for each tool - simple and advanced
|
| 31 |
+
_TOOL_EXAMPLES = {
|
| 32 |
+
"Web_Fetch": (
|
| 33 |
+
'Web_Fetch(url="https://example.com")',
|
| 34 |
+
'Web_Fetch(url="https://example.com", max_chars=5000, mode="url_scraper")',
|
| 35 |
+
),
|
| 36 |
+
"Web_Search": (
|
| 37 |
+
'Web_Search(query="Python tutorials")',
|
| 38 |
+
'Web_Search(query="AI news", max_results=10, search_type="news", date_filter="week")',
|
| 39 |
+
),
|
| 40 |
+
"Code_Interpreter": (
|
| 41 |
+
'Code_Interpreter(code="print(2 + 2)")',
|
| 42 |
+
'Code_Interpreter(code="import math; print(math.pi)", timeout=60)',
|
| 43 |
+
),
|
| 44 |
+
"Shell_Command": (
|
| 45 |
+
'Shell_Command(command="echo Hello")',
|
| 46 |
+
'Shell_Command(command="ls -la", timeout=30)',
|
| 47 |
+
),
|
| 48 |
+
"File_System": (
|
| 49 |
+
'File_System(action="list", path="/")',
|
| 50 |
+
'File_System(action="read", path="/notes.txt", max_chars=5000)',
|
| 51 |
+
),
|
| 52 |
+
"Obsidian_Vault": (
|
| 53 |
+
'Obsidian_Vault(action="list", path="/")',
|
| 54 |
+
'Obsidian_Vault(action="search", query="meeting notes", recursive=True)',
|
| 55 |
+
),
|
| 56 |
+
"Memory_Manager": (
|
| 57 |
+
'Memory_Manager(action="list")',
|
| 58 |
+
'Memory_Manager(action="save", text="Remember this fact", tags="important, facts")',
|
| 59 |
+
),
|
| 60 |
+
"Generate_Speech": (
|
| 61 |
+
'Generate_Speech(text="Hello, world!")',
|
| 62 |
+
'Generate_Speech(text="Welcome to the demo", model="Kokoro", voice="af_heart", speed=1.2)',
|
| 63 |
+
),
|
| 64 |
+
"Generate_Image": (
|
| 65 |
+
'Generate_Image(prompt="A sunset over mountains")',
|
| 66 |
+
'Generate_Image(prompt="A cyberpunk city", steps=50, cfg_scale=9.0, width=1024, height=768)',
|
| 67 |
+
),
|
| 68 |
+
"Generate_Video": (
|
| 69 |
+
'Generate_Video(prompt="A cat playing piano")',
|
| 70 |
+
'Generate_Video(prompt="Ocean waves", duration=5, aspect_ratio="16:9")',
|
| 71 |
+
),
|
| 72 |
+
"Deep_Research": (
|
| 73 |
+
'Deep_Research(query="Climate change effects")',
|
| 74 |
+
'Deep_Research(query="Quantum computing advances", max_sources=10, search_type="news")',
|
| 75 |
+
),
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _format_tool_usage(func) -> str:
|
| 80 |
+
"""Generate detailed usage information for a tool function."""
|
| 81 |
+
name = func.__name__
|
| 82 |
+
doc = func.__doc__ or "No description available."
|
| 83 |
+
|
| 84 |
+
# Extract just the summary (first paragraph) - skip Args/Returns sections
|
| 85 |
+
# since we generate our own detailed parameter list
|
| 86 |
+
doc_lines = doc.strip().split('\n')
|
| 87 |
+
summary_lines = []
|
| 88 |
+
for line in doc_lines:
|
| 89 |
+
stripped = line.strip().lower()
|
| 90 |
+
# Stop at Args:, Returns:, Parameters:, etc.
|
| 91 |
+
if stripped.startswith(('args:', 'returns:', 'parameters:', 'raises:', 'example:', 'note:', 'notes:')):
|
| 92 |
+
break
|
| 93 |
+
summary_lines.append(line)
|
| 94 |
+
summary = '\n'.join(summary_lines).strip()
|
| 95 |
+
|
| 96 |
+
# Get the signature
|
| 97 |
+
sig = inspect.signature(func)
|
| 98 |
+
|
| 99 |
+
# Try to get type hints
|
| 100 |
+
try:
|
| 101 |
+
hints = get_type_hints(func, include_extras=True)
|
| 102 |
+
except Exception:
|
| 103 |
+
hints = {}
|
| 104 |
+
|
| 105 |
+
lines = [f"=== {name} ===", "", summary, "", "Parameters:"]
|
| 106 |
+
|
| 107 |
+
for param_name, param in sig.parameters.items():
|
| 108 |
+
if param_name in ("self", "cls"):
|
| 109 |
+
continue
|
| 110 |
+
|
| 111 |
+
# Get type and description from Annotated if available
|
| 112 |
+
hint = hints.get(param_name)
|
| 113 |
+
type_str = "any"
|
| 114 |
+
desc = ""
|
| 115 |
+
|
| 116 |
+
if hint is not None:
|
| 117 |
+
if get_origin(hint) is Annotated:
|
| 118 |
+
args = get_args(hint)
|
| 119 |
+
if args:
|
| 120 |
+
type_str = getattr(args[0], "__name__", str(args[0]))
|
| 121 |
+
if len(args) > 1 and isinstance(args[1], str):
|
| 122 |
+
desc = args[1]
|
| 123 |
+
else:
|
| 124 |
+
type_str = getattr(hint, "__name__", str(hint))
|
| 125 |
+
|
| 126 |
+
# Check for default
|
| 127 |
+
if param.default is not inspect.Parameter.empty:
|
| 128 |
+
default_repr = repr(param.default)
|
| 129 |
+
if len(default_repr) > 50:
|
| 130 |
+
default_repr = default_repr[:47] + "..."
|
| 131 |
+
default_str = f" = {default_repr}"
|
| 132 |
+
else:
|
| 133 |
+
default_str = " (required)"
|
| 134 |
+
|
| 135 |
+
lines.append(f" - {param_name}: {type_str}{default_str}")
|
| 136 |
+
if desc:
|
| 137 |
+
lines.append(f" {desc}")
|
| 138 |
+
|
| 139 |
+
# Add examples
|
| 140 |
+
lines.append("")
|
| 141 |
+
lines.append("Examples:")
|
| 142 |
+
if name in _TOOL_EXAMPLES:
|
| 143 |
+
simple, advanced = _TOOL_EXAMPLES[name]
|
| 144 |
+
lines.append(f" {simple}")
|
| 145 |
+
lines.append(f" {advanced}")
|
| 146 |
+
else:
|
| 147 |
+
lines.append(f" {name}(...)")
|
| 148 |
+
|
| 149 |
+
return "\n".join(lines)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _wrap_tool_for_no_arg_usage(func):
|
| 153 |
+
"""
|
| 154 |
+
Wrap a tool function so that calling it with no arguments
|
| 155 |
+
returns usage information instead of raising an error.
|
| 156 |
+
"""
|
| 157 |
+
@functools.wraps(func)
|
| 158 |
+
def wrapper(*args, **kwargs):
|
| 159 |
+
# If called with no arguments, return usage info
|
| 160 |
+
if not args and not kwargs:
|
| 161 |
+
return _format_tool_usage(func)
|
| 162 |
+
return func(*args, **kwargs)
|
| 163 |
+
|
| 164 |
+
# Preserve the original function for introspection
|
| 165 |
+
wrapper._original_func = func
|
| 166 |
+
return wrapper
|
| 167 |
+
|
| 168 |
def search_packages(query: str = "") -> str:
|
| 169 |
"""Search for installed Python packages by name. If query is empty, lists all."""
|
| 170 |
packages = []
|
|
|
|
| 179 |
return "\n".join(packages)
|
| 180 |
|
| 181 |
def _get_tools_map():
|
| 182 |
+
"""Get all tools wrapped to return usage info when called with no arguments."""
|
| 183 |
+
raw_tools = {
|
| 184 |
"Web_Fetch": Web_Fetch,
|
| 185 |
"Web_Search": Web_Search,
|
| 186 |
"Memory_Manager": Memory_Manager,
|
|
|
|
| 195 |
"Shell_Command": Shell_Command,
|
| 196 |
"Code_Interpreter": Code_Interpreter,
|
| 197 |
}
|
| 198 |
+
return {name: _wrap_tool_for_no_arg_usage(func) for name, func in raw_tools.items()}
|
|
|
|
|
|
|
|
|
|
| 199 |
|
| 200 |
def search_tools(query: str) -> str:
|
| 201 |
"""Search for tools by name or description. Returns usage info for matches."""
|
|
|
|
| 203 |
matches = []
|
| 204 |
tools = _get_tools_map()
|
| 205 |
for name, func in tools.items():
|
| 206 |
+
# Get original function for docstring if wrapped
|
| 207 |
+
original = getattr(func, '_original_func', func)
|
| 208 |
+
doc = (original.__doc__ or "").lower()
|
| 209 |
if query in name.lower() or query in doc:
|
| 210 |
matches.append((name, func))
|
| 211 |
|
|
|
|
| 214 |
|
| 215 |
output = []
|
| 216 |
for name, func in matches:
|
| 217 |
+
output.append(_format_tool_usage(getattr(func, '_original_func', func)))
|
|
|
|
| 218 |
output.append("")
|
| 219 |
return "\n".join(output)
|
| 220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
def _initialize_mock_modules():
|
| 222 |
"""
|
| 223 |
Registers a mock 'functions' module in sys.modules so that LLMs
|
| 224 |
can do 'from functions import ...' without error.
|
| 225 |
+
Uses wrapped tools that return usage info when called with no args.
|
| 226 |
"""
|
| 227 |
mock_module = types.ModuleType("functions")
|
| 228 |
|
| 229 |
+
# Add wrapped tools (return usage when called with no args)
|
| 230 |
for name, tool in _get_tools_map().items():
|
| 231 |
setattr(mock_module, name, tool)
|
| 232 |
|
| 233 |
# Add helpers
|
| 234 |
helpers = {
|
|
|
|
| 235 |
"search_tools": search_tools,
|
|
|
|
| 236 |
"search_packages": search_packages,
|
| 237 |
}
|
| 238 |
for name, func in helpers.items():
|
|
|
|
| 245 |
# Single source of truth for the LLM-facing tool description
|
| 246 |
TOOL_SUMMARY = (
|
| 247 |
"Executes Python code as the unified interface for the entire tools ecosystem. "
|
| 248 |
+
"Use Agent Terminal repeatedly whenever you need to chain or combine tool operations. Always read a tool's definiton to see usage info before invoking it. "
|
|
|
|
| 249 |
"Available tools: `Web_Fetch`, `Web_Search`, `Code_Interpreter`, `Shell_Command`, `File_System`, `Obsidian_Vault`, `Memory_Manager`, `Generate_Speech`, `Generate_Image`, `Generate_Video`, `Deep_Research`."
|
| 250 |
)
|
| 251 |
|
|
|
|
| 256 |
)
|
| 257 |
def Agent_Terminal(input: Annotated[str, (
|
| 258 |
"Python source code to run; stdout is captured and returned. "
|
| 259 |
+
"Call any tool with no arguments to get its full usage info (e.g., `Generate_Image()`). "
|
| 260 |
+
"Helper commands: "
|
| 261 |
+
"`search_tools('query')` to search tools by name or capability; "
|
|
|
|
| 262 |
"`search_packages('query')` to search for installed Python libraries."
|
| 263 |
)]) -> str:
|
| 264 |
_log_call_start("Agent_Terminal", input=_truncate_for_log(input or "", 300))
|
|
|
|
| 270 |
old_cwd = os.getcwd()
|
| 271 |
redirected_output = sys.stdout = StringIO()
|
| 272 |
|
| 273 |
+
# Get wrapped tools that return usage info when called with no args
|
| 274 |
+
wrapped_tools = _get_tools_map()
|
| 275 |
+
|
| 276 |
# Prepare the execution environment with all tools
|
| 277 |
tools_env = {
|
| 278 |
+
**wrapped_tools,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
"search_tools": search_tools,
|
|
|
|
| 280 |
"search_packages": search_packages,
|
| 281 |
"print": print, # Ensure print is available
|
| 282 |
"__builtins__": __builtins__,
|
|
|
|
| 285 |
try:
|
| 286 |
os.chdir(ROOT_DIR)
|
| 287 |
|
| 288 |
+
# Parse code and print results of ALL expression statements (not just the last)
|
| 289 |
tree = ast.parse(input)
|
| 290 |
+
|
| 291 |
+
for node in tree.body:
|
| 292 |
+
if isinstance(node, ast.Expr):
|
| 293 |
+
# This is a standalone expression - evaluate and print its result
|
| 294 |
+
expr = compile(ast.Expression(node.value), filename="<string>", mode="eval")
|
| 295 |
+
result_val = eval(expr, tools_env)
|
| 296 |
+
if result_val is not None:
|
| 297 |
+
print(result_val)
|
| 298 |
+
else:
|
| 299 |
+
# This is a statement (assignment, if, for, etc.) - just execute it
|
| 300 |
+
mod = ast.Module(body=[node], type_ignores=[])
|
| 301 |
+
exec(compile(mod, filename="<string>", mode="exec"), tools_env)
|
|
|
|
|
|
|
| 302 |
|
| 303 |
result = redirected_output.getvalue()
|
| 304 |
except Exception as exc: # pylint: disable=broad-except
|