hanzo 0.3.19__py3-none-any.whl → 0.3.21__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 hanzo might be problematic. Click here for more details.
- hanzo/cli.py +1 -1
- hanzo/dev.py +37 -1
- hanzo/fallback_handler.py +249 -0
- hanzo/memory_manager.py +425 -0
- hanzo/rate_limiter.py +332 -0
- hanzo/streaming.py +271 -0
- {hanzo-0.3.19.dist-info → hanzo-0.3.21.dist-info}/METADATA +1 -1
- {hanzo-0.3.19.dist-info → hanzo-0.3.21.dist-info}/RECORD +10 -6
- {hanzo-0.3.19.dist-info → hanzo-0.3.21.dist-info}/WHEEL +0 -0
- {hanzo-0.3.19.dist-info → hanzo-0.3.21.dist-info}/entry_points.txt +0 -0
hanzo/memory_manager.py
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory management system for Hanzo Dev.
|
|
3
|
+
Provides persistent context and memory like Claude Desktop.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, List, Any, Optional
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from dataclasses import dataclass, asdict
|
|
12
|
+
import hashlib
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class MemoryItem:
|
|
16
|
+
"""A single memory item."""
|
|
17
|
+
id: str
|
|
18
|
+
content: str
|
|
19
|
+
type: str # 'context', 'instruction', 'fact', 'code'
|
|
20
|
+
created_at: str
|
|
21
|
+
tags: List[str]
|
|
22
|
+
priority: int = 0 # Higher priority items are kept longer
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> Dict:
|
|
25
|
+
return asdict(self)
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_dict(cls, data: Dict) -> 'MemoryItem':
|
|
29
|
+
return cls(**data)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MemoryManager:
|
|
33
|
+
"""Manages persistent memory and context for AI conversations."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, workspace_dir: str = None):
|
|
36
|
+
"""Initialize memory manager."""
|
|
37
|
+
if workspace_dir:
|
|
38
|
+
self.memory_dir = Path(workspace_dir) / ".hanzo" / "memory"
|
|
39
|
+
else:
|
|
40
|
+
self.memory_dir = Path.home() / ".hanzo" / "memory"
|
|
41
|
+
|
|
42
|
+
self.memory_dir.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
self.memory_file = self.memory_dir / "context.json"
|
|
44
|
+
self.session_file = self.memory_dir / "session.json"
|
|
45
|
+
|
|
46
|
+
self.memories: List[MemoryItem] = []
|
|
47
|
+
self.session_context: Dict[str, Any] = {}
|
|
48
|
+
|
|
49
|
+
self.load_memories()
|
|
50
|
+
self.load_session()
|
|
51
|
+
|
|
52
|
+
def load_memories(self):
|
|
53
|
+
"""Load persistent memories from disk."""
|
|
54
|
+
if self.memory_file.exists():
|
|
55
|
+
try:
|
|
56
|
+
with open(self.memory_file, 'r') as f:
|
|
57
|
+
data = json.load(f)
|
|
58
|
+
self.memories = [MemoryItem.from_dict(item) for item in data.get('memories', [])]
|
|
59
|
+
except Exception as e:
|
|
60
|
+
print(f"Error loading memories: {e}")
|
|
61
|
+
self.memories = []
|
|
62
|
+
else:
|
|
63
|
+
# Initialize with default memories
|
|
64
|
+
self._init_default_memories()
|
|
65
|
+
|
|
66
|
+
def _init_default_memories(self):
|
|
67
|
+
"""Initialize with helpful default memories."""
|
|
68
|
+
defaults = [
|
|
69
|
+
MemoryItem(
|
|
70
|
+
id=self._generate_id("system"),
|
|
71
|
+
content="I am Hanzo Dev, an AI coding assistant with multiple orchestrator modes.",
|
|
72
|
+
type="instruction",
|
|
73
|
+
created_at=datetime.now().isoformat(),
|
|
74
|
+
tags=["system", "identity"],
|
|
75
|
+
priority=10
|
|
76
|
+
),
|
|
77
|
+
MemoryItem(
|
|
78
|
+
id=self._generate_id("capabilities"),
|
|
79
|
+
content="I can read/write files, search code, run commands, and use various AI models.",
|
|
80
|
+
type="fact",
|
|
81
|
+
created_at=datetime.now().isoformat(),
|
|
82
|
+
tags=["system", "capabilities"],
|
|
83
|
+
priority=9
|
|
84
|
+
),
|
|
85
|
+
MemoryItem(
|
|
86
|
+
id=self._generate_id("help"),
|
|
87
|
+
content="Use /help for commands, #memory for context management, or just chat naturally.",
|
|
88
|
+
type="instruction",
|
|
89
|
+
created_at=datetime.now().isoformat(),
|
|
90
|
+
tags=["system", "usage"],
|
|
91
|
+
priority=8
|
|
92
|
+
),
|
|
93
|
+
]
|
|
94
|
+
self.memories = defaults
|
|
95
|
+
self.save_memories()
|
|
96
|
+
|
|
97
|
+
def save_memories(self):
|
|
98
|
+
"""Save memories to disk."""
|
|
99
|
+
try:
|
|
100
|
+
data = {
|
|
101
|
+
'memories': [m.to_dict() for m in self.memories],
|
|
102
|
+
'updated_at': datetime.now().isoformat()
|
|
103
|
+
}
|
|
104
|
+
with open(self.memory_file, 'w') as f:
|
|
105
|
+
json.dump(data, f, indent=2)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"Error saving memories: {e}")
|
|
108
|
+
|
|
109
|
+
def load_session(self):
|
|
110
|
+
"""Load current session context."""
|
|
111
|
+
if self.session_file.exists():
|
|
112
|
+
try:
|
|
113
|
+
with open(self.session_file, 'r') as f:
|
|
114
|
+
self.session_context = json.load(f)
|
|
115
|
+
except:
|
|
116
|
+
self.session_context = {}
|
|
117
|
+
else:
|
|
118
|
+
self.session_context = {
|
|
119
|
+
'started_at': datetime.now().isoformat(),
|
|
120
|
+
'messages': [],
|
|
121
|
+
'current_task': None,
|
|
122
|
+
'preferences': {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
def save_session(self):
|
|
126
|
+
"""Save session context."""
|
|
127
|
+
try:
|
|
128
|
+
with open(self.session_file, 'w') as f:
|
|
129
|
+
json.dump(self.session_context, f, indent=2)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
print(f"Error saving session: {e}")
|
|
132
|
+
|
|
133
|
+
def add_memory(self, content: str, type: str = "context", tags: List[str] = None, priority: int = 0) -> str:
|
|
134
|
+
"""Add a new memory item."""
|
|
135
|
+
memory_id = self._generate_id(content)
|
|
136
|
+
|
|
137
|
+
# Check if similar memory exists
|
|
138
|
+
for mem in self.memories:
|
|
139
|
+
if mem.content == content:
|
|
140
|
+
return mem.id # Don't duplicate
|
|
141
|
+
|
|
142
|
+
memory = MemoryItem(
|
|
143
|
+
id=memory_id,
|
|
144
|
+
content=content,
|
|
145
|
+
type=type,
|
|
146
|
+
created_at=datetime.now().isoformat(),
|
|
147
|
+
tags=tags or [],
|
|
148
|
+
priority=priority
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
self.memories.append(memory)
|
|
152
|
+
self.save_memories()
|
|
153
|
+
|
|
154
|
+
return memory_id
|
|
155
|
+
|
|
156
|
+
def remove_memory(self, memory_id: str) -> bool:
|
|
157
|
+
"""Remove a memory by ID."""
|
|
158
|
+
for i, mem in enumerate(self.memories):
|
|
159
|
+
if mem.id == memory_id:
|
|
160
|
+
del self.memories[i]
|
|
161
|
+
self.save_memories()
|
|
162
|
+
return True
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
def clear_memories(self, keep_system: bool = True):
|
|
166
|
+
"""Clear all memories, optionally keeping system memories."""
|
|
167
|
+
if keep_system:
|
|
168
|
+
self.memories = [m for m in self.memories if "system" in m.tags]
|
|
169
|
+
else:
|
|
170
|
+
self.memories = []
|
|
171
|
+
self.save_memories()
|
|
172
|
+
|
|
173
|
+
def get_memories(self, type: str = None, tags: List[str] = None) -> List[MemoryItem]:
|
|
174
|
+
"""Get memories filtered by type or tags."""
|
|
175
|
+
result = self.memories
|
|
176
|
+
|
|
177
|
+
if type:
|
|
178
|
+
result = [m for m in result if m.type == type]
|
|
179
|
+
|
|
180
|
+
if tags:
|
|
181
|
+
result = [m for m in result if any(tag in m.tags for tag in tags)]
|
|
182
|
+
|
|
183
|
+
# Sort by priority and creation date
|
|
184
|
+
result.sort(key=lambda m: (-m.priority, m.created_at), reverse=True)
|
|
185
|
+
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
def get_context_string(self, max_tokens: int = 2000) -> str:
|
|
189
|
+
"""Get a formatted context string for AI prompts."""
|
|
190
|
+
# Sort memories by priority
|
|
191
|
+
sorted_memories = sorted(self.memories, key=lambda m: -m.priority)
|
|
192
|
+
|
|
193
|
+
context_parts = []
|
|
194
|
+
token_count = 0
|
|
195
|
+
|
|
196
|
+
for memory in sorted_memories:
|
|
197
|
+
# Rough token estimation (4 chars = 1 token)
|
|
198
|
+
memory_tokens = len(memory.content) // 4
|
|
199
|
+
|
|
200
|
+
if token_count + memory_tokens > max_tokens:
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
if memory.type == "instruction":
|
|
204
|
+
context_parts.append(f"INSTRUCTION: {memory.content}")
|
|
205
|
+
elif memory.type == "fact":
|
|
206
|
+
context_parts.append(f"FACT: {memory.content}")
|
|
207
|
+
elif memory.type == "code":
|
|
208
|
+
context_parts.append(f"CODE CONTEXT:\n{memory.content}")
|
|
209
|
+
else:
|
|
210
|
+
context_parts.append(memory.content)
|
|
211
|
+
|
|
212
|
+
token_count += memory_tokens
|
|
213
|
+
|
|
214
|
+
return "\n\n".join(context_parts)
|
|
215
|
+
|
|
216
|
+
def add_message(self, role: str, content: str):
|
|
217
|
+
"""Add a message to session history."""
|
|
218
|
+
self.session_context['messages'].append({
|
|
219
|
+
'role': role,
|
|
220
|
+
'content': content,
|
|
221
|
+
'timestamp': datetime.now().isoformat()
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
# Keep only last 50 messages
|
|
225
|
+
if len(self.session_context['messages']) > 50:
|
|
226
|
+
self.session_context['messages'] = self.session_context['messages'][-50:]
|
|
227
|
+
|
|
228
|
+
self.save_session()
|
|
229
|
+
|
|
230
|
+
def get_recent_messages(self, count: int = 10) -> List[Dict]:
|
|
231
|
+
"""Get recent messages from session."""
|
|
232
|
+
return self.session_context['messages'][-count:]
|
|
233
|
+
|
|
234
|
+
def set_preference(self, key: str, value: Any):
|
|
235
|
+
"""Set a user preference."""
|
|
236
|
+
self.session_context['preferences'][key] = value
|
|
237
|
+
self.save_session()
|
|
238
|
+
|
|
239
|
+
def get_preference(self, key: str, default: Any = None) -> Any:
|
|
240
|
+
"""Get a user preference."""
|
|
241
|
+
return self.session_context['preferences'].get(key, default)
|
|
242
|
+
|
|
243
|
+
def _generate_id(self, content: str) -> str:
|
|
244
|
+
"""Generate a unique ID for a memory item."""
|
|
245
|
+
hash_input = f"{content}{datetime.now().isoformat()}"
|
|
246
|
+
return hashlib.md5(hash_input.encode()).hexdigest()[:8]
|
|
247
|
+
|
|
248
|
+
def summarize_for_ai(self) -> str:
|
|
249
|
+
"""Create a summary suitable for AI context."""
|
|
250
|
+
summary = []
|
|
251
|
+
|
|
252
|
+
# Add system memories
|
|
253
|
+
system_memories = self.get_memories(tags=["system"])
|
|
254
|
+
if system_memories:
|
|
255
|
+
summary.append("SYSTEM CONTEXT:")
|
|
256
|
+
for mem in system_memories[:3]: # Top 3 system memories
|
|
257
|
+
summary.append(f"- {mem.content}")
|
|
258
|
+
|
|
259
|
+
# Add recent instructions
|
|
260
|
+
instructions = self.get_memories(type="instruction")
|
|
261
|
+
if instructions:
|
|
262
|
+
summary.append("\nINSTRUCTIONS:")
|
|
263
|
+
for mem in instructions[:3]: # Top 3 instructions
|
|
264
|
+
summary.append(f"- {mem.content}")
|
|
265
|
+
|
|
266
|
+
# Add important facts
|
|
267
|
+
facts = self.get_memories(type="fact")
|
|
268
|
+
if facts:
|
|
269
|
+
summary.append("\nKEY FACTS:")
|
|
270
|
+
for mem in facts[:5]: # Top 5 facts
|
|
271
|
+
summary.append(f"- {mem.content}")
|
|
272
|
+
|
|
273
|
+
# Add current task if set
|
|
274
|
+
if self.session_context.get('current_task'):
|
|
275
|
+
summary.append(f"\nCURRENT TASK: {self.session_context['current_task']}")
|
|
276
|
+
|
|
277
|
+
return "\n".join(summary)
|
|
278
|
+
|
|
279
|
+
def export_memories(self, file_path: str):
|
|
280
|
+
"""Export memories to a file."""
|
|
281
|
+
data = {
|
|
282
|
+
'memories': [m.to_dict() for m in self.memories],
|
|
283
|
+
'session': self.session_context,
|
|
284
|
+
'exported_at': datetime.now().isoformat()
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
with open(file_path, 'w') as f:
|
|
288
|
+
json.dump(data, f, indent=2)
|
|
289
|
+
|
|
290
|
+
def import_memories(self, file_path: str):
|
|
291
|
+
"""Import memories from a file."""
|
|
292
|
+
with open(file_path, 'r') as f:
|
|
293
|
+
data = json.load(f)
|
|
294
|
+
|
|
295
|
+
# Merge memories (avoid duplicates)
|
|
296
|
+
existing_ids = {m.id for m in self.memories}
|
|
297
|
+
|
|
298
|
+
for mem_data in data.get('memories', []):
|
|
299
|
+
if mem_data['id'] not in existing_ids:
|
|
300
|
+
self.memories.append(MemoryItem.from_dict(mem_data))
|
|
301
|
+
|
|
302
|
+
# Merge session preferences
|
|
303
|
+
if 'session' in data and 'preferences' in data['session']:
|
|
304
|
+
self.session_context['preferences'].update(data['session']['preferences'])
|
|
305
|
+
|
|
306
|
+
self.save_memories()
|
|
307
|
+
self.save_session()
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def handle_memory_command(command: str, memory_manager: MemoryManager, console) -> bool:
|
|
311
|
+
"""
|
|
312
|
+
Handle #memory commands.
|
|
313
|
+
Returns True if command was handled, False otherwise.
|
|
314
|
+
"""
|
|
315
|
+
from rich.table import Table
|
|
316
|
+
from rich.panel import Panel
|
|
317
|
+
|
|
318
|
+
parts = command.strip().split(maxsplit=2)
|
|
319
|
+
|
|
320
|
+
if len(parts) == 1 or parts[1] == "show":
|
|
321
|
+
# Show current memories
|
|
322
|
+
memories = memory_manager.get_memories()
|
|
323
|
+
|
|
324
|
+
if not memories:
|
|
325
|
+
console.print("[yellow]No memories stored.[/yellow]")
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
table = Table(title="Current Memories", show_header=True,
|
|
329
|
+
header_style="bold magenta")
|
|
330
|
+
table.add_column("ID", style="cyan", width=10)
|
|
331
|
+
table.add_column("Type", width=12)
|
|
332
|
+
table.add_column("Content", width=50)
|
|
333
|
+
table.add_column("Priority", width=8)
|
|
334
|
+
|
|
335
|
+
for mem in memories[:10]: # Show top 10
|
|
336
|
+
content = mem.content[:47] + "..." if len(mem.content) > 50 else mem.content
|
|
337
|
+
table.add_row(mem.id, mem.type, content, str(mem.priority))
|
|
338
|
+
|
|
339
|
+
console.print(table)
|
|
340
|
+
|
|
341
|
+
if len(memories) > 10:
|
|
342
|
+
console.print(f"[dim]... and {len(memories) - 10} more[/dim]")
|
|
343
|
+
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
elif parts[1] == "add":
|
|
347
|
+
if len(parts) < 3:
|
|
348
|
+
console.print("[red]Usage: #memory add <content>[/red]")
|
|
349
|
+
return True
|
|
350
|
+
|
|
351
|
+
content = parts[2]
|
|
352
|
+
memory_id = memory_manager.add_memory(content, type="context")
|
|
353
|
+
console.print(f"[green]Added memory: {memory_id}[/green]")
|
|
354
|
+
return True
|
|
355
|
+
|
|
356
|
+
elif parts[1] == "remove":
|
|
357
|
+
if len(parts) < 3:
|
|
358
|
+
console.print("[red]Usage: #memory remove <id>[/red]")
|
|
359
|
+
return True
|
|
360
|
+
|
|
361
|
+
memory_id = parts[2]
|
|
362
|
+
if memory_manager.remove_memory(memory_id):
|
|
363
|
+
console.print(f"[green]Removed memory: {memory_id}[/green]")
|
|
364
|
+
else:
|
|
365
|
+
console.print(f"[red]Memory not found: {memory_id}[/red]")
|
|
366
|
+
return True
|
|
367
|
+
|
|
368
|
+
elif parts[1] == "clear":
|
|
369
|
+
memory_manager.clear_memories(keep_system=True)
|
|
370
|
+
console.print("[green]Cleared all non-system memories.[/green]")
|
|
371
|
+
return True
|
|
372
|
+
|
|
373
|
+
elif parts[1] == "save":
|
|
374
|
+
memory_manager.save_memories()
|
|
375
|
+
memory_manager.save_session()
|
|
376
|
+
console.print("[green]Memories saved.[/green]")
|
|
377
|
+
return True
|
|
378
|
+
|
|
379
|
+
elif parts[1] == "export":
|
|
380
|
+
if len(parts) < 3:
|
|
381
|
+
file_path = "hanzo_memories.json"
|
|
382
|
+
else:
|
|
383
|
+
file_path = parts[2]
|
|
384
|
+
|
|
385
|
+
memory_manager.export_memories(file_path)
|
|
386
|
+
console.print(f"[green]Exported memories to {file_path}[/green]")
|
|
387
|
+
return True
|
|
388
|
+
|
|
389
|
+
elif parts[1] == "import":
|
|
390
|
+
if len(parts) < 3:
|
|
391
|
+
console.print("[red]Usage: #memory import <file_path>[/red]")
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
file_path = parts[2]
|
|
395
|
+
try:
|
|
396
|
+
memory_manager.import_memories(file_path)
|
|
397
|
+
console.print(f"[green]Imported memories from {file_path}[/green]")
|
|
398
|
+
except Exception as e:
|
|
399
|
+
console.print(f"[red]Error importing: {e}[/red]")
|
|
400
|
+
return True
|
|
401
|
+
|
|
402
|
+
elif parts[1] == "context":
|
|
403
|
+
# Show AI context
|
|
404
|
+
context = memory_manager.summarize_for_ai()
|
|
405
|
+
console.print(Panel(context, title="[bold cyan]AI Context[/bold cyan]",
|
|
406
|
+
title_align="left", border_style="dim cyan"))
|
|
407
|
+
return True
|
|
408
|
+
|
|
409
|
+
elif parts[1] == "help":
|
|
410
|
+
help_text = """Memory Commands:
|
|
411
|
+
#memory [show] - Show current memories
|
|
412
|
+
#memory add <text> - Add new memory
|
|
413
|
+
#memory remove <id> - Remove memory by ID
|
|
414
|
+
#memory clear - Clear all memories (keep system)
|
|
415
|
+
#memory save - Save memories to disk
|
|
416
|
+
#memory export [file] - Export memories to file
|
|
417
|
+
#memory import <file> - Import memories from file
|
|
418
|
+
#memory context - Show AI context summary
|
|
419
|
+
#memory help - Show this help"""
|
|
420
|
+
|
|
421
|
+
console.print(Panel(help_text, title="[bold cyan]Memory Help[/bold cyan]",
|
|
422
|
+
title_align="left", border_style="dim cyan"))
|
|
423
|
+
return True
|
|
424
|
+
|
|
425
|
+
return False
|