aloop 0.1.0__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.

Potentially problematic release.


This version of aloop might be problematic. Click here for more details.

Files changed (62) hide show
  1. agent/__init__.py +0 -0
  2. agent/agent.py +182 -0
  3. agent/base.py +406 -0
  4. agent/context.py +126 -0
  5. agent/todo.py +149 -0
  6. agent/tool_executor.py +54 -0
  7. agent/verification.py +135 -0
  8. aloop-0.1.0.dist-info/METADATA +246 -0
  9. aloop-0.1.0.dist-info/RECORD +62 -0
  10. aloop-0.1.0.dist-info/WHEEL +5 -0
  11. aloop-0.1.0.dist-info/entry_points.txt +2 -0
  12. aloop-0.1.0.dist-info/licenses/LICENSE +21 -0
  13. aloop-0.1.0.dist-info/top_level.txt +9 -0
  14. cli.py +19 -0
  15. config.py +146 -0
  16. interactive.py +865 -0
  17. llm/__init__.py +51 -0
  18. llm/base.py +26 -0
  19. llm/compat.py +226 -0
  20. llm/content_utils.py +309 -0
  21. llm/litellm_adapter.py +450 -0
  22. llm/message_types.py +245 -0
  23. llm/model_manager.py +265 -0
  24. llm/retry.py +95 -0
  25. main.py +246 -0
  26. memory/__init__.py +20 -0
  27. memory/compressor.py +554 -0
  28. memory/manager.py +538 -0
  29. memory/serialization.py +82 -0
  30. memory/short_term.py +88 -0
  31. memory/token_tracker.py +203 -0
  32. memory/types.py +51 -0
  33. tools/__init__.py +6 -0
  34. tools/advanced_file_ops.py +557 -0
  35. tools/base.py +51 -0
  36. tools/calculator.py +50 -0
  37. tools/code_navigator.py +975 -0
  38. tools/explore.py +254 -0
  39. tools/file_ops.py +150 -0
  40. tools/git_tools.py +791 -0
  41. tools/notify.py +69 -0
  42. tools/parallel_execute.py +420 -0
  43. tools/session_manager.py +205 -0
  44. tools/shell.py +147 -0
  45. tools/shell_background.py +470 -0
  46. tools/smart_edit.py +491 -0
  47. tools/todo.py +130 -0
  48. tools/web_fetch.py +673 -0
  49. tools/web_search.py +61 -0
  50. utils/__init__.py +15 -0
  51. utils/logger.py +105 -0
  52. utils/model_pricing.py +49 -0
  53. utils/runtime.py +75 -0
  54. utils/terminal_ui.py +422 -0
  55. utils/tui/__init__.py +39 -0
  56. utils/tui/command_registry.py +49 -0
  57. utils/tui/components.py +306 -0
  58. utils/tui/input_handler.py +393 -0
  59. utils/tui/model_ui.py +204 -0
  60. utils/tui/progress.py +292 -0
  61. utils/tui/status_bar.py +178 -0
  62. 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)