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.
- agent/__init__.py +0 -0
- agent/agent.py +182 -0
- agent/base.py +406 -0
- agent/context.py +126 -0
- agent/todo.py +149 -0
- agent/tool_executor.py +54 -0
- agent/verification.py +135 -0
- aloop-0.1.0.dist-info/METADATA +246 -0
- aloop-0.1.0.dist-info/RECORD +62 -0
- aloop-0.1.0.dist-info/WHEEL +5 -0
- aloop-0.1.0.dist-info/entry_points.txt +2 -0
- aloop-0.1.0.dist-info/licenses/LICENSE +21 -0
- aloop-0.1.0.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/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
memory/token_tracker.py
ADDED
|
@@ -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
|