Coverage for integrations / coding_agent / backend_repair_tools.py: 71.4%

35 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-12 04:49 +0000

1"""Backend Repair Tools — agent-callable wrappers over Nunba's 

2per-backend TTS venv install/wipe layer. 

3 

4Why this exists 

5--------------- 

6``error_advice.handle_exception(category='tts.probe', 

7context={'backend': 'indic_parler'})`` creates ``self_heal`` AgentGoals 

8that the coding daemon picks up. The autoresearch tool family in 

9``autoevolve_code_tools.py`` only does source edits — useful when the 

10fix is "patch a Python file", useless when the fix is "the venv at 

11``~/Documents/Nunba/data/venvs/indic_parler/`` is missing the 

12``parler_tts`` package". Source-edit alone leaves the live broken 

13venv broken until the next rebuild. 

14 

15This module gives the agent a tool that actually repairs the venv: 

16``repair_backend_venv(backend_name, wipe_first=False)`` calls Nunba's 

17canonical ``install_backend_full()``, the same function the user-side 

18"Set up TTS" UI uses. 

19 

20Wired in 

21-------- 

22* ``mcp_http_bridge.py`` registers ``BACKEND_REPAIR_TOOLS`` for autogen 

23 via the same module-list registration loop that exposes 

24 ``AUTOEVOLVE_CODE_TOOLS`` / ``AUTO_EVOLVE_TOOLS`` / 

25 ``THOUGHT_EXPERIMENT_TOOLS`` — see 

26 ``mcp_http_bridge._load_tools`` around the registration tuple list. 

27* ``goal_manager._build_self_heal_prompt`` mentions 

28 ``repair_backend_venv`` when the goal's ``category`` is one of the 

29 TTS / subprocess install-failure shapes AND ``context.backend`` is 

30 set on the goal config. 

31 

32Bundled-mode-only contract 

33-------------------------- 

34In source-mode HARTOS (a checkout without the Nunba freeze), Nunba's 

35``tts.*`` modules are not on ``sys.path``. The tool returns a 

36graceful ``{success: False, message: 'Backend repair tools require 

37the Nunba bundled environment ...'}`` JSON so the agent can mark the 

38goal failed instead of crashing the autogen group. 

39""" 

40 

41import json 

42import logging 

43from pathlib import Path 

44from typing import Optional 

45 

46logger = logging.getLogger('hevolve.backend_repair') 

47 

48 

49def _get_known_backends() -> set: 

50 """Return engine_ids known to HARTOS's ``ENGINE_REGISTRY``. 

51 

52 Single source of truth — never hand-maintain a second list. When 

53 the registry can't be loaded (HARTOS-side circular import during 

54 boot, missing module), returns an empty set so the caller fails 

55 closed with a clear message rather than silently accepting any 

56 string. 

57 """ 

58 try: 

59 from integrations.channels.media.tts_router import ENGINE_REGISTRY 

60 return {spec.engine_id for spec in ENGINE_REGISTRY.values()} 

61 except Exception as e: 

62 logger.warning( 

63 f"backend_repair_tools: ENGINE_REGISTRY unavailable for " 

64 f"validation ({type(e).__name__}: {e})" 

65 ) 

66 return set() 

67 

68 

69def _resolve_log_path(backend_name: str) -> Optional[str]: 

70 """Return the path Nunba writes pip output to for this backend. 

71 

72 Mirrors ``tts.backend_venv._venv_log_path`` so the agent can grep 

73 a specific file when ``install_backend_full`` returns False. 

74 Best-effort — returns None if the path cannot be derived (the 

75 repair still runs; only the diagnostic hint is missing). 

76 """ 

77 try: 

78 from core.platform_paths import get_log_dir # type: ignore 

79 log_dir = Path(get_log_dir()) 

80 except Exception: 

81 log_dir = Path.home() / 'Documents' / 'Nunba' / 'logs' 

82 try: 

83 return str(log_dir / f'venv_{backend_name}.log') 

84 except Exception: 

85 return None 

86 

87 

88def repair_backend_venv(backend_name: str, wipe_first: bool = False) -> str: 

89 """Reinstall a TTS backend's per-engine venv. 

90 

91 Use this tool when a ``self_heal`` goal carries 

92 ``category='tts.probe'``, ``category='tts.install'``, 

93 ``category='tts.install.self_heal_exhausted'``, or 

94 ``category='subprocess.tool_load'`` AND ``context.backend`` names 

95 a broken backend. 

96 

97 The work is delegated to Nunba's existing 

98 ``tts.package_installer.install_backend_full`` — the same function 

99 the user-side "Set up TTS" UI calls. That function: 

100 

101 1. Creates the per-engine venv if missing (idempotent). 

102 2. Routes the canonical pip_install_plan from 

103 ``ENGINE_REGISTRY[backend].pip_install_plan`` into either the 

104 backend's venv (when ``install_target='venv'``) or the main 

105 interpreter (when ``install_target='main'``). 

106 3. Applies post-install patches. 

107 4. Downloads model weights via huggingface_hub. 

108 

109 Args: 

110 backend_name: One of the engine_ids in 

111 ``integrations.channels.media.tts_router.ENGINE_REGISTRY``. 

112 Common names: ``'indic_parler'``, ``'kokoro'``, 

113 ``'melotts'``, ``'f5_tts'``, ``'chatterbox_turbo'``, 

114 ``'chatterbox'``, ``'omnivoice'``, ``'piper'``, 

115 ``'pocket_tts'``, ``'neutts_air'``. 

116 wipe_first: When True, delete the existing venv directory 

117 before reinstall. Use after corruption, partial installs, 

118 or when changing transitive-dep cages. Default False 

119 (idempotent reinstall — pip skips already-satisfied specs). 

120 

121 Returns: 

122 JSON string with: 

123 ``success`` (bool): pip + model_weights both succeeded. 

124 ``backend`` (str): the validated backend name. 

125 ``message`` (str): human-readable status. 

126 ``log_path`` (str | None): path to the venv install log 

127 (``~/Documents/Nunba/logs/venv_<backend>.log``). 

128 ``wiped`` (bool): whether ``wipe_venv`` was actually called. 

129 """ 

130 log_path = _resolve_log_path(backend_name) 

131 

132 # Validate against ENGINE_REGISTRY — prevents path-traversal / 

133 # arbitrary-name injection BEFORE any filesystem touch. 

134 known = _get_known_backends() 

135 if not known: 

136 return json.dumps({ 

137 'success': False, 

138 'backend': backend_name, 

139 'message': ( 

140 'Cannot validate backend: HARTOS ENGINE_REGISTRY ' 

141 'unavailable in this process. Likely a non-bundled ' 

142 'HARTOS server without tts_router on sys.path.' 

143 ), 

144 'log_path': log_path, 

145 'wiped': False, 

146 }) 

147 if backend_name not in known: 

148 return json.dumps({ 

149 'success': False, 

150 'backend': backend_name, 

151 'message': ( 

152 f"Unknown backend {backend_name!r}. " 

153 f"Known backends: {sorted(known)}" 

154 ), 

155 'log_path': log_path, 

156 'wiped': False, 

157 }) 

158 

159 # Lazy-import Nunba's venv layer. In source-mode HARTOS the 

160 # imports fail — return a structured "not in bundled" message. 

161 try: 

162 from tts.backend_venv import wipe_venv as _wipe_venv # type: ignore 

163 from tts.package_installer import install_backend_full # type: ignore 

164 except ImportError as e: 

165 return json.dumps({ 

166 'success': False, 

167 'backend': backend_name, 

168 'message': ( 

169 f'Backend repair requires the Nunba bundled environment ' 

170 f'(tts.backend_venv + tts.package_installer must be ' 

171 f'importable). ImportError: {e}. Source-mode HARTOS ' 

172 f'deploys cannot reinstall TTS venvs — the agent should ' 

173 f'mark this self_heal goal failed and the operator ' 

174 f'should run the repair from a Nunba install.' 

175 ), 

176 'log_path': log_path, 

177 'wiped': False, 

178 }) 

179 

180 wiped = False 

181 if wipe_first: 

182 try: 

183 _wipe_venv(backend_name) 

184 wiped = True 

185 logger.info(f"repair_backend_venv: wiped {backend_name!r}") 

186 except Exception as e: 

187 return json.dumps({ 

188 'success': False, 

189 'backend': backend_name, 

190 'message': ( 

191 f'wipe_venv({backend_name!r}) failed: ' 

192 f'{type(e).__name__}: {e}' 

193 ), 

194 'log_path': log_path, 

195 'wiped': False, 

196 }) 

197 

198 try: 

199 ok, msg = install_backend_full(backend_name) 

200 except Exception as e: 

201 return json.dumps({ 

202 'success': False, 

203 'backend': backend_name, 

204 'message': ( 

205 f'install_backend_full({backend_name!r}) raised: ' 

206 f'{type(e).__name__}: {e}' 

207 ), 

208 'log_path': log_path, 

209 'wiped': wiped, 

210 }) 

211 

212 return json.dumps({ 

213 'success': bool(ok), 

214 'backend': backend_name, 

215 'message': str(msg), 

216 'log_path': log_path, 

217 'wiped': wiped, 

218 }) 

219 

220 

221# Tool registration list — same shape as AUTOEVOLVE_CODE_TOOLS / 

222# AUTO_EVOLVE_TOOLS / THOUGHT_EXPERIMENT_TOOLS. Consumed by 

223# mcp_http_bridge._load_tools via the registration tuple list around 

224# line 998. The 'tags' field is documentary — the bridge's dedup is 

225# by name, and the goal_manager.tool_tags filter is per goal_type. 

226BACKEND_REPAIR_TOOLS = [ 

227 { 

228 'name': 'repair_backend_venv', 

229 'func': repair_backend_venv, 

230 'description': ( 

231 'Reinstall a broken TTS backend venv (pip install plan + ' 

232 'model weights). Use when a self_heal goal carries ' 

233 'category in {tts.probe, tts.install, ' 

234 'tts.install.self_heal_exhausted, subprocess.tool_load} ' 

235 'AND context.backend names the failed backend. Optional ' 

236 'wipe_first=True for full clean reinstall (use after ' 

237 'corruption / version-conflict situations).' 

238 ), 

239 'tags': ['coding', 'self_heal', 'tts'], 

240 }, 

241]