hanuscode 1.0.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.
- hanus/__init__.py +5 -0
- hanus/__main__.py +10 -0
- hanus/action_handlers.py +76 -0
- hanus/action_parser.py +82 -0
- hanus/agent_runner.py +1445 -0
- hanus/analysis/__init__.py +5 -0
- hanus/analysis/debt.py +702 -0
- hanus/analysis/dependencies.py +475 -0
- hanus/cache/__init__.py +5 -0
- hanus/cache/response_cache.py +560 -0
- hanus/config.py +401 -0
- hanus/connectors/__init__.py +19 -0
- hanus/connectors/base.py +114 -0
- hanus/connectors/claude_connector.py +146 -0
- hanus/connectors/gemini_connector.py +141 -0
- hanus/connectors/glm_connector.py +160 -0
- hanus/connectors/ollama_connector.py +174 -0
- hanus/connectors/openai_connector.py +122 -0
- hanus/connectors/registry.py +26 -0
- hanus/context/__init__.py +7 -0
- hanus/context/manager.py +837 -0
- hanus/context/selective.py +626 -0
- hanus/error_recovery/__init__.py +5 -0
- hanus/error_recovery/auto_fix.py +605 -0
- hanus/hooks/__init__.py +5 -0
- hanus/hooks/manager.py +247 -0
- hanus/instincts/__init__.py +44 -0
- hanus/instincts/cli.py +372 -0
- hanus/instincts/detector.py +281 -0
- hanus/instincts/evolver.py +361 -0
- hanus/instincts/manager.py +343 -0
- hanus/instincts/types.py +253 -0
- hanus/logger.py +81 -0
- hanus/memory/__init__.py +8 -0
- hanus/memory/manager.py +265 -0
- hanus/memory/types.py +119 -0
- hanus/monitor.py +341 -0
- hanus/parallel/__init__.py +5 -0
- hanus/parallel/executor.py +300 -0
- hanus/permissions.py +182 -0
- hanus/plan/__init__.py +8 -0
- hanus/plan/mode.py +267 -0
- hanus/plan/models.py +152 -0
- hanus/plugin_manager.py +754 -0
- hanus/plugin_registry.py +391 -0
- hanus/plugins/__init__.py +1 -0
- hanus/plugins/arena.py +630 -0
- hanus/plugins/code_review.py +123 -0
- hanus/plugins/cortex.py +1750 -0
- hanus/plugins/deps_check.py +27 -0
- hanus/plugins/git_ops.py +33 -0
- hanus/plugins/metasploit.py +530 -0
- hanus/plugins/notes.py +583 -0
- hanus/plugins/search_code.py +59 -0
- hanus/plugins/searchsploit.py +495 -0
- hanus/plugins/strategist.py +175 -0
- hanus/plugins/webui.py +5200 -0
- hanus/profiles.py +479 -0
- hanus/profiles_builtin/__init__.py +0 -0
- hanus/profiles_builtin/architect/profile.yaml +12 -0
- hanus/profiles_builtin/architect/system_prompt.txt +71 -0
- hanus/profiles_builtin/deep/profile.yaml +12 -0
- hanus/profiles_builtin/deep/system_prompt.txt +66 -0
- hanus/profiles_builtin/developer/__init__.py +0 -0
- hanus/profiles_builtin/developer/profile.yaml +9 -0
- hanus/profiles_builtin/developer/system_prompt.txt +176 -0
- hanus/profiles_builtin/speed/profile.yaml +12 -0
- hanus/profiles_builtin/speed/system_prompt.txt +51 -0
- hanus/project_tools.py +177 -0
- hanus/query_engine.py +1594 -0
- hanus/rules/__init__.py +237 -0
- hanus/search/__init__.py +5 -0
- hanus/search/semantic.py +596 -0
- hanus/session_manager.py +547 -0
- hanus/skill_manager.py +702 -0
- hanus/skills/__init__.py +4 -0
- hanus/subagent/__init__.py +8 -0
- hanus/subagent/agents/__init__.py +253 -0
- hanus/subagent/manager.py +309 -0
- hanus/subagent/types.py +266 -0
- hanus/suggestions/__init__.py +5 -0
- hanus/suggestions/proactive.py +451 -0
- hanus/tasks/__init__.py +8 -0
- hanus/tasks/manager.py +330 -0
- hanus/tasks/models.py +106 -0
- hanus/terminal_prompt.py +166 -0
- hanus/tools.py +1849 -0
- hanus/ui.py +939 -0
- hanuscode-1.0.0.dist-info/METADATA +1151 -0
- hanuscode-1.0.0.dist-info/RECORD +93 -0
- hanuscode-1.0.0.dist-info/WHEEL +5 -0
- hanuscode-1.0.0.dist-info/entry_points.txt +2 -0
- hanuscode-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# hanus/instincts/manager.py
|
|
2
|
+
"""
|
|
3
|
+
Gestor de instintos.
|
|
4
|
+
|
|
5
|
+
Maneja el almacenamiento, carga, y persistencia de instintos
|
|
6
|
+
con scope por proyecto y global.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional, Tuple
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
from hanus.instincts.types import (
|
|
16
|
+
Instinct, InstinctScope, InstinctStatus, InstinctDomain,
|
|
17
|
+
InstinctEvidence, InstinctGroup, ProjectInfo
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class InstinctManager:
|
|
22
|
+
"""
|
|
23
|
+
Gestiona el ciclo de vida de los instintos.
|
|
24
|
+
|
|
25
|
+
Los instintos se almacenan en:
|
|
26
|
+
- ~/.hanus/instincts/global/ - Instintos globales
|
|
27
|
+
- ~/.hanus/projects/<id>/instincts/ - Instintos del proyecto
|
|
28
|
+
|
|
29
|
+
Estructura de archivos:
|
|
30
|
+
- active/ - Instintos activos
|
|
31
|
+
- dormant/ - Instintos inactivos
|
|
32
|
+
- evolved/ - Instintos convertidos en skills
|
|
33
|
+
- inherited/ - Instintos importados
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, base_dir: Path = None, project_path: Path = None):
|
|
37
|
+
self.base_dir = base_dir or Path.home() / ".hanus"
|
|
38
|
+
self.project_path = project_path or Path.cwd()
|
|
39
|
+
|
|
40
|
+
# Directorios
|
|
41
|
+
self.global_dir = self.base_dir / "instincts" / "global"
|
|
42
|
+
self.projects_dir = self.base_dir / "projects"
|
|
43
|
+
|
|
44
|
+
# Crear directorios si no existen
|
|
45
|
+
self.global_dir.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
# Detectar proyecto
|
|
48
|
+
self.project_info = ProjectInfo.from_path(str(self.project_path))
|
|
49
|
+
self.project_dir = self.projects_dir / self.project_info.id
|
|
50
|
+
self.project_instincts_dir = self.project_dir / "instincts"
|
|
51
|
+
|
|
52
|
+
# Crear estructura de proyecto
|
|
53
|
+
self._ensure_project_structure()
|
|
54
|
+
|
|
55
|
+
# Cache de instintos
|
|
56
|
+
self._global_instincts: Dict[str, Instinct] = {}
|
|
57
|
+
self._project_instincts: Dict[str, Instinct] = {}
|
|
58
|
+
self._loaded = False
|
|
59
|
+
|
|
60
|
+
def _ensure_project_structure(self) -> None:
|
|
61
|
+
"""Asegura que la estructura de directorios existe."""
|
|
62
|
+
for subdir in ["active", "dormant", "evolved", "inherited"]:
|
|
63
|
+
(self.project_instincts_dir / subdir).mkdir(parents=True, exist_ok=True)
|
|
64
|
+
|
|
65
|
+
def load_all(self) -> Tuple[Dict[str, Instinct], Dict[str, Instinct]]:
|
|
66
|
+
"""Carga todos los instintos (globales y del proyecto)."""
|
|
67
|
+
self._global_instincts = self._load_instincts(self.global_dir)
|
|
68
|
+
self._project_instincts = self._load_instincts(self.project_instincts_dir)
|
|
69
|
+
self._loaded = True
|
|
70
|
+
return self._global_instincts, self._project_instincts
|
|
71
|
+
|
|
72
|
+
def _load_instincts(self, base_dir: Path) -> Dict[str, Instinct]:
|
|
73
|
+
"""Carga instintos desde un directorio."""
|
|
74
|
+
instincts = {}
|
|
75
|
+
|
|
76
|
+
for subdir in ["active", "inherited"]:
|
|
77
|
+
instinct_dir = base_dir / subdir
|
|
78
|
+
if not instinct_dir.exists():
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
for file_path in instinct_dir.glob("*.json"):
|
|
82
|
+
try:
|
|
83
|
+
content = file_path.read_text(encoding="utf-8")
|
|
84
|
+
data = json.loads(content)
|
|
85
|
+
instinct = Instinct.from_dict(data)
|
|
86
|
+
instincts[instinct.id] = instinct
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f"[InstinctManager] Error loading {file_path}: {e}")
|
|
89
|
+
|
|
90
|
+
return instincts
|
|
91
|
+
|
|
92
|
+
def get_all_instincts(self) -> Dict[str, Instinct]:
|
|
93
|
+
"""Obtiene todos los instintos aplicables (proyecto + global)."""
|
|
94
|
+
if not self._loaded:
|
|
95
|
+
self.load_all()
|
|
96
|
+
|
|
97
|
+
# Fusionar con precedencia: proyecto > global
|
|
98
|
+
all_instincts = {}
|
|
99
|
+
|
|
100
|
+
# Primero globales
|
|
101
|
+
for id_, instinct in self._global_instincts.items():
|
|
102
|
+
if instinct.status == InstinctStatus.ACTIVE:
|
|
103
|
+
all_instincts[id_] = instinct
|
|
104
|
+
|
|
105
|
+
# Luego proyecto (sobrescribe globales con mismo ID)
|
|
106
|
+
for id_, instinct in self._project_instincts.items():
|
|
107
|
+
if instinct.status == InstinctStatus.ACTIVE:
|
|
108
|
+
all_instincts[id_] = instinct
|
|
109
|
+
|
|
110
|
+
return all_instincts
|
|
111
|
+
|
|
112
|
+
def get_instinct(self, instinct_id: str) -> Optional[Instinct]:
|
|
113
|
+
"""Obtiene un instinto por ID."""
|
|
114
|
+
if not self._loaded:
|
|
115
|
+
self.load_all()
|
|
116
|
+
|
|
117
|
+
# Buscar primero en proyecto, luego en global
|
|
118
|
+
if instinct_id in self._project_instincts:
|
|
119
|
+
return self._project_instincts[instinct_id]
|
|
120
|
+
return self._global_instincts.get(instinct_id)
|
|
121
|
+
|
|
122
|
+
def add_instinct(self, instinct: Instinct) -> None:
|
|
123
|
+
"""Añade un nuevo instinto."""
|
|
124
|
+
if instinct.scope == InstinctScope.GLOBAL:
|
|
125
|
+
self._global_instincts[instinct.id] = instinct
|
|
126
|
+
self._save_instinct(instinct, self.global_dir)
|
|
127
|
+
else:
|
|
128
|
+
instinct.project_id = self.project_info.id
|
|
129
|
+
self._project_instincts[instinct.id] = instinct
|
|
130
|
+
self._save_instinct(instinct, self.project_instincts_dir)
|
|
131
|
+
|
|
132
|
+
def _save_instinct(self, instinct: Instinct, base_dir: Path) -> None:
|
|
133
|
+
"""Guarda un instinto en disco."""
|
|
134
|
+
subdir = "active" if instinct.status == InstinctStatus.ACTIVE else "dormant"
|
|
135
|
+
file_path = base_dir / subdir / f"{instinct.id}.json"
|
|
136
|
+
|
|
137
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
138
|
+
file_path.write_text(
|
|
139
|
+
json.dumps(instinct.to_dict(), indent=2, ensure_ascii=False),
|
|
140
|
+
encoding="utf-8"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def update_instinct(self, instinct: Instinct) -> None:
|
|
144
|
+
"""Actualiza un instinto existente."""
|
|
145
|
+
instinct.updated_at = datetime.now().isoformat()
|
|
146
|
+
|
|
147
|
+
if instinct.scope == InstinctScope.GLOBAL:
|
|
148
|
+
self._global_instincts[instinct.id] = instinct
|
|
149
|
+
self._save_instinct(instinct, self.global_dir)
|
|
150
|
+
else:
|
|
151
|
+
self._project_instincts[instinct.id] = instinct
|
|
152
|
+
self._save_instinct(instinct, self.project_instincts_dir)
|
|
153
|
+
|
|
154
|
+
def record_usage(self, instinct_id: str, success: bool,
|
|
155
|
+
evidence: Optional[InstinctEvidence] = None) -> None:
|
|
156
|
+
"""Registra el uso de un instinto."""
|
|
157
|
+
instinct = self.get_instinct(instinct_id)
|
|
158
|
+
if not instinct:
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
instinct.last_used = datetime.now().isoformat()
|
|
162
|
+
instinct.update_confidence(success)
|
|
163
|
+
|
|
164
|
+
if evidence:
|
|
165
|
+
instinct.evidence.append(evidence)
|
|
166
|
+
|
|
167
|
+
self.update_instinct(instinct)
|
|
168
|
+
|
|
169
|
+
def get_by_domain(self, domain: InstinctDomain) -> List[Instinct]:
|
|
170
|
+
"""Obtiene instintos por dominio."""
|
|
171
|
+
all_instincts = self.get_all_instincts()
|
|
172
|
+
return [i for i in all_instincts.values() if i.domain == domain]
|
|
173
|
+
|
|
174
|
+
def get_by_confidence(self, min_confidence: float = 0.7) -> List[Instinct]:
|
|
175
|
+
"""Obtiene instintos con confianza mínima."""
|
|
176
|
+
all_instincts = self.get_all_instincts()
|
|
177
|
+
return [i for i in all_instincts.values() if i.confidence >= min_confidence]
|
|
178
|
+
|
|
179
|
+
def group_by_domain(self) -> InstinctGroup:
|
|
180
|
+
"""Agrupa instintos por dominio."""
|
|
181
|
+
all_instincts = self.get_all_instincts()
|
|
182
|
+
groups: InstinctGroup = {}
|
|
183
|
+
|
|
184
|
+
for instinct in all_instincts.values():
|
|
185
|
+
domain = instinct.domain.value
|
|
186
|
+
if domain not in groups:
|
|
187
|
+
groups[domain] = []
|
|
188
|
+
groups[domain].append(instinct)
|
|
189
|
+
|
|
190
|
+
return groups
|
|
191
|
+
|
|
192
|
+
def search(self, query: str) -> List[Instinct]:
|
|
193
|
+
"""Busca instintos por nombre, descripción o tags."""
|
|
194
|
+
all_instincts = self.get_all_instincts()
|
|
195
|
+
query_lower = query.lower()
|
|
196
|
+
|
|
197
|
+
results = []
|
|
198
|
+
for instinct in all_instincts.values():
|
|
199
|
+
if (query_lower in instinct.name.lower() or
|
|
200
|
+
query_lower in instinct.description.lower() or
|
|
201
|
+
query_lower in instinct.trigger.lower() or
|
|
202
|
+
any(query_lower in tag.lower() for tag in instinct.tags)):
|
|
203
|
+
results.append(instinct)
|
|
204
|
+
|
|
205
|
+
return results
|
|
206
|
+
|
|
207
|
+
def export_instincts(self, scope: InstinctScope = None,
|
|
208
|
+
min_confidence: float = 0.5,
|
|
209
|
+
domains: List[InstinctDomain] = None) -> Dict:
|
|
210
|
+
"""Exporta instintos a formato portable."""
|
|
211
|
+
if not self._loaded:
|
|
212
|
+
self.load_all()
|
|
213
|
+
|
|
214
|
+
instincts_to_export = []
|
|
215
|
+
|
|
216
|
+
if scope == InstinctScope.GLOBAL:
|
|
217
|
+
instincts_to_export = list(self._global_instincts.values())
|
|
218
|
+
elif scope == InstinctScope.PROJECT:
|
|
219
|
+
instincts_to_export = list(self._project_instincts.values())
|
|
220
|
+
else:
|
|
221
|
+
instincts_to_export = list(self.get_all_instincts().values())
|
|
222
|
+
|
|
223
|
+
# Filtrar
|
|
224
|
+
filtered = [
|
|
225
|
+
i for i in instincts_to_export
|
|
226
|
+
if i.confidence >= min_confidence and
|
|
227
|
+
(domains is None or i.domain in domains)
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
"version": "1.0",
|
|
232
|
+
"exported_at": datetime.now().isoformat(),
|
|
233
|
+
"project": {
|
|
234
|
+
"id": self.project_info.id,
|
|
235
|
+
"name": self.project_info.name,
|
|
236
|
+
} if scope != InstinctScope.GLOBAL else None,
|
|
237
|
+
"instincts": [i.to_dict() for i in filtered],
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
def import_instincts(self, data: Dict, scope: InstinctScope,
|
|
241
|
+
force: bool = False,
|
|
242
|
+
min_confidence: float = 0.5) -> Tuple[int, List[str]]:
|
|
243
|
+
"""
|
|
244
|
+
Importa instintos desde un diccionario.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Tuple de (número importados, lista de mensajes)
|
|
248
|
+
"""
|
|
249
|
+
if not self._loaded:
|
|
250
|
+
self.load_all()
|
|
251
|
+
|
|
252
|
+
imported = 0
|
|
253
|
+
messages = []
|
|
254
|
+
|
|
255
|
+
for instinct_data in data.get("instincts", []):
|
|
256
|
+
try:
|
|
257
|
+
instinct = Instinct.from_dict(instinct_data)
|
|
258
|
+
|
|
259
|
+
# Filtrar por confianza
|
|
260
|
+
if instinct.confidence < min_confidence:
|
|
261
|
+
messages.append(f"Skipped {instinct.name}: confidence {instinct.confidence} < {min_confidence}")
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
# Verificar duplicados
|
|
265
|
+
existing = self.get_instinct(instinct.id)
|
|
266
|
+
if existing:
|
|
267
|
+
if existing.confidence >= instinct.confidence:
|
|
268
|
+
messages.append(f"Skipped {instinct.name}: existing has higher/equal confidence")
|
|
269
|
+
continue
|
|
270
|
+
if not force:
|
|
271
|
+
messages.append(f"Skipped {instinct.name}: duplicate (use --force)")
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
# Ajustar scope
|
|
275
|
+
instinct.scope = scope
|
|
276
|
+
if scope == InstinctScope.PROJECT:
|
|
277
|
+
instinct.project_id = self.project_info.id
|
|
278
|
+
|
|
279
|
+
# Guardar en inherited
|
|
280
|
+
target_dir = (self.project_instincts_dir if scope == InstinctScope.PROJECT
|
|
281
|
+
else self.global_dir) / "inherited"
|
|
282
|
+
file_path = target_dir / f"{instinct.id}.json"
|
|
283
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
284
|
+
file_path.write_text(
|
|
285
|
+
json.dumps(instinct.to_dict(), indent=2, ensure_ascii=False),
|
|
286
|
+
encoding="utf-8"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
imported += 1
|
|
290
|
+
messages.append(f"Imported: {instinct.name} ({instinct.confidence:.0%})")
|
|
291
|
+
|
|
292
|
+
except Exception as e:
|
|
293
|
+
messages.append(f"Error importing: {e}")
|
|
294
|
+
|
|
295
|
+
# Recargar
|
|
296
|
+
self._loaded = False
|
|
297
|
+
self.load_all()
|
|
298
|
+
|
|
299
|
+
return imported, messages
|
|
300
|
+
|
|
301
|
+
def get_stats(self) -> Dict:
|
|
302
|
+
"""Obtiene estadísticas de los instintos."""
|
|
303
|
+
if not self._loaded:
|
|
304
|
+
self.load_all()
|
|
305
|
+
|
|
306
|
+
global_count = len([i for i in self._global_instincts.values()
|
|
307
|
+
if i.status == InstinctStatus.ACTIVE])
|
|
308
|
+
project_count = len([i for i in self._project_instincts.values()
|
|
309
|
+
if i.status == InstinctStatus.ACTIVE])
|
|
310
|
+
|
|
311
|
+
# Por dominio
|
|
312
|
+
by_domain = {}
|
|
313
|
+
for instinct in self.get_all_instincts().values():
|
|
314
|
+
domain = instinct.domain.value
|
|
315
|
+
by_domain[domain] = by_domain.get(domain, 0) + 1
|
|
316
|
+
|
|
317
|
+
# Por confianza
|
|
318
|
+
high_confidence = len([i for i in self.get_all_instincts().values()
|
|
319
|
+
if i.confidence >= 0.8])
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
"total": global_count + project_count,
|
|
323
|
+
"global": global_count,
|
|
324
|
+
"project": project_count,
|
|
325
|
+
"by_domain": by_domain,
|
|
326
|
+
"high_confidence": high_confidence,
|
|
327
|
+
"project_info": {
|
|
328
|
+
"id": self.project_info.id,
|
|
329
|
+
"name": self.project_info.name,
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# Instancia global
|
|
335
|
+
_manager: Optional[InstinctManager] = None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def get_instinct_manager(project_path: Path = None) -> InstinctManager:
|
|
339
|
+
"""Obtiene el gestor de instintos."""
|
|
340
|
+
global _manager
|
|
341
|
+
if _manager is None or (project_path and _manager.project_path != project_path):
|
|
342
|
+
_manager = InstinctManager(project_path=project_path)
|
|
343
|
+
return _manager
|
hanus/instincts/types.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# hanus/instincts/types.py
|
|
2
|
+
"""
|
|
3
|
+
Tipos de datos para el sistema de instintos.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import List, Dict, Optional, Any
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InstinctScope(Enum):
|
|
15
|
+
"""Alcance del instinto."""
|
|
16
|
+
PROJECT = "project" # Solo para el proyecto actual
|
|
17
|
+
GLOBAL = "global" # Aplica a todos los proyectos
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InstinctStatus(Enum):
|
|
21
|
+
"""Estado del instinto."""
|
|
22
|
+
ACTIVE = "active" # Activo y siendo usado
|
|
23
|
+
DORMANT = "dormant" # No usado recientemente
|
|
24
|
+
EVOLVED = "evolved" # Convertido en skill/comando/agente
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class InstinctDomain(Enum):
|
|
28
|
+
"""Dominios de conocimiento."""
|
|
29
|
+
WORKFLOW = "workflow" # Flujos de trabajo
|
|
30
|
+
SECURITY = "security" # Seguridad
|
|
31
|
+
TESTING = "testing" # Testing
|
|
32
|
+
PERFORMANCE = "performance" # Performance
|
|
33
|
+
PATTERNS = "patterns" # Patrones de código
|
|
34
|
+
ARCHITECTURE = "architecture" # Arquitectura
|
|
35
|
+
DEBUGGING = "debugging" # Debugging
|
|
36
|
+
REFACTORING = "refactoring" # Refactoring
|
|
37
|
+
DOCUMENTATION = "documentation" # Documentación
|
|
38
|
+
GIT = "git" # Git y versionado
|
|
39
|
+
DATABASE = "database" # Base de datos
|
|
40
|
+
API = "api" # APIs
|
|
41
|
+
UI = "ui" # UI/Frontend
|
|
42
|
+
GENERAL = "general" # General
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class InstinctEvidence:
|
|
47
|
+
"""Evidencia que respalda un instinto."""
|
|
48
|
+
timestamp: str
|
|
49
|
+
action: str # Acción realizada
|
|
50
|
+
context: str # Contexto en el que ocurrió
|
|
51
|
+
outcome: str # Resultado (success/failure)
|
|
52
|
+
file_path: Optional[str] = None
|
|
53
|
+
snippet: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> Dict:
|
|
56
|
+
return {
|
|
57
|
+
"timestamp": self.timestamp,
|
|
58
|
+
"action": self.action,
|
|
59
|
+
"context": self.context,
|
|
60
|
+
"outcome": self.outcome,
|
|
61
|
+
"file_path": self.file_path,
|
|
62
|
+
"snippet": self.snippet,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_dict(cls, data: Dict) -> 'InstinctEvidence':
|
|
67
|
+
return cls(
|
|
68
|
+
timestamp=data["timestamp"],
|
|
69
|
+
action=data["action"],
|
|
70
|
+
context=data["context"],
|
|
71
|
+
outcome=data["outcome"],
|
|
72
|
+
file_path=data.get("file_path"),
|
|
73
|
+
snippet=data.get("snippet"),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class Instinct:
|
|
79
|
+
"""
|
|
80
|
+
Un instinto es un comportamiento aprendido.
|
|
81
|
+
|
|
82
|
+
Es atómico, tiene un score de confianza (0.3-0.9), está
|
|
83
|
+
etiquetado por dominio, respaldado por evidencia, y
|
|
84
|
+
tiene scope (proyecto o global).
|
|
85
|
+
"""
|
|
86
|
+
id: str # ID único
|
|
87
|
+
name: str # Nombre descriptivo
|
|
88
|
+
description: str # Descripción del comportamiento
|
|
89
|
+
trigger: str # Cuándo aplicar
|
|
90
|
+
action: str # Qué hacer
|
|
91
|
+
domain: InstinctDomain # Dominio de conocimiento
|
|
92
|
+
scope: InstinctScope # Proyecto o global
|
|
93
|
+
confidence: float # 0.3 - 0.9
|
|
94
|
+
evidence: List[InstinctEvidence] = field(default_factory=list)
|
|
95
|
+
observations: int = 0 # Número de veces observado
|
|
96
|
+
successes: int = 0 # Número de éxitos
|
|
97
|
+
failures: int = 0 # Número de fallos
|
|
98
|
+
created_at: str = ""
|
|
99
|
+
updated_at: str = ""
|
|
100
|
+
last_used: str = ""
|
|
101
|
+
project_id: Optional[str] = None # ID del proyecto (si scope=project)
|
|
102
|
+
evolved_to: Optional[str] = None # ID del skill/comando/agente evolucionado
|
|
103
|
+
status: InstinctStatus = InstinctStatus.ACTIVE
|
|
104
|
+
tags: List[str] = field(default_factory=list)
|
|
105
|
+
|
|
106
|
+
def __post_init__(self):
|
|
107
|
+
if not self.created_at:
|
|
108
|
+
self.created_at = datetime.now().isoformat()
|
|
109
|
+
if not self.updated_at:
|
|
110
|
+
self.updated_at = self.created_at
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def success_rate(self) -> float:
|
|
114
|
+
"""Calcula la tasa de éxito."""
|
|
115
|
+
total = self.successes + self.failures
|
|
116
|
+
if total == 0:
|
|
117
|
+
return 0.5
|
|
118
|
+
return self.successes / total
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def confidence_bar(self) -> str:
|
|
122
|
+
"""Barra de confianza visual."""
|
|
123
|
+
filled = int(self.confidence * 10)
|
|
124
|
+
empty = 10 - filled
|
|
125
|
+
return "█" * filled + "░" * empty
|
|
126
|
+
|
|
127
|
+
def update_confidence(self, success: bool) -> None:
|
|
128
|
+
"""Actualiza la confianza basada en un resultado."""
|
|
129
|
+
# Factor de aprendizaje (más lento conforme más observaciones)
|
|
130
|
+
alpha = 1.0 / (self.observations + 1)
|
|
131
|
+
|
|
132
|
+
if success:
|
|
133
|
+
# Aumentar confianza, pero nunca más de 0.95
|
|
134
|
+
self.confidence = min(0.95, self.confidence + alpha * (1 - self.confidence) * 0.5)
|
|
135
|
+
self.successes += 1
|
|
136
|
+
else:
|
|
137
|
+
# Disminuir confianza, pero nunca menos de 0.3
|
|
138
|
+
self.confidence = max(0.3, self.confidence - alpha * self.confidence * 0.3)
|
|
139
|
+
self.failures += 1
|
|
140
|
+
|
|
141
|
+
self.observations += 1
|
|
142
|
+
self.updated_at = datetime.now().isoformat()
|
|
143
|
+
|
|
144
|
+
def add_evidence(self, evidence: InstinctEvidence) -> None:
|
|
145
|
+
"""Añade evidencia al instinto."""
|
|
146
|
+
self.evidence.append(evidence)
|
|
147
|
+
self.update_confidence(evidence.outcome == "success")
|
|
148
|
+
|
|
149
|
+
def to_dict(self) -> Dict:
|
|
150
|
+
"""Convierte a diccionario para serialización."""
|
|
151
|
+
return {
|
|
152
|
+
"id": self.id,
|
|
153
|
+
"name": self.name,
|
|
154
|
+
"description": self.description,
|
|
155
|
+
"trigger": self.trigger,
|
|
156
|
+
"action": self.action,
|
|
157
|
+
"domain": self.domain.value,
|
|
158
|
+
"scope": self.scope.value,
|
|
159
|
+
"confidence": self.confidence,
|
|
160
|
+
"evidence": [e.to_dict() for e in self.evidence],
|
|
161
|
+
"observations": self.observations,
|
|
162
|
+
"successes": self.successes,
|
|
163
|
+
"failures": self.failures,
|
|
164
|
+
"created_at": self.created_at,
|
|
165
|
+
"updated_at": self.updated_at,
|
|
166
|
+
"last_used": self.last_used,
|
|
167
|
+
"project_id": self.project_id,
|
|
168
|
+
"evolved_to": self.evolved_to,
|
|
169
|
+
"status": self.status.value,
|
|
170
|
+
"tags": self.tags,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def from_dict(cls, data: Dict) -> 'Instinct':
|
|
175
|
+
"""Crea desde diccionario."""
|
|
176
|
+
return cls(
|
|
177
|
+
id=data["id"],
|
|
178
|
+
name=data["name"],
|
|
179
|
+
description=data["description"],
|
|
180
|
+
trigger=data["trigger"],
|
|
181
|
+
action=data["action"],
|
|
182
|
+
domain=InstinctDomain(data["domain"]),
|
|
183
|
+
scope=InstinctScope(data["scope"]),
|
|
184
|
+
confidence=data["confidence"],
|
|
185
|
+
evidence=[InstinctEvidence.from_dict(e) for e in data.get("evidence", [])],
|
|
186
|
+
observations=data.get("observations", 0),
|
|
187
|
+
successes=data.get("successes", 0),
|
|
188
|
+
failures=data.get("failures", 0),
|
|
189
|
+
created_at=data.get("created_at", ""),
|
|
190
|
+
updated_at=data.get("updated_at", ""),
|
|
191
|
+
last_used=data.get("last_used", ""),
|
|
192
|
+
project_id=data.get("project_id"),
|
|
193
|
+
evolved_to=data.get("evolved_to"),
|
|
194
|
+
status=InstinctStatus(data.get("status", "active")),
|
|
195
|
+
tags=data.get("tags", []),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def generate_id(cls, name: str, trigger: str) -> str:
|
|
200
|
+
"""Genera un ID único para un instinto."""
|
|
201
|
+
content = f"{name}:{trigger}"
|
|
202
|
+
return hashlib.md5(content.encode()).hexdigest()[:12]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass
|
|
206
|
+
class ProjectInfo:
|
|
207
|
+
"""Información del proyecto actual."""
|
|
208
|
+
id: str
|
|
209
|
+
name: str
|
|
210
|
+
path: str
|
|
211
|
+
remote_url: Optional[str] = None
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def from_path(cls, path: str) -> 'ProjectInfo':
|
|
215
|
+
"""Detecta información del proyecto desde el path."""
|
|
216
|
+
import os
|
|
217
|
+
import subprocess
|
|
218
|
+
|
|
219
|
+
# Intentar obtener git remote
|
|
220
|
+
remote_url = None
|
|
221
|
+
try:
|
|
222
|
+
result = subprocess.run(
|
|
223
|
+
["git", "remote", "get-url", "origin"],
|
|
224
|
+
cwd=path,
|
|
225
|
+
capture_output=True,
|
|
226
|
+
text=True,
|
|
227
|
+
timeout=5
|
|
228
|
+
)
|
|
229
|
+
if result.returncode == 0:
|
|
230
|
+
remote_url = result.stdout.strip()
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
# Generar ID único
|
|
235
|
+
if remote_url:
|
|
236
|
+
project_id = hashlib.md5(remote_url.encode()).hexdigest()[:12]
|
|
237
|
+
else:
|
|
238
|
+
# Usar path como fallback
|
|
239
|
+
project_id = hashlib.md5(path.encode()).hexdigest()[:12]
|
|
240
|
+
|
|
241
|
+
# Nombre del directorio
|
|
242
|
+
name = os.path.basename(path)
|
|
243
|
+
|
|
244
|
+
return cls(
|
|
245
|
+
id=project_id,
|
|
246
|
+
name=name,
|
|
247
|
+
path=path,
|
|
248
|
+
remote_url=remote_url,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# Type alias para instintos agrupados
|
|
253
|
+
InstinctGroup = Dict[str, List[Instinct]]
|
hanus/logger.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# hanus/logger.py — Sistema de logs centralizado
|
|
2
|
+
"""
|
|
3
|
+
Escribe logs a ~/.hanus/logs/hanus_YYYYMMDD.log
|
|
4
|
+
Niveles: DEBUG, INFO, WARN, ERROR
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
LOG_DIR = Path.home() / ".hanus" / "logs"
|
|
13
|
+
|
|
14
|
+
_logger: Optional[logging.Logger] = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def setup(verbose: bool = False) -> logging.Logger:
|
|
18
|
+
global _logger
|
|
19
|
+
if _logger:
|
|
20
|
+
return _logger
|
|
21
|
+
|
|
22
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
log_file = LOG_DIR / f"hanus_{time.strftime('%Y%m%d')}.log"
|
|
24
|
+
|
|
25
|
+
fmt = logging.Formatter(
|
|
26
|
+
"%(asctime)s %(levelname)-7s %(name)s %(message)s",
|
|
27
|
+
datefmt="%H:%M:%S",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
fh = logging.FileHandler(log_file, encoding="utf-8")
|
|
31
|
+
fh.setLevel(logging.DEBUG)
|
|
32
|
+
fh.setFormatter(fmt)
|
|
33
|
+
|
|
34
|
+
_logger = logging.getLogger("hanus")
|
|
35
|
+
_logger.setLevel(logging.DEBUG if verbose else logging.INFO)
|
|
36
|
+
_logger.addHandler(fh)
|
|
37
|
+
|
|
38
|
+
# No propagar al root logger (evita doble salida)
|
|
39
|
+
_logger.propagate = False
|
|
40
|
+
|
|
41
|
+
return _logger
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get() -> logging.Logger:
|
|
45
|
+
global _logger
|
|
46
|
+
if _logger is None:
|
|
47
|
+
_logger = setup()
|
|
48
|
+
return _logger
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Shortcuts
|
|
52
|
+
def debug(msg: str, **kw): get().debug(msg, **kw)
|
|
53
|
+
def info(msg: str, **kw): get().info(msg, **kw)
|
|
54
|
+
def warn(msg: str, **kw): get().warning(msg, **kw)
|
|
55
|
+
def error(msg: str, **kw): get().error(msg, **kw)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def log_request(provider: str, model: str, n_messages: int, attempt: int = 0):
|
|
59
|
+
get().info(f"REQUEST {provider}/{model} msgs={n_messages} attempt={attempt}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def log_response(provider: str, model: str, tokens_in: int, tokens_out: int,
|
|
63
|
+
cost: float, elapsed: float, stop_reason: str):
|
|
64
|
+
get().info(
|
|
65
|
+
f"RESPONSE {provider}/{model} "
|
|
66
|
+
f"in={tokens_in} out={tokens_out} cost=${cost:.5f} "
|
|
67
|
+
f"elapsed={elapsed:.1f}s stop={stop_reason}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def log_tool(tool: str, args: dict, success: bool, elapsed: float):
|
|
72
|
+
status = "OK" if success else "FAIL"
|
|
73
|
+
get().info(f"TOOL {tool:<20} {status} elapsed={elapsed:.2f}s args={str(args)[:120]}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def log_plugin(name: str, args: str, elapsed: float):
|
|
77
|
+
get().info(f"PLUGIN {name:<20} elapsed={elapsed:.2f}s args={args[:80]}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def log_error(ctx: str, exc: Exception):
|
|
81
|
+
get().error(f"ERROR {ctx}: {type(exc).__name__}: {exc}")
|
hanus/memory/__init__.py
ADDED