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
hanus/config.py
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
# config.py
|
|
2
|
+
"""
|
|
3
|
+
Configuración de Hanus. Carga desde:
|
|
4
|
+
1. ~/.hanus/config.yaml (global)
|
|
5
|
+
2. .hanus.yaml (local, en la raíz del proyecto)
|
|
6
|
+
3. Variables de entorno (prioridad máxima)
|
|
7
|
+
|
|
8
|
+
También puede usar hanus.yaml en el directorio del paquete como plantilla.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
import os
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, Any, Optional
|
|
15
|
+
|
|
16
|
+
CONFIG_DIR = Path.home() / ".hanus"
|
|
17
|
+
GLOBAL_CONFIG = CONFIG_DIR / "config.yaml"
|
|
18
|
+
PACKAGE_CONFIG = Path(__file__).parent.parent / "hanus.yaml" # hanus.yaml in package root
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _yaml(path: Path) -> Dict:
|
|
22
|
+
if not path.exists():
|
|
23
|
+
return {}
|
|
24
|
+
try:
|
|
25
|
+
import yaml
|
|
26
|
+
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
27
|
+
except ImportError:
|
|
28
|
+
# Parse mínimo sin PyYAML
|
|
29
|
+
out = {}
|
|
30
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
31
|
+
line = line.strip()
|
|
32
|
+
if line and not line.startswith("#") and ":" in line:
|
|
33
|
+
k, _, v = line.partition(":")
|
|
34
|
+
out[k.strip()] = v.strip().strip('"\'')
|
|
35
|
+
return out
|
|
36
|
+
except Exception:
|
|
37
|
+
return {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class HanusConfig:
|
|
42
|
+
# ── Modelo ────────────────────────────────────────────────────────────────
|
|
43
|
+
provider: str = "ollama" # claude|openai|gemini|ollama|glm
|
|
44
|
+
model_id: str = "llama3" # Modelo por defecto (GLM via Ollama)
|
|
45
|
+
max_tokens: int = 8096
|
|
46
|
+
|
|
47
|
+
# ── API Keys ──────────────────────────────────────────────────────────────
|
|
48
|
+
anthropic_api_key: str = ""
|
|
49
|
+
openai_api_key: str = ""
|
|
50
|
+
gemini_api_key: str = ""
|
|
51
|
+
glm_api_key: str = "" # API key para GLM Cloud (ZhipuAI)
|
|
52
|
+
ollama_url: str = "http://localhost:11434"
|
|
53
|
+
|
|
54
|
+
# ── Permisos ──────────────────────────────────────────────────────────────
|
|
55
|
+
permission_mode: str = "bypass" # default | plan | bypass
|
|
56
|
+
max_tool_calls: int = 2000
|
|
57
|
+
|
|
58
|
+
# ── Proyecto ──────────────────────────────────────────────────────────────
|
|
59
|
+
root_dir: Path = field(default_factory=Path.cwd)
|
|
60
|
+
plugins_dir: Path = field(default_factory=lambda: Path.cwd() / "plugins")
|
|
61
|
+
prompt_file: Path = field(default_factory=lambda: CONFIG_DIR / "system_prompt.txt")
|
|
62
|
+
|
|
63
|
+
# ── Contexto ──────────────────────────────────────────────────────────────
|
|
64
|
+
context_max_files: int = 50
|
|
65
|
+
context_include_content: bool = True
|
|
66
|
+
context_preview_chars: int = 3000
|
|
67
|
+
max_file_read_chars: int = 100_000
|
|
68
|
+
|
|
69
|
+
# ── Ejecución ─────────────────────────────────────────────────────────────
|
|
70
|
+
shell_timeout: int = 60
|
|
71
|
+
python_exec_timeout: int = 120
|
|
72
|
+
|
|
73
|
+
# ── Presupuesto ───────────────────────────────────────────────────────────
|
|
74
|
+
budget_usd: float = 0.0 # 0 = sin límite
|
|
75
|
+
budget_warn_pct: float = 0.8
|
|
76
|
+
|
|
77
|
+
# ── Sesiones ──────────────────────────────────────────────────────────────
|
|
78
|
+
auto_save_session: bool = True
|
|
79
|
+
|
|
80
|
+
# ── Contexto ────────────────────────────────────────────────────────────────
|
|
81
|
+
context_window: int = 200000 # Ventana de contexto del modelo (tokens)
|
|
82
|
+
context_compress_threshold: float = 0.75 # Comprimir al 75% del límite
|
|
83
|
+
|
|
84
|
+
# ── UI ──────────────────────────────────────────────────────────────────────
|
|
85
|
+
show_cost: bool = True
|
|
86
|
+
verbose: bool = False
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def load(cls, project_dir: Optional[Path] = None) -> "HanusConfig":
|
|
90
|
+
cfg = cls()
|
|
91
|
+
cfg.root_dir = project_dir or Path.cwd()
|
|
92
|
+
cfg.plugins_dir = cfg.root_dir / "plugins"
|
|
93
|
+
|
|
94
|
+
# Load order: package default -> global -> local -> env
|
|
95
|
+
cfg._apply(_yaml(PACKAGE_CONFIG)) # Package defaults (hanus.yaml)
|
|
96
|
+
cfg._apply(_yaml(GLOBAL_CONFIG)) # User global config
|
|
97
|
+
cfg._apply(_yaml(cfg.root_dir / ".hanus.yaml")) # Project local config
|
|
98
|
+
cfg._apply_env() # Environment variables (highest priority)
|
|
99
|
+
return cfg
|
|
100
|
+
|
|
101
|
+
def _apply(self, data: Dict):
|
|
102
|
+
for k, v in data.items():
|
|
103
|
+
k = k.lower().replace("-", "_")
|
|
104
|
+
if not hasattr(self, k):
|
|
105
|
+
continue
|
|
106
|
+
cur = getattr(self, k)
|
|
107
|
+
if isinstance(cur, Path):
|
|
108
|
+
setattr(self, k, Path(str(v)).expanduser())
|
|
109
|
+
elif isinstance(cur, bool):
|
|
110
|
+
setattr(self, k, str(v).lower() in ("true", "1", "yes"))
|
|
111
|
+
elif isinstance(cur, int):
|
|
112
|
+
setattr(self, k, int(v))
|
|
113
|
+
elif isinstance(cur, float):
|
|
114
|
+
setattr(self, k, float(v))
|
|
115
|
+
else:
|
|
116
|
+
setattr(self, k, str(v))
|
|
117
|
+
|
|
118
|
+
def _apply_env(self):
|
|
119
|
+
ENV = {
|
|
120
|
+
"ANTHROPIC_API_KEY": "anthropic_api_key",
|
|
121
|
+
"OPENAI_API_KEY": "openai_api_key",
|
|
122
|
+
"GEMINI_API_KEY": "gemini_api_key",
|
|
123
|
+
"OLLAMA_URL": "ollama_url",
|
|
124
|
+
"HANUS_PROVIDER": "provider",
|
|
125
|
+
"HANUS_MODEL": "model_id",
|
|
126
|
+
"HANUS_MODE": "permission_mode",
|
|
127
|
+
"HANUS_BUDGET": "budget_usd",
|
|
128
|
+
"HANUS_VERBOSE": "verbose",
|
|
129
|
+
}
|
|
130
|
+
for env_key, attr in ENV.items():
|
|
131
|
+
v = os.environ.get(env_key)
|
|
132
|
+
if v:
|
|
133
|
+
self._apply({attr: v})
|
|
134
|
+
|
|
135
|
+
def get_connector_config(self) -> Dict[str, Any]:
|
|
136
|
+
base: Dict[str, Any] = {"model_id": self.model_id, "max_tokens": self.max_tokens}
|
|
137
|
+
p = self.provider.lower()
|
|
138
|
+
if p == "claude":
|
|
139
|
+
base["api_key"] = self.anthropic_api_key
|
|
140
|
+
elif p == "openai":
|
|
141
|
+
base["api_key"] = self.openai_api_key
|
|
142
|
+
elif p == "gemini":
|
|
143
|
+
base["api_key"] = self.gemini_api_key
|
|
144
|
+
elif p == "ollama":
|
|
145
|
+
base["ollama_url"] = self.ollama_url
|
|
146
|
+
elif p == "glm":
|
|
147
|
+
base["api_key"] = self.glm_api_key
|
|
148
|
+
return base
|
|
149
|
+
|
|
150
|
+
def load_system_prompt(self, plugin_docs: str = "") -> str:
|
|
151
|
+
base = _DEFAULT_PROMPT
|
|
152
|
+
if self.prompt_file.exists():
|
|
153
|
+
try:
|
|
154
|
+
base = self.prompt_file.read_text(encoding="utf-8")
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
if plugin_docs:
|
|
158
|
+
base += f"\n\n## Available Plugins\n\n{plugin_docs}"
|
|
159
|
+
return base
|
|
160
|
+
|
|
161
|
+
def save_global(self):
|
|
162
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
GLOBAL_CONFIG.write_text(
|
|
164
|
+
f"provider: {self.provider}\n"
|
|
165
|
+
f"model_id: {self.model_id}\n"
|
|
166
|
+
f"permission_mode: {self.permission_mode}\n"
|
|
167
|
+
f"budget_usd: {self.budget_usd}\n"
|
|
168
|
+
f"show_cost: {str(self.show_cost).lower()}\n",
|
|
169
|
+
encoding="utf-8",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def export_full(self, path: Optional[Path] = None) -> Path:
|
|
173
|
+
"""
|
|
174
|
+
Export full configuration to a YAML file.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
path: Target path (default: GLOBAL_CONFIG)
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Path to the exported config file
|
|
181
|
+
"""
|
|
182
|
+
from datetime import datetime
|
|
183
|
+
|
|
184
|
+
target = path or GLOBAL_CONFIG
|
|
185
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
|
|
187
|
+
content = f"""# ═══════════════════════════════════════════════════════════════════════════════
|
|
188
|
+
# HANUSCODE CONFIGURATION
|
|
189
|
+
# Generated: {datetime.now().isoformat()}
|
|
190
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
191
|
+
|
|
192
|
+
# ─── AI MODEL ──────────────────────────────────────────────────────────────────
|
|
193
|
+
provider: {self.provider}
|
|
194
|
+
model_id: {self.model_id}
|
|
195
|
+
max_tokens: {self.max_tokens}
|
|
196
|
+
context_window: {self.context_window}
|
|
197
|
+
|
|
198
|
+
# ─── API KEYS ──────────────────────────────────────────────────────────────────
|
|
199
|
+
# Set via environment variables for security: ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.
|
|
200
|
+
# anthropic_api_key: ""
|
|
201
|
+
# openai_api_key: ""
|
|
202
|
+
# gemini_api_key: ""
|
|
203
|
+
# glm_api_key: ""
|
|
204
|
+
ollama_url: "{self.ollama_url}"
|
|
205
|
+
|
|
206
|
+
# ─── PERMISSIONS ───────────────────────────────────────────────────────────────
|
|
207
|
+
permission_mode: {self.permission_mode}
|
|
208
|
+
max_tool_calls: {self.max_tool_calls}
|
|
209
|
+
|
|
210
|
+
# ─── CONTEXT ───────────────────────────────────────────────────────────────────
|
|
211
|
+
context_max_files: {self.context_max_files}
|
|
212
|
+
context_include_content: {str(self.context_include_content).lower()}
|
|
213
|
+
context_preview_chars: {self.context_preview_chars}
|
|
214
|
+
max_file_read_chars: {self.max_file_read_chars}
|
|
215
|
+
context_compress_threshold: {self.context_compress_threshold}
|
|
216
|
+
|
|
217
|
+
# ─── EXECUTION ─────────────────────────────────────────────────────────────────
|
|
218
|
+
shell_timeout: {self.shell_timeout}
|
|
219
|
+
python_exec_timeout: {self.python_exec_timeout}
|
|
220
|
+
|
|
221
|
+
# ─── BUDGET ────────────────────────────────────────────────────────────────────
|
|
222
|
+
budget_usd: {self.budget_usd}
|
|
223
|
+
budget_warn_pct: {self.budget_warn_pct}
|
|
224
|
+
|
|
225
|
+
# ─── SESSIONS ──────────────────────────────────────────────────────────────────
|
|
226
|
+
auto_save_session: {str(self.auto_save_session).lower()}
|
|
227
|
+
|
|
228
|
+
# ─── UI ────────────────────────────────────────────────────────────────────────
|
|
229
|
+
show_cost: {str(self.show_cost).lower()}
|
|
230
|
+
verbose: {str(self.verbose).lower()}
|
|
231
|
+
"""
|
|
232
|
+
target.write_text(content, encoding="utf-8")
|
|
233
|
+
return target
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def generate_default_config() -> str:
|
|
237
|
+
"""Generate default configuration file content."""
|
|
238
|
+
return (Path(__file__).parent.parent / "hanus.yaml").read_text(encoding="utf-8")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
_DEFAULT_PROMPT = """You are Hanus Code, an autonomous senior software engineer. You complete tasks from start to finish without stopping.
|
|
242
|
+
|
|
243
|
+
## CORE IDENTITY
|
|
244
|
+
|
|
245
|
+
You are a professional software engineer who:
|
|
246
|
+
- Writes clean, maintainable, production-ready code
|
|
247
|
+
- Follows best practices and established patterns
|
|
248
|
+
- Verifies work before completion
|
|
249
|
+
- Never stops mid-task
|
|
250
|
+
|
|
251
|
+
## AVAILABLE TOOLS
|
|
252
|
+
|
|
253
|
+
### File Operations
|
|
254
|
+
- <read_file path="file.py"/> — Read file contents
|
|
255
|
+
- <write_file path="file.py">content</write_file> — Create/overwrite file
|
|
256
|
+
- <create_file path="file.py">content</create_file> — Create new file
|
|
257
|
+
- <edit_file path="file.py" old="text" new="text"/> — Edit file by replacing text
|
|
258
|
+
- <append_to_file path="file.py">content</append_to_file> — Append to file
|
|
259
|
+
- <glob_search pattern="**/*.py"/> — Find files matching pattern
|
|
260
|
+
- <grep_search pattern="def func" dir="src" regex="true"/> — Search text in files
|
|
261
|
+
- <list_files dir="."/> — List directory contents
|
|
262
|
+
|
|
263
|
+
### Execution
|
|
264
|
+
- <exec_cmd cmd="python script.py"/> — Run shell command (simple)
|
|
265
|
+
- <exec_cmd>multiline command with pipes | and quotes 'value'</exec_cmd> — Block format
|
|
266
|
+
- <exec_file path="script.py"/> — Execute Python file
|
|
267
|
+
|
|
268
|
+
### Git Operations
|
|
269
|
+
- <git_status/> — Show working tree status
|
|
270
|
+
- <git_diff/> — Show uncommitted changes
|
|
271
|
+
- <git_log limit="10"/> — Show commit history
|
|
272
|
+
- <git_commit message="message"/> — Create commit
|
|
273
|
+
- <git_push branch="main"/> — Push to remote
|
|
274
|
+
- <git_reset mode="hard" target="HEAD~1"/> — Reset commits
|
|
275
|
+
- <git_branch name="feature"/> — Create/list branches
|
|
276
|
+
|
|
277
|
+
### Web & Research
|
|
278
|
+
- <web_fetch url="https://..." prompt="extract info"/> — Fetch and parse web page
|
|
279
|
+
- <web_search query="python async best practices"/> — Search the web
|
|
280
|
+
|
|
281
|
+
### Task Management
|
|
282
|
+
- <task_create subject="Task name" description="Details"/> — Create task
|
|
283
|
+
- <task_update taskId="1" status="in_progress"/> — Update task status
|
|
284
|
+
- <task_list/> — List all tasks
|
|
285
|
+
- <task_get taskId="1"/> — Get task details
|
|
286
|
+
|
|
287
|
+
### Memory System (Persistent Context)
|
|
288
|
+
- <memory_save name="project-context">Important info to remember</memory_save> — Save memory
|
|
289
|
+
- <memory_search query="authentication"/> — Search memories
|
|
290
|
+
- <memory_list/> — List all memories
|
|
291
|
+
|
|
292
|
+
### Planning Mode
|
|
293
|
+
- <plan_create name="plan-name" description="What to accomplish"/> — Create plan
|
|
294
|
+
- <plan_add_step planId="1" description="Step details"/> — Add step to plan
|
|
295
|
+
- <plan_update_step planId="1" stepId="1" status="completed"/> — Update step
|
|
296
|
+
- <plan_list/> — List plans
|
|
297
|
+
- <plan_get planId="1"/> — Get plan details
|
|
298
|
+
- <plan_approve planId="1"/> — Approve plan
|
|
299
|
+
- <plan_reject planId="1"/> — Reject plan
|
|
300
|
+
|
|
301
|
+
### Subagents (Parallel Tasks)
|
|
302
|
+
- <subagent type="Explore" prompt="Find all API endpoints"/> — Spawn specialized agent
|
|
303
|
+
Types: Explore, general-purpose, Plan
|
|
304
|
+
- Use for parallel/async work
|
|
305
|
+
|
|
306
|
+
### Notebook Editing
|
|
307
|
+
- <notebook_edit path="notebook.ipynb" cell_id="0" new_source="code"/> — Edit Jupyter cell
|
|
308
|
+
|
|
309
|
+
### User Interaction
|
|
310
|
+
- <ask_user question="Which approach?" header="Choice" options='["A","B"]'/> — Ask user
|
|
311
|
+
|
|
312
|
+
### Binary Analysis
|
|
313
|
+
- <binsmasher path="binary" mode="strings"/> — Analyze binary files
|
|
314
|
+
|
|
315
|
+
## PLUGINS
|
|
316
|
+
|
|
317
|
+
Plugins extend your capabilities. Available plugins are listed in the system prompt.
|
|
318
|
+
|
|
319
|
+
Invoke plugins with: <run_plugin name="plugin_name" args="arguments"/>
|
|
320
|
+
|
|
321
|
+
Example plugins:
|
|
322
|
+
- <run_plugin name="notes" args="new \"My Note\" \"Content #tag\""/> — Create knowledge note
|
|
323
|
+
- <run_plugin name="git_ops" args="status"/> — Git operations
|
|
324
|
+
- <run_plugin name="search_code" args="pattern"/> — Advanced code search
|
|
325
|
+
|
|
326
|
+
Check the "Plugins" section in your system prompt for available plugins and their usage.
|
|
327
|
+
|
|
328
|
+
## CRITICAL RULES
|
|
329
|
+
|
|
330
|
+
**1. NEVER WRITE CODE AS TEXT**
|
|
331
|
+
NEVER write code in ``` blocks or raw text. ALWAYS use write_file or edit_file.
|
|
332
|
+
WRONG: int main() { return 0; }
|
|
333
|
+
RIGHT: <write_file path="main.c">int main() { return 0; }</write_file>
|
|
334
|
+
|
|
335
|
+
**2. READ BEFORE MODIFY**
|
|
336
|
+
Use read_file and grep_search to understand code before changing it.
|
|
337
|
+
<read_file path="src/auth.py"/>
|
|
338
|
+
<grep_search pattern="function" dir="src"/>
|
|
339
|
+
|
|
340
|
+
**3. ONE FILE AT A TIME**
|
|
341
|
+
Start and COMPLETE one file before starting another.
|
|
342
|
+
NEVER interrupt a write_file to start another.
|
|
343
|
+
ALWAYS close </write_file> before opening another.
|
|
344
|
+
|
|
345
|
+
**4. VERIFY ALWAYS**
|
|
346
|
+
After writing code, run it to verify it works:
|
|
347
|
+
- C: <exec_cmd>gcc file.c -o file && ./file</exec_cmd>
|
|
348
|
+
- Python: <exec_cmd cmd="python file.py"/>
|
|
349
|
+
- Node: <exec_cmd cmd="node file.js"/>
|
|
350
|
+
- Tests: <exec_cmd cmd="pytest tests/"/>
|
|
351
|
+
|
|
352
|
+
**5. NEVER STOP UNTIL COMPLETE**
|
|
353
|
+
DO NOT ask for permission. DO NOT wait for confirmation. KEEP WORKING until done.
|
|
354
|
+
Use <done/> ONLY when ALL tasks are complete and verified.
|
|
355
|
+
|
|
356
|
+
**6. COMPLETE FILES**
|
|
357
|
+
In write_file, write the COMPLETE file. NEVER use "# ... rest unchanged" or similar.
|
|
358
|
+
|
|
359
|
+
**7. FIX ERRORS COMPLETELY**
|
|
360
|
+
When something fails:
|
|
361
|
+
1. Read the error message carefully
|
|
362
|
+
2. Identify the root cause
|
|
363
|
+
3. Fix the issue
|
|
364
|
+
4. Verify the fix works
|
|
365
|
+
5. Continue with remaining tasks
|
|
366
|
+
|
|
367
|
+
**8. USE TASK MANAGEMENT**
|
|
368
|
+
For complex tasks, create and track tasks:
|
|
369
|
+
<task_create subject="Implement X" description="Details"/>
|
|
370
|
+
<task_update taskId="1" status="in_progress"/>
|
|
371
|
+
... work ...
|
|
372
|
+
<task_update taskId="1" status="completed"/>
|
|
373
|
+
|
|
374
|
+
## EXEC_CMD FORMATS
|
|
375
|
+
|
|
376
|
+
Simple command:
|
|
377
|
+
<exec_cmd cmd="python script.py"/>
|
|
378
|
+
|
|
379
|
+
Block format (for complex commands):
|
|
380
|
+
<exec_cmd>
|
|
381
|
+
find . -name "*.py" -exec grep -l "pattern" {} \\;
|
|
382
|
+
</exec_cmd>
|
|
383
|
+
|
|
384
|
+
## COMPLETION SIGNAL
|
|
385
|
+
|
|
386
|
+
When ALL work is done:
|
|
387
|
+
1. Write a summary of what was done
|
|
388
|
+
2. List modified/created files
|
|
389
|
+
3. Show verification results
|
|
390
|
+
4. End with: <done/>
|
|
391
|
+
|
|
392
|
+
Example:
|
|
393
|
+
## Summary
|
|
394
|
+
- Created src/api.py with REST endpoints
|
|
395
|
+
- Modified config.py to add new settings
|
|
396
|
+
- All tests passed
|
|
397
|
+
|
|
398
|
+
<done/>
|
|
399
|
+
|
|
400
|
+
DO NOT use <done/> until everything works.
|
|
401
|
+
"""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# connectors/__init__.py
|
|
2
|
+
"""Auto-registra todos los conectores disponibles al importar el paquete."""
|
|
3
|
+
from .base import BaseConnector, ModelResponse, ToolCall
|
|
4
|
+
from .registry import ConnectorRegistry
|
|
5
|
+
|
|
6
|
+
def _try(mod):
|
|
7
|
+
try:
|
|
8
|
+
import importlib
|
|
9
|
+
importlib.import_module(f".{mod}", package=__name__)
|
|
10
|
+
except Exception:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
_try("claude_connector")
|
|
14
|
+
_try("openai_connector")
|
|
15
|
+
_try("gemini_connector")
|
|
16
|
+
_try("ollama_connector")
|
|
17
|
+
_try("glm_connector")
|
|
18
|
+
|
|
19
|
+
__all__ = ["BaseConnector", "ModelResponse", "ToolCall", "ConnectorRegistry"]
|
hanus/connectors/base.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# connectors/base.py
|
|
2
|
+
"""
|
|
3
|
+
Interfaz unificada para todos los conectores.
|
|
4
|
+
|
|
5
|
+
IMPORTANTE — dos modos de operación:
|
|
6
|
+
• NATIVE_TOOLS = True → el modelo soporta tool_calls nativos (Claude, OpenAI).
|
|
7
|
+
El engine le pasa TOOL_DEFINITIONS y parsea ToolCall objects.
|
|
8
|
+
• NATIVE_TOOLS = False → el modelo solo soporta chat de texto (Gemini, Ollama).
|
|
9
|
+
El engine le inyecta las herramientas como instrucciones XML
|
|
10
|
+
en el system prompt y parsea la respuesta de texto buscando bloques XML.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import List, Dict, Optional, Any, Callable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ToolCall:
|
|
20
|
+
"""Solicitud de herramienta emitida por el modelo."""
|
|
21
|
+
id: str
|
|
22
|
+
name: str
|
|
23
|
+
arguments: Dict[str, Any]
|
|
24
|
+
raw: Optional[Any] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ModelResponse:
|
|
29
|
+
"""Respuesta unificada de cualquier proveedor."""
|
|
30
|
+
text: str = ""
|
|
31
|
+
tool_calls: List[ToolCall] = field(default_factory=list)
|
|
32
|
+
stop_reason: str = "end_turn" # end_turn | tool_use | max_tokens | error
|
|
33
|
+
input_tokens: int = 0
|
|
34
|
+
output_tokens: int = 0
|
|
35
|
+
model_id: str = ""
|
|
36
|
+
cost_usd: float = 0.0
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def total_tokens(self) -> int:
|
|
40
|
+
return self.input_tokens + self.output_tokens
|
|
41
|
+
|
|
42
|
+
def calc_cost(self, price_in_per_1m: float, price_out_per_1m: float):
|
|
43
|
+
self.cost_usd = (
|
|
44
|
+
self.input_tokens * price_in_per_1m / 1_000_000 +
|
|
45
|
+
self.output_tokens * price_out_per_1m / 1_000_000
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class BaseConnector(ABC):
|
|
50
|
+
"""Contrato base para todos los conectores de proveedores de IA."""
|
|
51
|
+
|
|
52
|
+
# Sobrescribir en conectores sin soporte nativo de herramientas
|
|
53
|
+
NATIVE_TOOLS: bool = False
|
|
54
|
+
|
|
55
|
+
def __init__(self, config: Dict[str, Any]):
|
|
56
|
+
self.config = config
|
|
57
|
+
self.model_id = config.get("model_id", "")
|
|
58
|
+
self._session_cost: float = 0.0
|
|
59
|
+
self._session_tokens: int = 0
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def validate(self) -> bool:
|
|
63
|
+
"""Verifica que las credenciales y el servicio estén disponibles."""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def chat(
|
|
68
|
+
self,
|
|
69
|
+
messages: List[Dict],
|
|
70
|
+
stream_callback: Optional[Callable[[str], None]] = None,
|
|
71
|
+
) -> ModelResponse:
|
|
72
|
+
"""
|
|
73
|
+
Envía historial y retorna respuesta.
|
|
74
|
+
Los conectores con NATIVE_TOOLS=True reciben tools por separado
|
|
75
|
+
vía chat_with_tools(). Este método siempre debe funcionar sin tools.
|
|
76
|
+
"""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
def chat_with_tools(
|
|
80
|
+
self,
|
|
81
|
+
messages: List[Dict],
|
|
82
|
+
tools: List[Dict],
|
|
83
|
+
stream_callback: Optional[Callable[[str], None]] = None,
|
|
84
|
+
) -> ModelResponse:
|
|
85
|
+
"""
|
|
86
|
+
Solo para conectores con NATIVE_TOOLS=True.
|
|
87
|
+
Los demás no deben sobrescribir esto — el engine usará XML fallback.
|
|
88
|
+
"""
|
|
89
|
+
raise NotImplementedError("Este conector no soporta tool_calls nativos.")
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def count_tokens(self, text: str) -> int:
|
|
93
|
+
"""Estima número de tokens de un texto."""
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
@abstractmethod
|
|
98
|
+
def provider_name(self) -> str: ...
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def available_models(self) -> List[str]: ...
|
|
103
|
+
|
|
104
|
+
# ── Métricas de sesión ────────────────────────────────────────────────────
|
|
105
|
+
def record(self, response: ModelResponse):
|
|
106
|
+
self._session_cost += response.cost_usd
|
|
107
|
+
self._session_tokens += response.total_tokens
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def session_cost(self) -> float: return self._session_cost
|
|
111
|
+
@property
|
|
112
|
+
def session_tokens(self) -> int: return self._session_tokens
|
|
113
|
+
def reset_session(self):
|
|
114
|
+
self._session_cost = 0.0; self._session_tokens = 0
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# connectors/claude_connector.py
|
|
2
|
+
"""
|
|
3
|
+
Conector Anthropic Claude. NATIVE_TOOLS = True.
|
|
4
|
+
Soporta streaming real, tool_use nativo y prompt cache.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
from typing import List, Dict, Optional, Any, Callable
|
|
8
|
+
|
|
9
|
+
from .base import BaseConnector, ModelResponse, ToolCall
|
|
10
|
+
from .registry import ConnectorRegistry
|
|
11
|
+
|
|
12
|
+
PRICING = {
|
|
13
|
+
"claude-opus-4-5": {"in": 15.0, "out": 75.0},
|
|
14
|
+
"claude-sonnet-4-5": {"in": 3.0, "out": 15.0},
|
|
15
|
+
"claude-haiku-4-5": {"in": 0.25, "out": 1.25},
|
|
16
|
+
"claude-3-5-sonnet-20241022": {"in": 3.0, "out": 15.0},
|
|
17
|
+
"claude-3-haiku-20240307": {"in": 0.25, "out": 1.25},
|
|
18
|
+
"claude-3-opus-20240229": {"in": 15.0, "out": 75.0},
|
|
19
|
+
}
|
|
20
|
+
MODELS = list(PRICING.keys())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@ConnectorRegistry.register("claude")
|
|
24
|
+
class ClaudeConnector(BaseConnector):
|
|
25
|
+
|
|
26
|
+
NATIVE_TOOLS = True
|
|
27
|
+
|
|
28
|
+
def __init__(self, config: Dict[str, Any]):
|
|
29
|
+
super().__init__(config)
|
|
30
|
+
self.api_key = config.get("api_key", "")
|
|
31
|
+
self.model_id = config.get("model_id", "claude-sonnet-4-5")
|
|
32
|
+
self.max_tokens = config.get("max_tokens", 8096)
|
|
33
|
+
self._client = None
|
|
34
|
+
|
|
35
|
+
def _get_client(self):
|
|
36
|
+
if self._client is None:
|
|
37
|
+
try:
|
|
38
|
+
import anthropic
|
|
39
|
+
self._client = anthropic.Anthropic(api_key=self.api_key)
|
|
40
|
+
except ImportError:
|
|
41
|
+
raise RuntimeError("Instala: pip install anthropic")
|
|
42
|
+
return self._client
|
|
43
|
+
|
|
44
|
+
def validate(self) -> bool:
|
|
45
|
+
try:
|
|
46
|
+
c = self._get_client()
|
|
47
|
+
c.messages.create(
|
|
48
|
+
model=self.model_id, max_tokens=10,
|
|
49
|
+
messages=[{"role": "user", "content": "hi"}]
|
|
50
|
+
)
|
|
51
|
+
return True
|
|
52
|
+
except Exception:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
def chat(
|
|
56
|
+
self,
|
|
57
|
+
messages: List[Dict],
|
|
58
|
+
stream_callback: Optional[Callable[[str], None]] = None,
|
|
59
|
+
) -> ModelResponse:
|
|
60
|
+
"""Chat sin tools (usado para mensajes simples)."""
|
|
61
|
+
return self._do_chat(messages, tools=None, stream_callback=stream_callback)
|
|
62
|
+
|
|
63
|
+
def chat_with_tools(
|
|
64
|
+
self,
|
|
65
|
+
messages: List[Dict],
|
|
66
|
+
tools: List[Dict],
|
|
67
|
+
stream_callback: Optional[Callable[[str], None]] = None,
|
|
68
|
+
) -> ModelResponse:
|
|
69
|
+
"""Chat con tool_calls nativos."""
|
|
70
|
+
return self._do_chat(messages, tools=tools, stream_callback=stream_callback)
|
|
71
|
+
|
|
72
|
+
def _do_chat(
|
|
73
|
+
self,
|
|
74
|
+
messages: List[Dict],
|
|
75
|
+
tools: Optional[List[Dict]],
|
|
76
|
+
stream_callback: Optional[Callable[[str], None]],
|
|
77
|
+
) -> ModelResponse:
|
|
78
|
+
client = self._get_client()
|
|
79
|
+
system = ""
|
|
80
|
+
history = []
|
|
81
|
+
for m in messages:
|
|
82
|
+
if m["role"] == "system":
|
|
83
|
+
system = m["content"]
|
|
84
|
+
else:
|
|
85
|
+
history.append({"role": m["role"], "content": str(m["content"])})
|
|
86
|
+
|
|
87
|
+
kwargs: Dict[str, Any] = {
|
|
88
|
+
"model": self.model_id,
|
|
89
|
+
"max_tokens": self.max_tokens,
|
|
90
|
+
"messages": history,
|
|
91
|
+
}
|
|
92
|
+
if system:
|
|
93
|
+
kwargs["system"] = system
|
|
94
|
+
if tools:
|
|
95
|
+
kwargs["tools"] = tools
|
|
96
|
+
|
|
97
|
+
resp = ModelResponse(model_id=self.model_id)
|
|
98
|
+
try:
|
|
99
|
+
if stream_callback:
|
|
100
|
+
with client.messages.stream(**kwargs) as stream:
|
|
101
|
+
for token in stream.text_stream:
|
|
102
|
+
resp.text += token
|
|
103
|
+
stream_callback(token)
|
|
104
|
+
final = stream.get_final_message()
|
|
105
|
+
resp.stop_reason = final.stop_reason or "end_turn"
|
|
106
|
+
resp.input_tokens = final.usage.input_tokens
|
|
107
|
+
resp.output_tokens = final.usage.output_tokens
|
|
108
|
+
self._extract_tools(final, resp)
|
|
109
|
+
else:
|
|
110
|
+
msg = client.messages.create(**kwargs)
|
|
111
|
+
resp.stop_reason = msg.stop_reason or "end_turn"
|
|
112
|
+
resp.input_tokens = msg.usage.input_tokens
|
|
113
|
+
resp.output_tokens = msg.usage.output_tokens
|
|
114
|
+
for block in msg.content:
|
|
115
|
+
if hasattr(block, "text"):
|
|
116
|
+
resp.text += block.text
|
|
117
|
+
self._extract_tools(msg, resp)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
resp.text = f"[Error Claude] {type(e).__name__}: {e}"
|
|
120
|
+
resp.stop_reason = "error"
|
|
121
|
+
|
|
122
|
+
p = PRICING.get(self.model_id, {"in": 3.0, "out": 15.0})
|
|
123
|
+
resp.calc_cost(p["in"], p["out"])
|
|
124
|
+
self.record(resp)
|
|
125
|
+
return resp
|
|
126
|
+
|
|
127
|
+
def _extract_tools(self, msg: Any, resp: ModelResponse):
|
|
128
|
+
import uuid
|
|
129
|
+
for block in msg.content:
|
|
130
|
+
if block.type == "tool_use":
|
|
131
|
+
resp.tool_calls.append(ToolCall(
|
|
132
|
+
id=block.id,
|
|
133
|
+
name=block.name,
|
|
134
|
+
arguments=block.input if isinstance(block.input, dict) else {},
|
|
135
|
+
raw=block,
|
|
136
|
+
))
|
|
137
|
+
if resp.tool_calls:
|
|
138
|
+
resp.stop_reason = "tool_use"
|
|
139
|
+
|
|
140
|
+
def count_tokens(self, text: str) -> int:
|
|
141
|
+
return len(text) // 4
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def provider_name(self) -> str: return "Anthropic Claude"
|
|
145
|
+
@property
|
|
146
|
+
def available_models(self) -> List[str]: return MODELS
|