hanzo-mcp 0.8.8__py3-none-any.whl → 0.9.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 hanzo-mcp might be problematic. Click here for more details.

Files changed (167) hide show
  1. hanzo_mcp/__init__.py +1 -3
  2. hanzo_mcp/analytics/posthog_analytics.py +4 -17
  3. hanzo_mcp/bridge.py +9 -25
  4. hanzo_mcp/cli.py +8 -17
  5. hanzo_mcp/cli_enhanced.py +5 -14
  6. hanzo_mcp/cli_plugin.py +3 -9
  7. hanzo_mcp/config/settings.py +6 -20
  8. hanzo_mcp/config/tool_config.py +2 -4
  9. hanzo_mcp/core/base_agent.py +88 -88
  10. hanzo_mcp/core/model_registry.py +238 -210
  11. hanzo_mcp/dev_server.py +5 -15
  12. hanzo_mcp/prompts/__init__.py +2 -6
  13. hanzo_mcp/prompts/project_todo_reminder.py +3 -9
  14. hanzo_mcp/prompts/tool_explorer.py +1 -3
  15. hanzo_mcp/prompts/utils.py +7 -21
  16. hanzo_mcp/server.py +6 -7
  17. hanzo_mcp/tools/__init__.py +29 -32
  18. hanzo_mcp/tools/agent/__init__.py +2 -1
  19. hanzo_mcp/tools/agent/agent.py +10 -30
  20. hanzo_mcp/tools/agent/agent_tool.py +23 -17
  21. hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
  22. hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
  23. hanzo_mcp/tools/agent/cli_tools.py +76 -75
  24. hanzo_mcp/tools/agent/code_auth.py +1 -3
  25. hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
  26. hanzo_mcp/tools/agent/critic_tool.py +8 -24
  27. hanzo_mcp/tools/agent/iching_tool.py +12 -36
  28. hanzo_mcp/tools/agent/network_tool.py +7 -18
  29. hanzo_mcp/tools/agent/prompt.py +1 -5
  30. hanzo_mcp/tools/agent/review_tool.py +10 -25
  31. hanzo_mcp/tools/agent/swarm_alias.py +1 -3
  32. hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
  33. hanzo_mcp/tools/common/batch_tool.py +15 -45
  34. hanzo_mcp/tools/common/config_tool.py +9 -28
  35. hanzo_mcp/tools/common/context.py +1 -3
  36. hanzo_mcp/tools/common/critic_tool.py +1 -3
  37. hanzo_mcp/tools/common/decorators.py +2 -6
  38. hanzo_mcp/tools/common/enhanced_base.py +2 -6
  39. hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
  40. hanzo_mcp/tools/common/forgiving_edit.py +9 -28
  41. hanzo_mcp/tools/common/mode.py +1 -5
  42. hanzo_mcp/tools/common/paginated_base.py +3 -11
  43. hanzo_mcp/tools/common/paginated_response.py +10 -30
  44. hanzo_mcp/tools/common/pagination.py +3 -9
  45. hanzo_mcp/tools/common/path_utils.py +34 -0
  46. hanzo_mcp/tools/common/permissions.py +14 -13
  47. hanzo_mcp/tools/common/personality.py +983 -701
  48. hanzo_mcp/tools/common/plugin_loader.py +3 -15
  49. hanzo_mcp/tools/common/stats.py +7 -19
  50. hanzo_mcp/tools/common/thinking_tool.py +1 -3
  51. hanzo_mcp/tools/common/tool_disable.py +2 -6
  52. hanzo_mcp/tools/common/tool_list.py +2 -6
  53. hanzo_mcp/tools/common/validation.py +1 -3
  54. hanzo_mcp/tools/compiler/__init__.py +8 -0
  55. hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
  56. hanzo_mcp/tools/config/config_tool.py +7 -13
  57. hanzo_mcp/tools/config/index_config.py +1 -3
  58. hanzo_mcp/tools/config/mode_tool.py +5 -15
  59. hanzo_mcp/tools/database/database_manager.py +3 -9
  60. hanzo_mcp/tools/database/graph.py +1 -3
  61. hanzo_mcp/tools/database/graph_add.py +3 -9
  62. hanzo_mcp/tools/database/graph_query.py +11 -34
  63. hanzo_mcp/tools/database/graph_remove.py +3 -9
  64. hanzo_mcp/tools/database/graph_search.py +6 -20
  65. hanzo_mcp/tools/database/graph_stats.py +11 -33
  66. hanzo_mcp/tools/database/sql.py +4 -12
  67. hanzo_mcp/tools/database/sql_query.py +6 -10
  68. hanzo_mcp/tools/database/sql_search.py +2 -6
  69. hanzo_mcp/tools/database/sql_stats.py +5 -15
  70. hanzo_mcp/tools/editor/neovim_command.py +1 -3
  71. hanzo_mcp/tools/editor/neovim_session.py +7 -13
  72. hanzo_mcp/tools/environment/__init__.py +8 -0
  73. hanzo_mcp/tools/environment/environment_detector.py +594 -0
  74. hanzo_mcp/tools/filesystem/__init__.py +28 -26
  75. hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
  76. hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
  77. hanzo_mcp/tools/filesystem/base.py +20 -12
  78. hanzo_mcp/tools/filesystem/content_replace.py +7 -12
  79. hanzo_mcp/tools/filesystem/diff.py +2 -10
  80. hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
  81. hanzo_mcp/tools/filesystem/edit.py +10 -18
  82. hanzo_mcp/tools/filesystem/find.py +312 -179
  83. hanzo_mcp/tools/filesystem/git_search.py +12 -24
  84. hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
  85. hanzo_mcp/tools/filesystem/read.py +14 -30
  86. hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
  87. hanzo_mcp/tools/filesystem/search.py +1160 -0
  88. hanzo_mcp/tools/filesystem/watch.py +2 -4
  89. hanzo_mcp/tools/filesystem/write.py +7 -10
  90. hanzo_mcp/tools/framework/__init__.py +8 -0
  91. hanzo_mcp/tools/framework/framework_modes.py +714 -0
  92. hanzo_mcp/tools/jupyter/base.py +6 -20
  93. hanzo_mcp/tools/jupyter/jupyter.py +4 -12
  94. hanzo_mcp/tools/llm/consensus_tool.py +8 -24
  95. hanzo_mcp/tools/llm/llm_manage.py +2 -6
  96. hanzo_mcp/tools/llm/llm_tool.py +17 -58
  97. hanzo_mcp/tools/llm/llm_unified.py +18 -59
  98. hanzo_mcp/tools/llm/provider_tools.py +1 -3
  99. hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
  100. hanzo_mcp/tools/mcp/mcp_add.py +3 -5
  101. hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
  102. hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
  103. hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
  104. hanzo_mcp/tools/memory/__init__.py +33 -40
  105. hanzo_mcp/tools/memory/conversation_memory.py +636 -0
  106. hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
  107. hanzo_mcp/tools/memory/memory_tools.py +7 -19
  108. hanzo_mcp/tools/search/find_tool.py +12 -34
  109. hanzo_mcp/tools/search/unified_search.py +27 -81
  110. hanzo_mcp/tools/shell/__init__.py +16 -4
  111. hanzo_mcp/tools/shell/auto_background.py +2 -6
  112. hanzo_mcp/tools/shell/base.py +1 -5
  113. hanzo_mcp/tools/shell/base_process.py +5 -7
  114. hanzo_mcp/tools/shell/bash_session.py +7 -24
  115. hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
  116. hanzo_mcp/tools/shell/bash_tool.py +3 -7
  117. hanzo_mcp/tools/shell/command_executor.py +26 -79
  118. hanzo_mcp/tools/shell/logs.py +4 -16
  119. hanzo_mcp/tools/shell/npx.py +2 -8
  120. hanzo_mcp/tools/shell/npx_tool.py +1 -3
  121. hanzo_mcp/tools/shell/pkill.py +4 -12
  122. hanzo_mcp/tools/shell/process_tool.py +2 -8
  123. hanzo_mcp/tools/shell/processes.py +5 -17
  124. hanzo_mcp/tools/shell/run_background.py +1 -3
  125. hanzo_mcp/tools/shell/run_command.py +1 -3
  126. hanzo_mcp/tools/shell/run_command_windows.py +1 -3
  127. hanzo_mcp/tools/shell/run_tool.py +56 -0
  128. hanzo_mcp/tools/shell/session_manager.py +2 -6
  129. hanzo_mcp/tools/shell/session_storage.py +2 -6
  130. hanzo_mcp/tools/shell/streaming_command.py +7 -23
  131. hanzo_mcp/tools/shell/uvx.py +4 -14
  132. hanzo_mcp/tools/shell/uvx_background.py +2 -6
  133. hanzo_mcp/tools/shell/uvx_tool.py +1 -3
  134. hanzo_mcp/tools/shell/zsh_tool.py +12 -20
  135. hanzo_mcp/tools/todo/todo.py +1 -3
  136. hanzo_mcp/tools/vector/__init__.py +97 -50
  137. hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
  138. hanzo_mcp/tools/vector/git_ingester.py +10 -30
  139. hanzo_mcp/tools/vector/index_tool.py +3 -9
  140. hanzo_mcp/tools/vector/infinity_store.py +11 -30
  141. hanzo_mcp/tools/vector/mock_infinity.py +159 -0
  142. hanzo_mcp/tools/vector/node_tool.py +538 -0
  143. hanzo_mcp/tools/vector/project_manager.py +4 -12
  144. hanzo_mcp/tools/vector/unified_vector.py +384 -0
  145. hanzo_mcp/tools/vector/vector.py +2 -6
  146. hanzo_mcp/tools/vector/vector_index.py +8 -8
  147. hanzo_mcp/tools/vector/vector_search.py +7 -21
  148. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
  149. hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
  150. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
  151. hanzo_mcp/tools/agent/swarm_tool.py +0 -723
  152. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
  153. hanzo_mcp/tools/filesystem/batch_search.py +0 -900
  154. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
  155. hanzo_mcp/tools/filesystem/find_files.py +0 -369
  156. hanzo_mcp/tools/filesystem/grep.py +0 -467
  157. hanzo_mcp/tools/filesystem/search_tool.py +0 -767
  158. hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
  159. hanzo_mcp/tools/filesystem/tree.py +0 -270
  160. hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
  161. hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
  162. hanzo_mcp/tools/todo/todo_read.py +0 -143
  163. hanzo_mcp/tools/todo/todo_write.py +0 -374
  164. hanzo_mcp-0.8.8.dist-info/RECORD +0 -192
  165. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
  166. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
  167. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/top_level.txt +0 -0
@@ -127,9 +127,7 @@ class JupyterBaseTool(FilesystemBaseTool, ABC):
127
127
  """
128
128
  tool_ctx.set_tool_info(self.name)
129
129
 
130
- async def parse_notebook(
131
- self, file_path: Path
132
- ) -> tuple[dict[str, Any], list[NotebookCellSource]]:
130
+ async def parse_notebook(self, file_path: Path) -> tuple[dict[str, Any], list[NotebookCellSource]]:
133
131
  """Parse a Jupyter notebook file.
134
132
 
135
133
  Args:
@@ -143,9 +141,7 @@ class JupyterBaseTool(FilesystemBaseTool, ABC):
143
141
  notebook = json.loads(content)
144
142
 
145
143
  # Get notebook language
146
- language = (
147
- notebook.get("metadata", {}).get("language_info", {}).get("name", "python")
148
- )
144
+ language = notebook.get("metadata", {}).get("language_info", {}).get("name", "python")
149
145
  cells = notebook.get("cells", [])
150
146
  processed_cells = []
151
147
 
@@ -177,9 +173,7 @@ class JupyterBaseTool(FilesystemBaseTool, ABC):
177
173
  text = output.get("text", "")
178
174
  if isinstance(text, list):
179
175
  text = "".join(text)
180
- outputs.append(
181
- NotebookCellOutput(output_type="stream", text=text)
182
- )
176
+ outputs.append(NotebookCellOutput(output_type="stream", text=text))
183
177
 
184
178
  elif output_type in ["execute_result", "display_data"]:
185
179
  # Process text output
@@ -205,11 +199,7 @@ class JupyterBaseTool(FilesystemBaseTool, ABC):
205
199
  media_type="image/jpeg",
206
200
  )
207
201
 
208
- outputs.append(
209
- NotebookCellOutput(
210
- output_type=output_type, text=text, image=image
211
- )
212
- )
202
+ outputs.append(NotebookCellOutput(output_type=output_type, text=text, image=image))
213
203
 
214
204
  elif output_type == "error":
215
205
  # Format error traceback
@@ -220,17 +210,13 @@ class JupyterBaseTool(FilesystemBaseTool, ABC):
220
210
  # Handle raw text strings and lists of strings
221
211
  if isinstance(traceback, list):
222
212
  # Clean ANSI escape codes and join the list but preserve the formatting
223
- clean_traceback = [
224
- clean_ansi_escapes(line) for line in traceback
225
- ]
213
+ clean_traceback = [clean_ansi_escapes(line) for line in traceback]
226
214
  traceback_text = "\n".join(clean_traceback)
227
215
  else:
228
216
  traceback_text = clean_ansi_escapes(str(traceback))
229
217
 
230
218
  error_text = f"{ename}: {evalue}\n{traceback_text}"
231
- outputs.append(
232
- NotebookCellOutput(output_type="error", text=error_text)
233
- )
219
+ outputs.append(NotebookCellOutput(output_type="error", text=error_text))
234
220
 
235
221
  # Create cell object
236
222
  processed_cell = NotebookCellSource(
@@ -151,9 +151,7 @@ jupyter --action create "new.ipynb"
151
151
  else:
152
152
  return f"Error: Unknown action '{action}'. Valid actions: read, edit, create, delete, execute"
153
153
 
154
- async def _handle_read(
155
- self, notebook_path: str, params: Dict[str, Any], tool_ctx
156
- ) -> str:
154
+ async def _handle_read(self, notebook_path: str, params: Dict[str, Any], tool_ctx) -> str:
157
155
  """Read notebook or specific cell."""
158
156
  exists, error_msg = await self.check_path_exists(notebook_path, tool_ctx)
159
157
  if not exists:
@@ -188,9 +186,7 @@ jupyter --action create "new.ipynb"
188
186
  await tool_ctx.error(f"Failed to read notebook: {str(e)}")
189
187
  return f"Error reading notebook: {str(e)}"
190
188
 
191
- async def _handle_edit(
192
- self, notebook_path: str, params: Dict[str, Any], tool_ctx
193
- ) -> str:
189
+ async def _handle_edit(self, notebook_path: str, params: Dict[str, Any], tool_ctx) -> str:
194
190
  """Edit notebook cell."""
195
191
  exists, error_msg = await self.check_path_exists(notebook_path, tool_ctx)
196
192
  if not exists:
@@ -295,9 +291,7 @@ jupyter --action create "new.ipynb"
295
291
  await tool_ctx.error(f"Failed to create notebook: {str(e)}")
296
292
  return f"Error creating notebook: {str(e)}"
297
293
 
298
- async def _handle_delete(
299
- self, notebook_path: str, params: Dict[str, Any], tool_ctx
300
- ) -> str:
294
+ async def _handle_delete(self, notebook_path: str, params: Dict[str, Any], tool_ctx) -> str:
301
295
  """Delete notebook or cell."""
302
296
  # If cell specified, delegate to edit with delete mode
303
297
  if params.get("cell_id") or params.get("cell_index") is not None:
@@ -316,9 +310,7 @@ jupyter --action create "new.ipynb"
316
310
  await tool_ctx.error(f"Failed to delete notebook: {str(e)}")
317
311
  return f"Error deleting notebook: {str(e)}"
318
312
 
319
- async def _handle_execute(
320
- self, notebook_path: str, params: Dict[str, Any], tool_ctx
321
- ) -> str:
313
+ async def _handle_execute(self, notebook_path: str, params: Dict[str, Any], tool_ctx) -> str:
322
314
  """Execute notebook cells (placeholder for future implementation)."""
323
315
  return "Error: Cell execution not yet implemented. Use a Jupyter kernel or server for execution."
324
316
 
@@ -211,22 +211,14 @@ The tool will:
211
211
  )
212
212
 
213
213
  # Prepare summary of results
214
- successful_responses = [
215
- (m, r) for m, r in results.items() if not r.startswith("Error:")
216
- ]
217
- failed_responses = [
218
- (m, r) for m, r in results.items() if r.startswith("Error:")
219
- ]
214
+ successful_responses = [(m, r) for m, r in results.items() if not r.startswith("Error:")]
215
+ failed_responses = [(m, r) for m, r in results.items() if r.startswith("Error:")]
220
216
 
221
217
  if not successful_responses:
222
- return "Error: All model queries failed:\n\n" + "\n".join(
223
- [f"{m}: {r}" for m, r in failed_responses]
224
- )
218
+ return "Error: All model queries failed:\n\n" + "\n".join([f"{m}: {r}" for m, r in failed_responses])
225
219
 
226
220
  # Use aggregation model to synthesize responses
227
- consensus = await self._aggregate_responses(
228
- successful_responses, prompt, aggregation_model
229
- )
221
+ consensus = await self._aggregate_responses(successful_responses, prompt, aggregation_model)
230
222
 
231
223
  # Format output
232
224
  output = ["=== LLM Consensus Analysis ==="]
@@ -245,9 +237,7 @@ The tool will:
245
237
  output.append("\n=== Individual Responses ===")
246
238
  for model, response in successful_responses:
247
239
  output.append(f"\n--- {model} ---")
248
- output.append(
249
- response[:500] + "..." if len(response) > 500 else response
250
- )
240
+ output.append(response[:500] + "..." if len(response) > 500 else response)
251
241
 
252
242
  if failed_responses:
253
243
  output.append("\n=== Failed Queries ===")
@@ -282,9 +272,7 @@ The tool will:
282
272
  # Create a mock context for the LLM tool
283
273
  mock_ctx = type("MockContext", (), {"client": None})()
284
274
 
285
- result = await asyncio.wait_for(
286
- self.llm_tool.call(mock_ctx, **params), timeout=timeout
287
- )
275
+ result = await asyncio.wait_for(self.llm_tool.call(mock_ctx, **params), timeout=timeout)
288
276
  return (model, result)
289
277
  except asyncio.TimeoutError:
290
278
  return (model, f"Error: Timeout after {timeout} seconds")
@@ -305,9 +293,7 @@ The tool will:
305
293
  ) -> str:
306
294
  """Use an LLM to aggregate and analyze responses."""
307
295
  # Prepare the aggregation prompt
308
- response_summary = "\n\n".join(
309
- [f"Model: {model}\nResponse: {response}" for model, response in responses]
310
- )
296
+ response_summary = "\n\n".join([f"Model: {model}\nResponse: {response}" for model, response in responses])
311
297
 
312
298
  aggregation_prompt = f"""You are analyzing responses from multiple AI models to the following prompt:
313
299
 
@@ -360,9 +346,7 @@ Be concise but thorough. Focus on providing actionable insights."""
360
346
  for model, response in responses:
361
347
  output.append(f"- {model}: {len(response)} characters")
362
348
 
363
- output.append(
364
- "\nNote: Advanced consensus analysis unavailable. Showing basic summary only."
365
- )
349
+ output.append("\nNote: Advanced consensus analysis unavailable. Showing basic summary only.")
366
350
 
367
351
  return "\n".join(output)
368
352
 
@@ -192,9 +192,7 @@ Providers are automatically detected based on environment variables:
192
192
  for provider in available_but_disabled:
193
193
  env_vars = available_providers.get(provider, [])
194
194
  output.append(f" - {provider}: {', '.join(env_vars)}")
195
- output.append(
196
- f" Use: llm_manage --action enable --provider {provider}"
197
- )
195
+ output.append(f" Use: llm_manage --action enable --provider {provider}")
198
196
  output.append("")
199
197
 
200
198
  # Show providers without API keys
@@ -356,9 +354,7 @@ Providers are automatically detected based on environment variables:
356
354
  except Exception as e:
357
355
  return f"Error listing models: {str(e)}"
358
356
 
359
- async def _test_model(
360
- self, ctx: MCPContext, provider: Optional[str], model: Optional[str]
361
- ) -> str:
357
+ async def _test_model(self, ctx: MCPContext, provider: Optional[str], model: Optional[str]) -> str:
362
358
  """Test a model to verify it works."""
363
359
  if not model and not provider:
364
360
  return "Error: Either model or provider is required for test action"
@@ -294,9 +294,7 @@ Available: {", ".join(available) if available else "None"}"""
294
294
  # Other actions can fall through to regular path if available
295
295
 
296
296
  if not LITELLM_AVAILABLE:
297
- return (
298
- "Error: LiteLLM is not installed. Install it with: pip install litellm"
299
- )
297
+ return "Error: LiteLLM is not installed. Install it with: pip install litellm"
300
298
 
301
299
  # Extract action
302
300
  action = params.get("action", "query")
@@ -315,9 +313,7 @@ Available: {", ".join(available) if available else "None"}"""
315
313
  elif action == "disable":
316
314
  return self._handle_disable(params.get("provider"))
317
315
  elif action == "test":
318
- return await self._handle_test(
319
- tool_ctx, params.get("model"), params.get("provider")
320
- )
316
+ return await self._handle_test(tool_ctx, params.get("model"), params.get("provider"))
321
317
  else:
322
318
  return f"Error: Unknown action '{action}'. Valid actions: query, consensus, list, models, enable, disable, test"
323
319
 
@@ -407,9 +403,7 @@ Available: {", ".join(available) if available else "None"}"""
407
403
  models = params.get("models")
408
404
  if not models:
409
405
  # Use configured or default models
410
- consensus_size = params.get("consensus_size") or self.config.get(
411
- "consensus_size", 3
412
- )
406
+ consensus_size = params.get("consensus_size") or self.config.get("consensus_size", 3)
413
407
  models = self._get_consensus_models(consensus_size)
414
408
 
415
409
  if not models:
@@ -449,11 +443,7 @@ Available: {", ".join(available) if available else "None"}"""
449
443
  if devil_model:
450
444
  # Create devil's advocate prompt
451
445
  responses_text = "\n\n".join(
452
- [
453
- f"Model {i + 1}: {resp['response']}"
454
- for i, resp in enumerate(responses)
455
- if resp["response"]
456
- ]
446
+ [f"Model {i + 1}: {resp['response']}" for i, resp in enumerate(responses) if resp["response"]]
457
447
  )
458
448
 
459
449
  devil_prompt = f"""You are a critical analyst. Review these responses to the question below and provide a devil's advocate perspective. Challenge assumptions, point out weaknesses, and suggest alternative viewpoints.
@@ -477,14 +467,10 @@ Provide your critical analysis:"""
477
467
  }
478
468
 
479
469
  # Aggregate responses
480
- judge_model = params.get("judge_model") or self.config.get(
481
- "default_judge_model", "gpt-4o"
482
- )
470
+ judge_model = params.get("judge_model") or self.config.get("default_judge_model", "gpt-4o")
483
471
  include_raw = params.get("include_raw", False)
484
472
 
485
- return await self._aggregate_consensus(
486
- responses, prompt, judge_model, include_raw, devil_response, tool_ctx
487
- )
473
+ return await self._aggregate_consensus(responses, prompt, judge_model, include_raw, devil_response, tool_ctx)
488
474
 
489
475
  def _handle_list(self) -> str:
490
476
  """List available providers."""
@@ -518,9 +504,7 @@ Provide your critical analysis:"""
518
504
  output.append(f"{provider}: {status}")
519
505
  output.append(f" Environment variables: {', '.join(env_vars)}")
520
506
 
521
- output.append(
522
- "\nUse 'llm --action enable/disable --provider <name>' to manage providers"
523
- )
507
+ output.append("\nUse 'llm --action enable/disable --provider <name>' to manage providers")
524
508
 
525
509
  return "\n".join(output)
526
510
 
@@ -560,16 +544,10 @@ Provide your critical analysis:"""
560
544
  # Show providers with counts
561
545
  for provider_name, models in sorted(all_models.items()):
562
546
  if models:
563
- available = (
564
- "✅" if provider_name in self.available_providers else "❌"
565
- )
566
- output.append(
567
- f"{available} {provider_name}: {len(models)} models"
568
- )
547
+ available = "✅" if provider_name in self.available_providers else "❌"
548
+ output.append(f"{available} {provider_name}: {len(models)} models")
569
549
 
570
- output.append(
571
- "\nUse 'llm --action models --provider <name>' to see specific models"
572
- )
550
+ output.append("\nUse 'llm --action models --provider <name>' to see specific models")
573
551
 
574
552
  return "\n".join(output)
575
553
 
@@ -608,9 +586,7 @@ Provide your critical analysis:"""
608
586
  else:
609
587
  return f"{provider} is already disabled"
610
588
 
611
- async def _handle_test(
612
- self, tool_ctx, model: Optional[str], provider: Optional[str]
613
- ) -> str:
589
+ async def _handle_test(self, tool_ctx, model: Optional[str], provider: Optional[str]) -> str:
614
590
  """Test a model or provider."""
615
591
  if not model and not provider:
616
592
  return "Error: Either model or provider is required for test action"
@@ -666,11 +642,7 @@ Provide your critical analysis:"""
666
642
  break
667
643
 
668
644
  provider = self._get_provider_for_model(model)
669
- if (
670
- provider
671
- and provider in self.available_providers
672
- and provider not in disabled
673
- ):
645
+ if provider and provider in self.available_providers and provider not in disabled:
674
646
  models.append(model)
675
647
 
676
648
  # If still need more, add from available providers
@@ -703,9 +675,7 @@ Provide your critical analysis:"""
703
675
  """Query multiple models in parallel."""
704
676
 
705
677
  async def query_with_info(model: str) -> Dict[str, Any]:
706
- result = await self._query_single_model(
707
- model, prompt, system_prompt, temperature, max_tokens
708
- )
678
+ result = await self._query_single_model(model, prompt, system_prompt, temperature, max_tokens)
709
679
  return {
710
680
  "model": model,
711
681
  "response": result.get("response"),
@@ -784,12 +754,7 @@ Provide your critical analysis:"""
784
754
  return "Error: All models failed to respond"
785
755
 
786
756
  # Format responses for aggregation
787
- responses_text = "\n\n".join(
788
- [
789
- f"Model: {r['model']}\nResponse: {r['response']}"
790
- for r in successful_responses
791
- ]
792
- )
757
+ responses_text = "\n\n".join([f"Model: {r['model']}\nResponse: {r['response']}" for r in successful_responses])
793
758
 
794
759
  if devil_response:
795
760
  responses_text += f"\n\nDevil's Advocate ({devil_response['model']}):\n{devil_response['response']}"
@@ -818,23 +783,17 @@ Be concise and highlight the most important findings."""
818
783
  if tool_ctx:
819
784
  await tool_ctx.info(f"Aggregating responses with {judge_model}...")
820
785
 
821
- judge_result = await self._query_single_model(
822
- judge_model, aggregation_prompt, None, 0.3, None
823
- )
786
+ judge_result = await self._query_single_model(judge_model, aggregation_prompt, None, 0.3, None)
824
787
 
825
788
  if not judge_result["success"]:
826
789
  return f"Error: Judge model failed: {judge_result.get('error', 'Unknown error')}"
827
790
 
828
791
  # Format output
829
- output = [
830
- f"=== Consensus Analysis ({len(successful_responses)} models) ===\n"
831
- ]
792
+ output = [f"=== Consensus Analysis ({len(successful_responses)} models) ===\n"]
832
793
  output.append(judge_result["response"])
833
794
 
834
795
  # Add model list
835
- output.append(
836
- f"\nModels consulted: {', '.join([r['model'] for r in successful_responses])}"
837
- )
796
+ output.append(f"\nModels consulted: {', '.join([r['model'] for r in successful_responses])}")
838
797
  if devil_response:
839
798
  output.append(f"Devil's Advocate: {devil_response['model']}")
840
799
 
@@ -260,7 +260,7 @@ llm "Explain this code" --model gpt-4o
260
260
  llm --action consensus "Is this approach correct?" --devils-advocate
261
261
  llm --action models --provider openai
262
262
 
263
- Available: {', '.join(available) if available else 'None'}"""
263
+ Available: {", ".join(available) if available else "None"}"""
264
264
 
265
265
  @override
266
266
  async def call(
@@ -281,9 +281,7 @@ Available: {', '.join(available) if available else 'None'}"""
281
281
  pass
282
282
 
283
283
  if not LITELLM_AVAILABLE:
284
- return (
285
- "Error: LiteLLM is not installed. Install it with: pip install litellm"
286
- )
284
+ return "Error: LiteLLM is not installed. Install it with: pip install litellm"
287
285
 
288
286
  # Extract action
289
287
  action = params.get("action", "query")
@@ -302,9 +300,7 @@ Available: {', '.join(available) if available else 'None'}"""
302
300
  elif action == "disable":
303
301
  return self._handle_disable(params.get("provider"))
304
302
  elif action == "test":
305
- return await self._handle_test(
306
- tool_ctx, params.get("model"), params.get("provider")
307
- )
303
+ return await self._handle_test(tool_ctx, params.get("model"), params.get("provider"))
308
304
  else:
309
305
  return f"Error: Unknown action '{action}'. Valid actions: query, consensus, list, models, enable, disable, test"
310
306
 
@@ -394,9 +390,7 @@ Available: {', '.join(available) if available else 'None'}"""
394
390
  models = params.get("models")
395
391
  if not models:
396
392
  # Use configured or default models
397
- consensus_size = params.get("consensus_size") or self.config.get(
398
- "consensus_size", 3
399
- )
393
+ consensus_size = params.get("consensus_size") or self.config.get("consensus_size", 3)
400
394
  models = self._get_consensus_models(consensus_size)
401
395
 
402
396
  if not models:
@@ -436,11 +430,7 @@ Available: {', '.join(available) if available else 'None'}"""
436
430
  if devil_model:
437
431
  # Create devil's advocate prompt
438
432
  responses_text = "\n\n".join(
439
- [
440
- f"Model {i+1}: {resp['response']}"
441
- for i, resp in enumerate(responses)
442
- if resp["response"]
443
- ]
433
+ [f"Model {i + 1}: {resp['response']}" for i, resp in enumerate(responses) if resp["response"]]
444
434
  )
445
435
 
446
436
  devil_prompt = f"""You are a critical analyst. Review these responses to the question below and provide a devil's advocate perspective. Challenge assumptions, point out weaknesses, and suggest alternative viewpoints.
@@ -464,14 +454,10 @@ Provide your critical analysis:"""
464
454
  }
465
455
 
466
456
  # Aggregate responses
467
- judge_model = params.get("judge_model") or self.config.get(
468
- "default_judge_model", "gpt-4o"
469
- )
457
+ judge_model = params.get("judge_model") or self.config.get("default_judge_model", "gpt-4o")
470
458
  include_raw = params.get("include_raw", False)
471
459
 
472
- return await self._aggregate_consensus(
473
- responses, prompt, judge_model, include_raw, devil_response, tool_ctx
474
- )
460
+ return await self._aggregate_consensus(responses, prompt, judge_model, include_raw, devil_response, tool_ctx)
475
461
 
476
462
  def _handle_list(self) -> str:
477
463
  """List available providers."""
@@ -505,9 +491,7 @@ Provide your critical analysis:"""
505
491
  output.append(f"{provider}: {status}")
506
492
  output.append(f" Environment variables: {', '.join(env_vars)}")
507
493
 
508
- output.append(
509
- "\nUse 'llm --action enable/disable --provider <name>' to manage providers"
510
- )
494
+ output.append("\nUse 'llm --action enable/disable --provider <name>' to manage providers")
511
495
 
512
496
  return "\n".join(output)
513
497
 
@@ -547,16 +531,10 @@ Provide your critical analysis:"""
547
531
  # Show providers with counts
548
532
  for provider_name, models in sorted(all_models.items()):
549
533
  if models:
550
- available = (
551
- "✅" if provider_name in self.available_providers else "❌"
552
- )
553
- output.append(
554
- f"{available} {provider_name}: {len(models)} models"
555
- )
534
+ available = "✅" if provider_name in self.available_providers else "❌"
535
+ output.append(f"{available} {provider_name}: {len(models)} models")
556
536
 
557
- output.append(
558
- "\nUse 'llm --action models --provider <name>' to see specific models"
559
- )
537
+ output.append("\nUse 'llm --action models --provider <name>' to see specific models")
560
538
 
561
539
  return "\n".join(output)
562
540
 
@@ -595,9 +573,7 @@ Provide your critical analysis:"""
595
573
  else:
596
574
  return f"{provider} is already disabled"
597
575
 
598
- async def _handle_test(
599
- self, tool_ctx, model: Optional[str], provider: Optional[str]
600
- ) -> str:
576
+ async def _handle_test(self, tool_ctx, model: Optional[str], provider: Optional[str]) -> str:
601
577
  """Test a model or provider."""
602
578
  if not model and not provider:
603
579
  return "Error: Either model or provider is required for test action"
@@ -653,11 +629,7 @@ Provide your critical analysis:"""
653
629
  break
654
630
 
655
631
  provider = self._get_provider_for_model(model)
656
- if (
657
- provider
658
- and provider in self.available_providers
659
- and provider not in disabled
660
- ):
632
+ if provider and provider in self.available_providers and provider not in disabled:
661
633
  models.append(model)
662
634
 
663
635
  # If still need more, add from available providers
@@ -690,9 +662,7 @@ Provide your critical analysis:"""
690
662
  """Query multiple models in parallel."""
691
663
 
692
664
  async def query_with_info(model: str) -> Dict[str, Any]:
693
- result = await self._query_single_model(
694
- model, prompt, system_prompt, temperature, max_tokens
695
- )
665
+ result = await self._query_single_model(model, prompt, system_prompt, temperature, max_tokens)
696
666
  return {
697
667
  "model": model,
698
668
  "response": result.get("response"),
@@ -771,12 +741,7 @@ Provide your critical analysis:"""
771
741
  return "Error: All models failed to respond"
772
742
 
773
743
  # Format responses for aggregation
774
- responses_text = "\n\n".join(
775
- [
776
- f"Model: {r['model']}\nResponse: {r['response']}"
777
- for r in successful_responses
778
- ]
779
- )
744
+ responses_text = "\n\n".join([f"Model: {r['model']}\nResponse: {r['response']}" for r in successful_responses])
780
745
 
781
746
  if devil_response:
782
747
  responses_text += f"\n\nDevil's Advocate ({devil_response['model']}):\n{devil_response['response']}"
@@ -805,23 +770,17 @@ Be concise and highlight the most important findings."""
805
770
  if tool_ctx:
806
771
  await tool_ctx.info(f"Aggregating responses with {judge_model}...")
807
772
 
808
- judge_result = await self._query_single_model(
809
- judge_model, aggregation_prompt, None, 0.3, None
810
- )
773
+ judge_result = await self._query_single_model(judge_model, aggregation_prompt, None, 0.3, None)
811
774
 
812
775
  if not judge_result["success"]:
813
776
  return f"Error: Judge model failed: {judge_result.get('error', 'Unknown error')}"
814
777
 
815
778
  # Format output
816
- output = [
817
- f"=== Consensus Analysis ({len(successful_responses)} models) ===\n"
818
- ]
779
+ output = [f"=== Consensus Analysis ({len(successful_responses)} models) ===\n"]
819
780
  output.append(judge_result["response"])
820
781
 
821
782
  # Add model list
822
- output.append(
823
- f"\nModels consulted: {', '.join([r['model'] for r in successful_responses])}"
824
- )
783
+ output.append(f"\nModels consulted: {', '.join([r['model'] for r in successful_responses])}")
825
784
  if devil_response:
826
785
  output.append(f"Devil's Advocate: {devil_response['model']}")
827
786
 
@@ -71,9 +71,7 @@ class ProviderToolParams(TypedDict, total=False):
71
71
  class BaseProviderTool(BaseTool):
72
72
  """Base class for provider-specific LLM tools."""
73
73
 
74
- def __init__(
75
- self, provider: str, default_model: str, model_variants: Dict[str, str]
76
- ):
74
+ def __init__(self, provider: str, default_model: str, model_variants: Dict[str, str]):
77
75
  """Initialize provider tool.
78
76
 
79
77
  Args: