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
@@ -0,0 +1,203 @@
1
+ """Token counting and cost tracking for memory management."""
2
+
3
+ import logging
4
+ from typing import Dict
5
+
6
+ from llm.content_utils import extract_text
7
+ from llm.message_types import LLMMessage
8
+ from utils.model_pricing import MODEL_PRICING
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class TokenTracker:
14
+ """Tracks token usage and costs across conversations."""
15
+
16
+ # Use imported pricing configuration
17
+ PRICING = MODEL_PRICING
18
+
19
+ def __init__(self):
20
+ """Initialize token tracker."""
21
+ self.total_input_tokens = 0
22
+ self.total_output_tokens = 0
23
+ self.compression_savings = 0 # Tokens saved through compression
24
+ self.compression_cost = 0 # Tokens spent on compression
25
+
26
+ def count_message_tokens(self, message: LLMMessage, provider: str, model: str) -> int:
27
+ """Count tokens in a message.
28
+
29
+ Args:
30
+ message: LLMMessage to count tokens for
31
+ provider: LLM provider name ("openai", "anthropic", "gemini")
32
+ model: Model identifier
33
+
34
+ Returns:
35
+ Token count
36
+ """
37
+ content = self._extract_content(message)
38
+
39
+ if provider == "openai":
40
+ return self._count_openai_tokens(content, model)
41
+ elif provider == "anthropic":
42
+ return self._count_anthropic_tokens(content)
43
+ elif provider == "gemini":
44
+ return self._count_gemini_tokens(content)
45
+ else:
46
+ # Fallback: rough estimate
47
+ return len(str(content)) // 4
48
+
49
+ def _extract_content(self, message) -> str:
50
+ """Extract text content from message.
51
+
52
+ Uses centralized extract_text from content_utils.
53
+ """
54
+ # Use centralized extraction
55
+ text = extract_text(message.content)
56
+
57
+ # For token counting, also include tool calls as string representation
58
+ if hasattr(message, "tool_calls") and message.tool_calls:
59
+ text += "\n" + str(message.tool_calls)
60
+
61
+ return text if text else str(message.content)
62
+
63
+ def _count_openai_tokens(self, text: str, model: str) -> int:
64
+ """Count tokens using tiktoken for OpenAI models."""
65
+ try:
66
+ import tiktoken
67
+
68
+ try:
69
+ encoding = tiktoken.encoding_for_model(model)
70
+ except KeyError:
71
+ # Fallback to cl100k_base for unknown models
72
+ encoding = tiktoken.get_encoding("cl100k_base")
73
+
74
+ return len(encoding.encode(text))
75
+ except ImportError:
76
+ logger.warning("tiktoken not installed, using fallback estimation")
77
+ return len(text) // 4
78
+ except Exception as e:
79
+ logger.warning(f"Error counting tokens: {e}, using fallback")
80
+ return len(text) // 4
81
+
82
+ def _count_anthropic_tokens(self, text: str) -> int:
83
+ """Count tokens for Anthropic models.
84
+
85
+ Note: Anthropic SDK no longer provides a direct token counting method.
86
+ Using estimation: ~3.5 characters per token (based on Claude's tokenizer).
87
+ """
88
+ # Anthropic's rough estimation: 1 token ≈ 3.5 characters
89
+ # This is more accurate than 4 chars/token for Claude models
90
+ return int(len(text) / 3.5) if len(text) > 0 else 0
91
+
92
+ def _count_gemini_tokens(self, text: str) -> int:
93
+ """Estimate tokens for Gemini models.
94
+
95
+ Note: Google doesn't provide a token counting API for Gemini,
96
+ so we use an approximation.
97
+ """
98
+ # Rough estimate: 1 token ≈ 4 characters
99
+ return len(text) // 4
100
+
101
+ def add_input_tokens(self, count: int):
102
+ """Record input tokens used."""
103
+ self.total_input_tokens += count
104
+
105
+ def add_output_tokens(self, count: int):
106
+ """Record output tokens generated."""
107
+ self.total_output_tokens += count
108
+
109
+ def add_compression_savings(self, saved: int):
110
+ """Record tokens saved through compression."""
111
+ self.compression_savings += saved
112
+
113
+ def add_compression_cost(self, cost: int):
114
+ """Record tokens spent on compression."""
115
+ self.compression_cost += cost
116
+
117
+ def calculate_cost(
118
+ self, model: str, input_tokens: int = None, output_tokens: int = None
119
+ ) -> float:
120
+ """Calculate cost for given token usage.
121
+
122
+ Args:
123
+ model: Model identifier
124
+ input_tokens: Input token count (None = use total)
125
+ output_tokens: Output token count (None = use total)
126
+
127
+ Returns:
128
+ Cost in USD
129
+ """
130
+ if input_tokens is None:
131
+ input_tokens = self.total_input_tokens
132
+ if output_tokens is None:
133
+ output_tokens = self.total_output_tokens
134
+
135
+ # Find matching pricing
136
+ pricing = None
137
+ for model_key, price in self.PRICING.items():
138
+ if model_key in model:
139
+ pricing = price
140
+ break
141
+
142
+ if not pricing:
143
+ logger.info(
144
+ f"No pricing found for model {model}, using default pricing (DeepSeek-Reasoner equivalent)"
145
+ )
146
+ # Fallback to default pricing (using reasonable mid-tier estimate)
147
+ pricing = self.PRICING["default"]
148
+
149
+ # Calculate cost
150
+ input_cost = (input_tokens * pricing["input"]) / 1_000_000
151
+ output_cost = (output_tokens * pricing["output"]) / 1_000_000
152
+
153
+ return input_cost + output_cost
154
+
155
+ def get_total_cost(self, model: str) -> float:
156
+ """Get total cost for this conversation.
157
+
158
+ Args:
159
+ model: Model identifier
160
+
161
+ Returns:
162
+ Total cost in USD
163
+ """
164
+ return self.calculate_cost(model)
165
+
166
+ def get_net_savings(self, model: str) -> Dict[str, float]:
167
+ """Calculate net token and cost savings after accounting for compression overhead.
168
+
169
+ Args:
170
+ model: Model identifier
171
+
172
+ Returns:
173
+ Dict with net_tokens, net_cost, savings_percentage
174
+ """
175
+ net_tokens = self.compression_savings - self.compression_cost
176
+
177
+ # Calculate cost of saved tokens
178
+ saved_cost = self.calculate_cost(
179
+ model, input_tokens=self.compression_savings, output_tokens=0
180
+ )
181
+ compression_cost = self.calculate_cost(
182
+ model, input_tokens=0, output_tokens=self.compression_cost
183
+ )
184
+ net_cost = saved_cost - compression_cost
185
+
186
+ # Calculate percentage
187
+ total_tokens = self.total_input_tokens + self.total_output_tokens
188
+ savings_percentage = (net_tokens / total_tokens * 100) if total_tokens > 0 else 0
189
+
190
+ return {
191
+ "net_tokens": net_tokens,
192
+ "net_cost": net_cost,
193
+ "savings_percentage": savings_percentage,
194
+ "total_saved_tokens": self.compression_savings,
195
+ "compression_overhead_tokens": self.compression_cost,
196
+ }
197
+
198
+ def reset(self):
199
+ """Reset all counters."""
200
+ self.total_input_tokens = 0
201
+ self.total_output_tokens = 0
202
+ self.compression_savings = 0
203
+ self.compression_cost = 0
memory/types.py ADDED
@@ -0,0 +1,51 @@
1
+ """Data types for memory management system."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import Any, Dict, List
6
+
7
+ from llm.base import LLMMessage
8
+
9
+
10
+ @dataclass
11
+ class CompressedMemory:
12
+ """Represents a compressed memory segment.
13
+
14
+ The messages list contains ALL messages to keep after compression,
15
+ including any summary (converted to a user message at the front).
16
+ """
17
+
18
+ messages: List[LLMMessage] = field(default_factory=list) # All messages after compression
19
+ original_message_count: int = 0
20
+ compressed_tokens: int = 0
21
+ original_tokens: int = 0
22
+ compression_ratio: float = 0.0
23
+ created_at: datetime = field(default_factory=datetime.now)
24
+ metadata: Dict[str, Any] = field(default_factory=dict)
25
+
26
+ @property
27
+ def token_savings(self) -> int:
28
+ """Calculate tokens saved by compression."""
29
+ return self.original_tokens - self.compressed_tokens
30
+
31
+ @property
32
+ def savings_percentage(self) -> float:
33
+ """Calculate percentage of tokens saved."""
34
+ if self.original_tokens == 0:
35
+ return 0.0
36
+ return (self.token_savings / self.original_tokens) * 100
37
+
38
+
39
+ @dataclass
40
+ class CompressionStrategy:
41
+ """Enum-like class for compression strategies.
42
+
43
+ Supported strategies:
44
+ - DELETION: Used for very few messages (<5), simply removes oldest
45
+ - SLIDING_WINDOW: Keeps recent N messages, summarizes the rest
46
+ - SELECTIVE: Intelligently preserves important messages, summarizes others (primary strategy)
47
+ """
48
+
49
+ DELETION = "deletion" # Simply delete old messages
50
+ SLIDING_WINDOW = "sliding_window" # Summarize old messages, keep recent
51
+ SELECTIVE = "selective" # Preserve important messages, summarize others
tools/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Tools package for agent tool implementations."""
2
+
3
+ from .explore import ExploreTool
4
+ from .parallel_execute import ParallelExecutionTool
5
+
6
+ __all__ = ["ExploreTool", "ParallelExecutionTool"]