aloop 0.1.1__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.
- agent/__init__.py +0 -0
- agent/agent.py +182 -0
- agent/base.py +406 -0
- agent/context.py +126 -0
- agent/prompts/__init__.py +1 -0
- agent/todo.py +149 -0
- agent/tool_executor.py +54 -0
- agent/verification.py +135 -0
- aloop-0.1.1.dist-info/METADATA +252 -0
- aloop-0.1.1.dist-info/RECORD +66 -0
- aloop-0.1.1.dist-info/WHEEL +5 -0
- aloop-0.1.1.dist-info/entry_points.txt +2 -0
- aloop-0.1.1.dist-info/licenses/LICENSE +21 -0
- aloop-0.1.1.dist-info/top_level.txt +9 -0
- cli.py +19 -0
- config.py +146 -0
- interactive.py +865 -0
- llm/__init__.py +51 -0
- llm/base.py +26 -0
- llm/compat.py +226 -0
- llm/content_utils.py +309 -0
- llm/litellm_adapter.py +450 -0
- llm/message_types.py +245 -0
- llm/model_manager.py +265 -0
- llm/retry.py +95 -0
- main.py +246 -0
- memory/__init__.py +20 -0
- memory/compressor.py +554 -0
- memory/manager.py +538 -0
- memory/serialization.py +82 -0
- memory/short_term.py +88 -0
- memory/store/__init__.py +6 -0
- memory/store/memory_store.py +100 -0
- memory/store/yaml_file_memory_store.py +414 -0
- memory/token_tracker.py +203 -0
- memory/types.py +51 -0
- tools/__init__.py +6 -0
- tools/advanced_file_ops.py +557 -0
- tools/base.py +51 -0
- tools/calculator.py +50 -0
- tools/code_navigator.py +975 -0
- tools/explore.py +254 -0
- tools/file_ops.py +150 -0
- tools/git_tools.py +791 -0
- tools/notify.py +69 -0
- tools/parallel_execute.py +420 -0
- tools/session_manager.py +205 -0
- tools/shell.py +147 -0
- tools/shell_background.py +470 -0
- tools/smart_edit.py +491 -0
- tools/todo.py +130 -0
- tools/web_fetch.py +673 -0
- tools/web_search.py +61 -0
- utils/__init__.py +15 -0
- utils/logger.py +105 -0
- utils/model_pricing.py +49 -0
- utils/runtime.py +75 -0
- utils/terminal_ui.py +422 -0
- utils/tui/__init__.py +39 -0
- utils/tui/command_registry.py +49 -0
- utils/tui/components.py +306 -0
- utils/tui/input_handler.py +393 -0
- utils/tui/model_ui.py +204 -0
- utils/tui/progress.py +292 -0
- utils/tui/status_bar.py +178 -0
- utils/tui/theme.py +165 -0
tools/web_search.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Web search tool using DuckDuckGo."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
from ddgs import DDGS
|
|
7
|
+
|
|
8
|
+
from .base import BaseTool
|
|
9
|
+
|
|
10
|
+
# Default timeout for web search operations
|
|
11
|
+
DEFAULT_SEARCH_TIMEOUT = 30.0
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _sync_search(query: str, max_results: int = 5) -> List[Dict[str, str]]:
|
|
15
|
+
"""Synchronous search function to run in thread."""
|
|
16
|
+
with DDGS() as ddgs:
|
|
17
|
+
return list(ddgs.text(query, max_results=max_results))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WebSearchTool(BaseTool):
|
|
21
|
+
"""Simple web search using DuckDuckGo (no API key needed)."""
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def name(self) -> str:
|
|
25
|
+
return "web_search"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def description(self) -> str:
|
|
29
|
+
return "Search the web for information using DuckDuckGo"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def parameters(self) -> Dict[str, Any]:
|
|
33
|
+
return {
|
|
34
|
+
"query": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "Search query",
|
|
37
|
+
},
|
|
38
|
+
"timeout": {
|
|
39
|
+
"type": "number",
|
|
40
|
+
"description": "Optional timeout in seconds (default: 30)",
|
|
41
|
+
"default": DEFAULT_SEARCH_TIMEOUT,
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async def execute(self, query: str, timeout: float = DEFAULT_SEARCH_TIMEOUT) -> str:
|
|
46
|
+
"""Execute web search and return results."""
|
|
47
|
+
try:
|
|
48
|
+
timeout_val = float(timeout) if timeout else DEFAULT_SEARCH_TIMEOUT
|
|
49
|
+
results = []
|
|
50
|
+
async with asyncio.timeout(timeout_val):
|
|
51
|
+
search_results = await asyncio.to_thread(_sync_search, query, 5)
|
|
52
|
+
for r in search_results:
|
|
53
|
+
title = r.get("title", "")
|
|
54
|
+
href = r.get("href", "")
|
|
55
|
+
body = r.get("body", "")
|
|
56
|
+
results.append(f"[{title}]({href})\n{body}\n")
|
|
57
|
+
return "\n---\n".join(results) if results else "No results found"
|
|
58
|
+
except TimeoutError:
|
|
59
|
+
return f"Error: Web search timed out after {timeout}s"
|
|
60
|
+
except Exception as e:
|
|
61
|
+
return f"Error searching web: {str(e)}"
|
utils/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Utility modules for agentic loop."""
|
|
2
|
+
|
|
3
|
+
from . import terminal_ui
|
|
4
|
+
from .logger import get_log_file_path, get_logger, setup_logger
|
|
5
|
+
|
|
6
|
+
# Note: Runtime functions are NOT exported here to avoid circular imports.
|
|
7
|
+
# Import directly from utils.runtime when needed:
|
|
8
|
+
# from utils.runtime import get_config_file, get_sessions_dir, etc.
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"setup_logger",
|
|
12
|
+
"get_logger",
|
|
13
|
+
"get_log_file_path",
|
|
14
|
+
"terminal_ui",
|
|
15
|
+
]
|
utils/logger.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Logging configuration for the agentic loop system."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from .runtime import get_log_dir
|
|
9
|
+
|
|
10
|
+
# Global flag to track if logging has been initialized
|
|
11
|
+
_logging_initialized = False
|
|
12
|
+
_log_file_path = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def setup_logger(
|
|
16
|
+
log_dir: Optional[str] = None,
|
|
17
|
+
log_level: Optional[str] = None,
|
|
18
|
+
log_to_console: bool = False,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Configure the logging system globally.
|
|
21
|
+
|
|
22
|
+
This should be called once at the start of the application when --verbose is enabled.
|
|
23
|
+
Logging is written to .aloop/logs/ by default.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
log_dir: Directory to store log files (default: .aloop/logs/)
|
|
27
|
+
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
28
|
+
log_to_console: Whether to also log to console
|
|
29
|
+
"""
|
|
30
|
+
global _logging_initialized, _log_file_path
|
|
31
|
+
|
|
32
|
+
if _logging_initialized:
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
# Use runtime log directory by default
|
|
36
|
+
if log_dir is None:
|
|
37
|
+
log_dir = get_log_dir()
|
|
38
|
+
|
|
39
|
+
# Get log level from Config if not provided
|
|
40
|
+
if log_level is None:
|
|
41
|
+
try:
|
|
42
|
+
from config import Config
|
|
43
|
+
|
|
44
|
+
log_level = Config.LOG_LEVEL
|
|
45
|
+
except ImportError:
|
|
46
|
+
log_level = "DEBUG"
|
|
47
|
+
|
|
48
|
+
# Set root logger level
|
|
49
|
+
level = getattr(logging, log_level.upper(), logging.DEBUG)
|
|
50
|
+
logging.root.setLevel(level)
|
|
51
|
+
|
|
52
|
+
# Create formatter
|
|
53
|
+
formatter = logging.Formatter(
|
|
54
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Add file handler (always enabled when setup_logger is called)
|
|
58
|
+
log_path = Path(log_dir)
|
|
59
|
+
log_path.mkdir(exist_ok=True, parents=True)
|
|
60
|
+
|
|
61
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
62
|
+
log_file = log_path / f"aloop_{timestamp}.log"
|
|
63
|
+
_log_file_path = str(log_file)
|
|
64
|
+
|
|
65
|
+
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
|
66
|
+
file_handler.setLevel(level)
|
|
67
|
+
file_handler.setFormatter(formatter)
|
|
68
|
+
logging.root.addHandler(file_handler)
|
|
69
|
+
|
|
70
|
+
# Add console handler if enabled
|
|
71
|
+
if log_to_console:
|
|
72
|
+
console_handler = logging.StreamHandler()
|
|
73
|
+
console_handler.setLevel(logging.WARNING)
|
|
74
|
+
console_handler.setFormatter(formatter)
|
|
75
|
+
logging.root.addHandler(console_handler)
|
|
76
|
+
|
|
77
|
+
_logging_initialized = True
|
|
78
|
+
|
|
79
|
+
# Log initialization message
|
|
80
|
+
logging.info(f"Logging initialized. Level: {log_level}, File: {_log_file_path}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_logger(name: str) -> logging.Logger:
|
|
84
|
+
"""Get a logger instance for a module.
|
|
85
|
+
|
|
86
|
+
Note: This no longer auto-initializes logging. Logging is only enabled
|
|
87
|
+
when --verbose flag is used and setup_logger() is called explicitly.
|
|
88
|
+
Without verbose mode, logs go nowhere (NullHandler behavior).
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
name: Logger name (typically __name__)
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Logger instance
|
|
95
|
+
"""
|
|
96
|
+
return logging.getLogger(name)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_log_file_path() -> Optional[str]:
|
|
100
|
+
"""Get the path to the current log file.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Path to log file, or None if logging to file is disabled
|
|
104
|
+
"""
|
|
105
|
+
return _log_file_path
|
utils/model_pricing.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""model pricing - updated at 2026-01-24"""
|
|
2
|
+
|
|
3
|
+
# Pricing is in USD per 1 million tokens
|
|
4
|
+
MODEL_PRICING = {
|
|
5
|
+
# --- OpenAI ---
|
|
6
|
+
"gpt-5": {"input": 1.25, "output": 10.00},
|
|
7
|
+
"gpt-4.5": {"input": 75.00, "output": 150.00},
|
|
8
|
+
"gpt-4o": {"input": 2.50, "output": 10.00},
|
|
9
|
+
"gpt-4o-mini": {"input": 0.15, "output": 0.60},
|
|
10
|
+
"o1": {"input": 15.00, "output": 60.00},
|
|
11
|
+
"o1-mini": {"input": 1.10, "output": 4.40},
|
|
12
|
+
"o3": {"input": 2.00, "output": 8.00},
|
|
13
|
+
"o3-mini": {"input": 0.55, "output": 2.20},
|
|
14
|
+
"o4-mini": {"input": 1.10, "output": 4.40},
|
|
15
|
+
# --- Anthropic ---
|
|
16
|
+
"claude-opus-4-5": {"input": 5.00, "output": 25.00},
|
|
17
|
+
"claude-sonnet-4-5": {"input": 3.00, "output": 15.00},
|
|
18
|
+
"claude-haiku-4-5": {"input": 1.00, "output": 5.00},
|
|
19
|
+
"claude-opus-4-1": {"input": 15.00, "output": 75.00},
|
|
20
|
+
"claude-sonnet-4": {"input": 3.00, "output": 15.00},
|
|
21
|
+
"claude-opus-4": {"input": 15.00, "output": 75.00},
|
|
22
|
+
"claude-haiku-3": {"input": 0.25, "output": 1.25},
|
|
23
|
+
"claude-3-5-sonnet-20241022": {"input": 3.00, "output": 15.00},
|
|
24
|
+
"claude-3-5-haiku-20241022": {"input": 0.80, "output": 4.00},
|
|
25
|
+
"claude-3-opus-20240229": {"input": 15.00, "output": 75.00},
|
|
26
|
+
# --- Google Gemini ---
|
|
27
|
+
"gemini-3-pro": {"input": 2.00, "output": 12.00},
|
|
28
|
+
"gemini-3-pro-preview": {"input": 2.00, "output": 12.00},
|
|
29
|
+
"gemini-3-flash": {"input": 0.50, "output": 3.00},
|
|
30
|
+
"gemini-3-flash-preview": {"input": 0.50, "output": 3.00},
|
|
31
|
+
"gemini-2-5-pro": {"input": 1.25, "output": 10.00},
|
|
32
|
+
"gemini-2-5-flash": {"input": 0.30, "output": 2.50},
|
|
33
|
+
"gemini-2-5-flash-lite": {"input": 0.10, "output": 0.40},
|
|
34
|
+
"gemini-2-0-flash": {"input": 0.10, "output": 0.40},
|
|
35
|
+
"gemini-2-0-flash-lite": {"input": 0.075, "output": 0.30},
|
|
36
|
+
"gemini-1-5-pro": {"input": 1.25, "output": 5.00},
|
|
37
|
+
"gemini-1-5-flash": {"input": 0.075, "output": 0.30},
|
|
38
|
+
# --- DeepSeek ---
|
|
39
|
+
"deepseek-v3": {"input": 0.14, "output": 0.28},
|
|
40
|
+
"deepseek-reasoner": {"input": 0.55, "output": 2.19},
|
|
41
|
+
# --- xAI (Grok) ---
|
|
42
|
+
"grok-4": {"input": 3.00, "output": 15.00},
|
|
43
|
+
"grok-4-fast": {"input": 0.20, "output": 0.50},
|
|
44
|
+
# --- Mistral ---
|
|
45
|
+
"mistral-large-2": {"input": 2.00, "output": 6.00},
|
|
46
|
+
"mistral-small-3": {"input": 0.10, "output": 0.30},
|
|
47
|
+
# --- Default ---
|
|
48
|
+
"default": {"input": 0.55, "output": 2.19},
|
|
49
|
+
}
|
utils/runtime.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Runtime directory management for aloop.
|
|
2
|
+
|
|
3
|
+
All runtime data is stored under ~/.aloop/ directory:
|
|
4
|
+
- config: Configuration file (created by config.py on first import)
|
|
5
|
+
- sessions/: YAML-based session persistence
|
|
6
|
+
- logs/: Log files (only created with --verbose)
|
|
7
|
+
- history: Interactive mode command history
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
RUNTIME_DIR = os.path.join(os.path.expanduser("~"), ".aloop")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_runtime_dir() -> str:
|
|
16
|
+
"""Get the runtime directory path.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Path to ~/.aloop directory
|
|
20
|
+
"""
|
|
21
|
+
return RUNTIME_DIR
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_config_file() -> str:
|
|
25
|
+
"""Get the configuration file path.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Path to ~/.aloop/config
|
|
29
|
+
"""
|
|
30
|
+
return os.path.join(RUNTIME_DIR, "config")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_sessions_dir() -> str:
|
|
34
|
+
"""Get the sessions directory path.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Path to ~/.aloop/sessions/
|
|
38
|
+
"""
|
|
39
|
+
return os.path.join(RUNTIME_DIR, "sessions")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_log_dir() -> str:
|
|
43
|
+
"""Get the log directory path.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Path to ~/.aloop/logs/
|
|
47
|
+
"""
|
|
48
|
+
return os.path.join(RUNTIME_DIR, "logs")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_history_file() -> str:
|
|
52
|
+
"""Get the command history file path.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Path to ~/.aloop/history
|
|
56
|
+
"""
|
|
57
|
+
return os.path.join(RUNTIME_DIR, "history")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def ensure_runtime_dirs(create_logs: bool = False) -> None:
|
|
61
|
+
"""Ensure runtime directories exist.
|
|
62
|
+
|
|
63
|
+
Creates:
|
|
64
|
+
- ~/.aloop/sessions/
|
|
65
|
+
- ~/.aloop/logs/ (only if create_logs=True)
|
|
66
|
+
|
|
67
|
+
Note: ~/.aloop/config is created by config.py on first import.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
create_logs: Whether to create the logs directory (for --verbose mode)
|
|
71
|
+
"""
|
|
72
|
+
os.makedirs(os.path.join(RUNTIME_DIR, "sessions"), exist_ok=True)
|
|
73
|
+
|
|
74
|
+
if create_logs:
|
|
75
|
+
os.makedirs(os.path.join(RUNTIME_DIR, "logs"), exist_ok=True)
|