hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.1__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 (178) hide show
  1. hanzo_mcp/__init__.py +6 -0
  2. hanzo_mcp/__main__.py +1 -1
  3. hanzo_mcp/analytics/__init__.py +2 -2
  4. hanzo_mcp/analytics/posthog_analytics.py +76 -82
  5. hanzo_mcp/cli.py +31 -36
  6. hanzo_mcp/cli_enhanced.py +94 -72
  7. hanzo_mcp/cli_plugin.py +27 -17
  8. hanzo_mcp/config/__init__.py +2 -2
  9. hanzo_mcp/config/settings.py +112 -88
  10. hanzo_mcp/config/tool_config.py +32 -34
  11. hanzo_mcp/dev_server.py +66 -67
  12. hanzo_mcp/prompts/__init__.py +94 -12
  13. hanzo_mcp/prompts/enhanced_prompts.py +809 -0
  14. hanzo_mcp/prompts/example_custom_prompt.py +6 -5
  15. hanzo_mcp/prompts/project_todo_reminder.py +0 -1
  16. hanzo_mcp/prompts/tool_explorer.py +10 -7
  17. hanzo_mcp/server.py +17 -21
  18. hanzo_mcp/server_enhanced.py +15 -22
  19. hanzo_mcp/tools/__init__.py +56 -28
  20. hanzo_mcp/tools/agent/__init__.py +16 -19
  21. hanzo_mcp/tools/agent/agent.py +82 -65
  22. hanzo_mcp/tools/agent/agent_tool.py +152 -122
  23. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
  24. hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
  25. hanzo_mcp/tools/agent/clarification_tool.py +11 -10
  26. hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
  27. hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
  28. hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
  29. hanzo_mcp/tools/agent/code_auth.py +102 -107
  30. hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
  31. hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
  32. hanzo_mcp/tools/agent/critic_tool.py +86 -73
  33. hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
  34. hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
  35. hanzo_mcp/tools/agent/iching_tool.py +404 -139
  36. hanzo_mcp/tools/agent/network_tool.py +89 -73
  37. hanzo_mcp/tools/agent/prompt.py +2 -1
  38. hanzo_mcp/tools/agent/review_tool.py +101 -98
  39. hanzo_mcp/tools/agent/swarm_alias.py +87 -0
  40. hanzo_mcp/tools/agent/swarm_tool.py +246 -161
  41. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
  42. hanzo_mcp/tools/agent/tool_adapter.py +21 -11
  43. hanzo_mcp/tools/common/__init__.py +1 -1
  44. hanzo_mcp/tools/common/base.py +3 -5
  45. hanzo_mcp/tools/common/batch_tool.py +46 -39
  46. hanzo_mcp/tools/common/config_tool.py +120 -84
  47. hanzo_mcp/tools/common/context.py +1 -5
  48. hanzo_mcp/tools/common/context_fix.py +5 -3
  49. hanzo_mcp/tools/common/critic_tool.py +4 -8
  50. hanzo_mcp/tools/common/decorators.py +58 -56
  51. hanzo_mcp/tools/common/enhanced_base.py +29 -32
  52. hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
  53. hanzo_mcp/tools/common/forgiving_edit.py +91 -87
  54. hanzo_mcp/tools/common/mode.py +15 -17
  55. hanzo_mcp/tools/common/mode_loader.py +27 -24
  56. hanzo_mcp/tools/common/paginated_base.py +61 -53
  57. hanzo_mcp/tools/common/paginated_response.py +72 -79
  58. hanzo_mcp/tools/common/pagination.py +50 -53
  59. hanzo_mcp/tools/common/permissions.py +4 -4
  60. hanzo_mcp/tools/common/personality.py +186 -138
  61. hanzo_mcp/tools/common/plugin_loader.py +54 -54
  62. hanzo_mcp/tools/common/stats.py +65 -47
  63. hanzo_mcp/tools/common/test_helpers.py +31 -0
  64. hanzo_mcp/tools/common/thinking_tool.py +4 -8
  65. hanzo_mcp/tools/common/tool_disable.py +17 -12
  66. hanzo_mcp/tools/common/tool_enable.py +13 -14
  67. hanzo_mcp/tools/common/tool_list.py +36 -28
  68. hanzo_mcp/tools/common/truncate.py +23 -23
  69. hanzo_mcp/tools/config/__init__.py +4 -4
  70. hanzo_mcp/tools/config/config_tool.py +42 -29
  71. hanzo_mcp/tools/config/index_config.py +37 -34
  72. hanzo_mcp/tools/config/mode_tool.py +175 -55
  73. hanzo_mcp/tools/database/__init__.py +15 -12
  74. hanzo_mcp/tools/database/database_manager.py +77 -75
  75. hanzo_mcp/tools/database/graph.py +137 -91
  76. hanzo_mcp/tools/database/graph_add.py +30 -18
  77. hanzo_mcp/tools/database/graph_query.py +178 -102
  78. hanzo_mcp/tools/database/graph_remove.py +33 -28
  79. hanzo_mcp/tools/database/graph_search.py +97 -75
  80. hanzo_mcp/tools/database/graph_stats.py +91 -59
  81. hanzo_mcp/tools/database/sql.py +107 -79
  82. hanzo_mcp/tools/database/sql_query.py +30 -24
  83. hanzo_mcp/tools/database/sql_search.py +29 -25
  84. hanzo_mcp/tools/database/sql_stats.py +47 -35
  85. hanzo_mcp/tools/editor/neovim_command.py +25 -28
  86. hanzo_mcp/tools/editor/neovim_edit.py +21 -23
  87. hanzo_mcp/tools/editor/neovim_session.py +60 -54
  88. hanzo_mcp/tools/filesystem/__init__.py +31 -30
  89. hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
  90. hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
  91. hanzo_mcp/tools/filesystem/base.py +1 -1
  92. hanzo_mcp/tools/filesystem/batch_search.py +316 -224
  93. hanzo_mcp/tools/filesystem/content_replace.py +4 -4
  94. hanzo_mcp/tools/filesystem/diff.py +71 -59
  95. hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
  96. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
  97. hanzo_mcp/tools/filesystem/edit.py +4 -4
  98. hanzo_mcp/tools/filesystem/find.py +173 -80
  99. hanzo_mcp/tools/filesystem/find_files.py +73 -52
  100. hanzo_mcp/tools/filesystem/git_search.py +157 -104
  101. hanzo_mcp/tools/filesystem/grep.py +8 -8
  102. hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
  103. hanzo_mcp/tools/filesystem/read.py +12 -10
  104. hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
  105. hanzo_mcp/tools/filesystem/search_tool.py +263 -207
  106. hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
  107. hanzo_mcp/tools/filesystem/tree.py +35 -33
  108. hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
  109. hanzo_mcp/tools/filesystem/watch.py +37 -36
  110. hanzo_mcp/tools/filesystem/write.py +4 -8
  111. hanzo_mcp/tools/jupyter/__init__.py +4 -4
  112. hanzo_mcp/tools/jupyter/base.py +4 -5
  113. hanzo_mcp/tools/jupyter/jupyter.py +67 -47
  114. hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
  115. hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
  116. hanzo_mcp/tools/llm/__init__.py +5 -7
  117. hanzo_mcp/tools/llm/consensus_tool.py +72 -52
  118. hanzo_mcp/tools/llm/llm_manage.py +101 -60
  119. hanzo_mcp/tools/llm/llm_tool.py +226 -166
  120. hanzo_mcp/tools/llm/provider_tools.py +25 -26
  121. hanzo_mcp/tools/lsp/__init__.py +1 -1
  122. hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
  123. hanzo_mcp/tools/mcp/__init__.py +2 -3
  124. hanzo_mcp/tools/mcp/mcp_add.py +27 -25
  125. hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
  126. hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
  127. hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
  128. hanzo_mcp/tools/memory/__init__.py +39 -21
  129. hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
  130. hanzo_mcp/tools/memory/memory_tools.py +90 -108
  131. hanzo_mcp/tools/search/__init__.py +7 -2
  132. hanzo_mcp/tools/search/find_tool.py +297 -212
  133. hanzo_mcp/tools/search/unified_search.py +366 -314
  134. hanzo_mcp/tools/shell/__init__.py +8 -7
  135. hanzo_mcp/tools/shell/auto_background.py +56 -49
  136. hanzo_mcp/tools/shell/base.py +1 -1
  137. hanzo_mcp/tools/shell/base_process.py +75 -75
  138. hanzo_mcp/tools/shell/bash_session.py +2 -2
  139. hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
  140. hanzo_mcp/tools/shell/bash_tool.py +24 -31
  141. hanzo_mcp/tools/shell/command_executor.py +12 -12
  142. hanzo_mcp/tools/shell/logs.py +43 -33
  143. hanzo_mcp/tools/shell/npx.py +13 -13
  144. hanzo_mcp/tools/shell/npx_background.py +24 -21
  145. hanzo_mcp/tools/shell/npx_tool.py +18 -22
  146. hanzo_mcp/tools/shell/open.py +19 -21
  147. hanzo_mcp/tools/shell/pkill.py +31 -26
  148. hanzo_mcp/tools/shell/process_tool.py +32 -32
  149. hanzo_mcp/tools/shell/processes.py +57 -58
  150. hanzo_mcp/tools/shell/run_background.py +24 -25
  151. hanzo_mcp/tools/shell/run_command.py +5 -5
  152. hanzo_mcp/tools/shell/run_command_windows.py +5 -5
  153. hanzo_mcp/tools/shell/session_storage.py +3 -3
  154. hanzo_mcp/tools/shell/streaming_command.py +141 -126
  155. hanzo_mcp/tools/shell/uvx.py +24 -25
  156. hanzo_mcp/tools/shell/uvx_background.py +35 -33
  157. hanzo_mcp/tools/shell/uvx_tool.py +18 -22
  158. hanzo_mcp/tools/todo/__init__.py +6 -2
  159. hanzo_mcp/tools/todo/todo.py +50 -37
  160. hanzo_mcp/tools/todo/todo_read.py +5 -8
  161. hanzo_mcp/tools/todo/todo_write.py +5 -7
  162. hanzo_mcp/tools/vector/__init__.py +40 -28
  163. hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
  164. hanzo_mcp/tools/vector/git_ingester.py +170 -179
  165. hanzo_mcp/tools/vector/index_tool.py +96 -44
  166. hanzo_mcp/tools/vector/infinity_store.py +283 -228
  167. hanzo_mcp/tools/vector/mock_infinity.py +39 -40
  168. hanzo_mcp/tools/vector/project_manager.py +88 -78
  169. hanzo_mcp/tools/vector/vector.py +59 -42
  170. hanzo_mcp/tools/vector/vector_index.py +30 -27
  171. hanzo_mcp/tools/vector/vector_search.py +64 -45
  172. hanzo_mcp/types.py +6 -4
  173. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/METADATA +1 -1
  174. hanzo_mcp-0.8.1.dist-info/RECORD +185 -0
  175. hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
  176. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/WHEEL +0 -0
  177. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/entry_points.txt +0 -0
  178. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,23 @@
1
1
  """Unified LLM tool with multiple actions including consensus mode."""
2
2
 
3
- from typing import Annotated, TypedDict, Unpack, final, override, Optional, List, Dict, Any
4
- import asyncio
5
3
  import os
6
4
  import json
5
+ import asyncio
6
+ from typing import (
7
+ Any,
8
+ Dict,
9
+ List,
10
+ Unpack,
11
+ Optional,
12
+ Annotated,
13
+ TypedDict,
14
+ final,
15
+ override,
16
+ )
7
17
  from pathlib import Path
8
18
 
9
- from mcp.server.fastmcp import Context as MCPContext
10
19
  from pydantic import Field
20
+ from mcp.server.fastmcp import Context as MCPContext
11
21
 
12
22
  from hanzo_mcp.tools.common.base import BaseTool
13
23
  from hanzo_mcp.tools.common.context import create_tool_context
@@ -15,6 +25,7 @@ from hanzo_mcp.tools.common.context import create_tool_context
15
25
  # Check if litellm is available
16
26
  try:
17
27
  import litellm
28
+
18
29
  LITELLM_AVAILABLE = True
19
30
  except ImportError:
20
31
  LITELLM_AVAILABLE = False
@@ -136,6 +147,7 @@ ConsensusSize = Annotated[
136
147
 
137
148
  class LLMParams(TypedDict, total=False):
138
149
  """Parameters for LLM tool."""
150
+
139
151
  action: str
140
152
  model: Optional[str]
141
153
  models: Optional[List[str]]
@@ -155,10 +167,10 @@ class LLMParams(TypedDict, total=False):
155
167
  @final
156
168
  class LLMTool(BaseTool):
157
169
  """Unified LLM tool with multiple actions."""
158
-
170
+
159
171
  # Config file for settings
160
172
  CONFIG_FILE = Path.home() / ".hanzo" / "mcp" / "llm_config.json"
161
-
173
+
162
174
  # Default consensus models in order of preference
163
175
  DEFAULT_CONSENSUS_MODELS = [
164
176
  "gpt-4o", # OpenAI's latest
@@ -168,7 +180,7 @@ class LLMTool(BaseTool):
168
180
  "mistral/mistral-large-latest", # Mistral's best
169
181
  "perplexity/llama-3.1-sonar-large-128k-chat", # Perplexity with search
170
182
  ]
171
-
183
+
172
184
  # API key environment variables
173
185
  API_KEY_ENV_VARS = {
174
186
  "openai": ["OPENAI_API_KEY"],
@@ -187,33 +199,33 @@ class LLMTool(BaseTool):
187
199
  "voyage": ["VOYAGE_API_KEY"],
188
200
  "deepseek": ["DEEPSEEK_API_KEY"],
189
201
  }
190
-
202
+
191
203
  def __init__(self):
192
204
  """Initialize the unified LLM tool."""
193
205
  self.available_providers = self._detect_available_providers()
194
206
  self.config = self._load_config()
195
-
207
+
196
208
  def _detect_available_providers(self) -> Dict[str, List[str]]:
197
209
  """Detect which providers have API keys configured."""
198
210
  available = {}
199
-
211
+
200
212
  for provider, env_vars in self.API_KEY_ENV_VARS.items():
201
213
  for var in env_vars:
202
214
  if os.getenv(var):
203
215
  available[provider] = env_vars
204
216
  break
205
-
217
+
206
218
  return available
207
-
219
+
208
220
  def _load_config(self) -> Dict[str, Any]:
209
221
  """Load configuration from file."""
210
222
  if self.CONFIG_FILE.exists():
211
223
  try:
212
- with open(self.CONFIG_FILE, 'r') as f:
224
+ with open(self.CONFIG_FILE, "r") as f:
213
225
  return json.load(f)
214
- except:
226
+ except Exception:
215
227
  pass
216
-
228
+
217
229
  # Default config
218
230
  return {
219
231
  "disabled_providers": [],
@@ -221,11 +233,11 @@ class LLMTool(BaseTool):
221
233
  "default_judge_model": "gpt-4o",
222
234
  "consensus_size": 3,
223
235
  }
224
-
236
+
225
237
  def _save_config(self):
226
238
  """Save configuration to file."""
227
239
  self.CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
228
- with open(self.CONFIG_FILE, 'w') as f:
240
+ with open(self.CONFIG_FILE, "w") as f:
229
241
  json.dump(self.config, f, indent=2)
230
242
 
231
243
  @property
@@ -239,7 +251,7 @@ class LLMTool(BaseTool):
239
251
  def description(self) -> str:
240
252
  """Get the tool description."""
241
253
  available = list(self.available_providers.keys())
242
-
254
+
243
255
  return f"""Query LLMs. Default: single query. Actions: consensus, list, models, test.
244
256
 
245
257
  Usage:
@@ -248,7 +260,7 @@ llm "Explain this code" --model gpt-4o
248
260
  llm --action consensus "Is this approach correct?" --devils-advocate
249
261
  llm --action models --provider openai
250
262
 
251
- Available: {', '.join(available) if available else 'None'}"""
263
+ Available: {", ".join(available) if available else "None"}"""
252
264
 
253
265
  @override
254
266
  async def call(
@@ -260,20 +272,22 @@ Available: {', '.join(available) if available else 'None'}"""
260
272
  # Create tool context only if we have a proper MCP context
261
273
  tool_ctx = None
262
274
  try:
263
- if hasattr(ctx, 'client') and ctx.client and hasattr(ctx.client, 'server'):
275
+ if hasattr(ctx, "client") and ctx.client and hasattr(ctx.client, "server"):
264
276
  tool_ctx = create_tool_context(ctx)
265
277
  if tool_ctx:
266
278
  await tool_ctx.set_tool_info(self.name)
267
- except:
279
+ except Exception:
268
280
  # Running in test mode without MCP context
269
281
  pass
270
282
 
271
283
  if not LITELLM_AVAILABLE:
272
- return "Error: LiteLLM is not installed. Install it with: pip install litellm"
284
+ return (
285
+ "Error: LiteLLM is not installed. Install it with: pip install litellm"
286
+ )
273
287
 
274
288
  # Extract action
275
289
  action = params.get("action", "query")
276
-
290
+
277
291
  # Route to appropriate handler
278
292
  if action == "query":
279
293
  return await self._handle_query(tool_ctx, params)
@@ -288,7 +302,9 @@ Available: {', '.join(available) if available else 'None'}"""
288
302
  elif action == "disable":
289
303
  return self._handle_disable(params.get("provider"))
290
304
  elif action == "test":
291
- return await self._handle_test(tool_ctx, params.get("model"), params.get("provider"))
305
+ return await self._handle_test(
306
+ tool_ctx, params.get("model"), params.get("provider")
307
+ )
292
308
  else:
293
309
  return f"Error: Unknown action '{action}'. Valid actions: query, consensus, list, models, enable, disable, test"
294
310
 
@@ -296,10 +312,10 @@ Available: {', '.join(available) if available else 'None'}"""
296
312
  """Handle single model query."""
297
313
  model = params.get("model")
298
314
  prompt = params.get("prompt")
299
-
315
+
300
316
  if not prompt:
301
317
  return "Error: prompt is required for query action"
302
-
318
+
303
319
  # Auto-select model if not specified
304
320
  if not model:
305
321
  if self.available_providers:
@@ -316,39 +332,39 @@ Available: {', '.join(available) if available else 'None'}"""
316
332
  model = f"{provider}/default"
317
333
  else:
318
334
  return "Error: No model specified and no API keys found"
319
-
335
+
320
336
  # Check if we have API key for this model
321
337
  provider = self._get_provider_for_model(model)
322
338
  if provider and provider not in self.available_providers:
323
339
  env_vars = self.API_KEY_ENV_VARS.get(provider, [])
324
340
  return f"Error: No API key found for {provider}. Set one of: {', '.join(env_vars)}"
325
-
341
+
326
342
  # Build messages
327
343
  messages = []
328
344
  if params.get("system_prompt"):
329
345
  messages.append({"role": "system", "content": params["system_prompt"]})
330
346
  messages.append({"role": "user", "content": prompt})
331
-
347
+
332
348
  # Build kwargs
333
349
  kwargs = {
334
350
  "model": model,
335
351
  "messages": messages,
336
352
  "temperature": params.get("temperature", 0.7),
337
353
  }
338
-
354
+
339
355
  if params.get("max_tokens"):
340
356
  kwargs["max_tokens"] = params["max_tokens"]
341
-
357
+
342
358
  if params.get("json_mode"):
343
359
  kwargs["response_format"] = {"type": "json_object"}
344
-
360
+
345
361
  if params.get("stream"):
346
362
  kwargs["stream"] = True
347
-
363
+
348
364
  try:
349
365
  if tool_ctx:
350
366
  await tool_ctx.info(f"Querying {model}...")
351
-
367
+
352
368
  if kwargs.get("stream"):
353
369
  # Handle streaming response
354
370
  response_text = ""
@@ -360,7 +376,7 @@ Available: {', '.join(available) if available else 'None'}"""
360
376
  # Regular response
361
377
  response = await litellm.acompletion(**kwargs)
362
378
  return response.choices[0].message.content
363
-
379
+
364
380
  except Exception as e:
365
381
  error_msg = str(e)
366
382
  if "model_not_found" in error_msg or "does not exist" in error_msg:
@@ -373,33 +389,35 @@ Available: {', '.join(available) if available else 'None'}"""
373
389
  prompt = params.get("prompt")
374
390
  if not prompt:
375
391
  return "Error: prompt is required for consensus action"
376
-
392
+
377
393
  # Determine models to use
378
394
  models = params.get("models")
379
395
  if not models:
380
396
  # Use configured or default models
381
- consensus_size = params.get("consensus_size") or self.config.get("consensus_size", 3)
397
+ consensus_size = params.get("consensus_size") or self.config.get(
398
+ "consensus_size", 3
399
+ )
382
400
  models = self._get_consensus_models(consensus_size)
383
-
401
+
384
402
  if not models:
385
403
  return "Error: No models available for consensus. Set API keys for at least 2 providers."
386
-
404
+
387
405
  if len(models) < 2:
388
406
  return "Error: Consensus requires at least 2 models"
389
-
407
+
390
408
  # Check for devil's advocate mode
391
409
  devils_advocate = params.get("devils_advocate", False)
392
410
  if devils_advocate and len(models) < 3:
393
411
  return "Error: Devil's advocate mode requires at least 3 models"
394
-
412
+
395
413
  if tool_ctx:
396
414
  await tool_ctx.info(f"Running consensus with {len(models)} models...")
397
-
415
+
398
416
  # Query models in parallel
399
417
  system_prompt = params.get("system_prompt")
400
418
  temperature = params.get("temperature", 0.7)
401
419
  max_tokens = params.get("max_tokens")
402
-
420
+
403
421
  # Split models if using devil's advocate
404
422
  if devils_advocate:
405
423
  consensus_models = models[:-1]
@@ -407,21 +425,24 @@ Available: {', '.join(available) if available else 'None'}"""
407
425
  else:
408
426
  consensus_models = models
409
427
  devil_model = None
410
-
428
+
411
429
  # Query consensus models
412
430
  responses = await self._query_models_parallel(
413
431
  consensus_models, prompt, system_prompt, temperature, max_tokens, tool_ctx
414
432
  )
415
-
433
+
416
434
  # Get devil's advocate response if enabled
417
435
  devil_response = None
418
436
  if devil_model:
419
437
  # Create devil's advocate prompt
420
- responses_text = "\n\n".join([
421
- f"Model {i+1}: {resp['response']}"
422
- for i, resp in enumerate(responses) if resp['response']
423
- ])
424
-
438
+ 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
+ ]
444
+ )
445
+
425
446
  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.
426
447
 
427
448
  Original Question: {prompt}
@@ -430,22 +451,24 @@ Responses from other models:
430
451
  {responses_text}
431
452
 
432
453
  Provide your critical analysis:"""
433
-
454
+
434
455
  devil_result = await self._query_single_model(
435
456
  devil_model, devil_prompt, system_prompt, temperature, max_tokens
436
457
  )
437
-
438
- if devil_result['success']:
458
+
459
+ if devil_result["success"]:
439
460
  devil_response = {
440
- 'model': devil_model,
441
- 'response': devil_result['response'],
442
- 'time_ms': devil_result['time_ms']
461
+ "model": devil_model,
462
+ "response": devil_result["response"],
463
+ "time_ms": devil_result["time_ms"],
443
464
  }
444
-
465
+
445
466
  # Aggregate responses
446
- judge_model = params.get("judge_model") or self.config.get("default_judge_model", "gpt-4o")
467
+ judge_model = params.get("judge_model") or self.config.get(
468
+ "default_judge_model", "gpt-4o"
469
+ )
447
470
  include_raw = params.get("include_raw", False)
448
-
471
+
449
472
  return await self._aggregate_consensus(
450
473
  responses, prompt, judge_model, include_raw, devil_response, tool_ctx
451
474
  )
@@ -453,64 +476,66 @@ Provide your critical analysis:"""
453
476
  def _handle_list(self) -> str:
454
477
  """List available providers."""
455
478
  output = ["=== LLM Providers ==="]
456
-
479
+
457
480
  # Get all possible providers
458
481
  all_providers = sorted(self.API_KEY_ENV_VARS.keys())
459
482
  disabled = self.config.get("disabled_providers", [])
460
-
483
+
461
484
  output.append(f"Total providers: {len(all_providers)}")
462
485
  output.append(f"Available: {len(self.available_providers)}")
463
486
  output.append(f"Disabled: {len(disabled)}\n")
464
-
487
+
465
488
  for provider in all_providers:
466
489
  status_parts = []
467
-
490
+
468
491
  # Check if API key exists
469
492
  if provider in self.available_providers:
470
493
  status_parts.append("✅ API key found")
471
494
  else:
472
495
  status_parts.append("❌ No API key")
473
-
496
+
474
497
  # Check if disabled
475
498
  if provider in disabled:
476
499
  status_parts.append("🚫 Disabled")
477
-
500
+
478
501
  # Show environment variables
479
502
  env_vars = self.API_KEY_ENV_VARS.get(provider, [])
480
503
  status = " | ".join(status_parts)
481
-
504
+
482
505
  output.append(f"{provider}: {status}")
483
506
  output.append(f" Environment variables: {', '.join(env_vars)}")
484
-
485
- output.append("\nUse 'llm --action enable/disable --provider <name>' to manage providers")
486
-
507
+
508
+ output.append(
509
+ "\nUse 'llm --action enable/disable --provider <name>' to manage providers"
510
+ )
511
+
487
512
  return "\n".join(output)
488
513
 
489
514
  def _handle_models(self, provider: Optional[str] = None) -> str:
490
515
  """List available models."""
491
516
  try:
492
517
  all_models = self._get_all_models()
493
-
518
+
494
519
  if not all_models:
495
520
  return "No models available or LiteLLM not properly initialized"
496
-
521
+
497
522
  output = ["=== Available LLM Models ==="]
498
-
523
+
499
524
  if provider:
500
525
  # Show models for specific provider
501
526
  provider_lower = provider.lower()
502
527
  models = all_models.get(provider_lower, [])
503
-
528
+
504
529
  if not models:
505
530
  return f"No models found for provider '{provider}'"
506
-
531
+
507
532
  output.append(f"\n{provider.upper()} ({len(models)} models):")
508
533
  output.append("-" * 40)
509
-
534
+
510
535
  # Show first 50 models
511
536
  for model in models[:50]:
512
537
  output.append(f" {model}")
513
-
538
+
514
539
  if len(models) > 50:
515
540
  output.append(f" ... and {len(models) - 50} more")
516
541
  else:
@@ -518,17 +543,23 @@ Provide your critical analysis:"""
518
543
  total_models = sum(len(models) for models in all_models.values())
519
544
  output.append(f"Total models available: {total_models}")
520
545
  output.append("")
521
-
546
+
522
547
  # Show providers with counts
523
548
  for provider_name, models in sorted(all_models.items()):
524
549
  if models:
525
- available = "✅" if provider_name in self.available_providers else "❌"
526
- output.append(f"{available} {provider_name}: {len(models)} models")
527
-
528
- output.append("\nUse 'llm --action models --provider <name>' to see specific models")
529
-
550
+ available = (
551
+ "✅" if provider_name in self.available_providers else "❌"
552
+ )
553
+ output.append(
554
+ f"{available} {provider_name}: {len(models)} models"
555
+ )
556
+
557
+ output.append(
558
+ "\nUse 'llm --action models --provider <name>' to see specific models"
559
+ )
560
+
530
561
  return "\n".join(output)
531
-
562
+
532
563
  except Exception as e:
533
564
  return f"Error listing models: {str(e)}"
534
565
 
@@ -536,10 +567,10 @@ Provide your critical analysis:"""
536
567
  """Enable a provider."""
537
568
  if not provider:
538
569
  return "Error: provider is required for enable action"
539
-
570
+
540
571
  provider = provider.lower()
541
572
  disabled = self.config.get("disabled_providers", [])
542
-
573
+
543
574
  if provider in disabled:
544
575
  disabled.remove(provider)
545
576
  self.config["disabled_providers"] = disabled
@@ -552,10 +583,10 @@ Provide your critical analysis:"""
552
583
  """Disable a provider."""
553
584
  if not provider:
554
585
  return "Error: provider is required for disable action"
555
-
586
+
556
587
  provider = provider.lower()
557
588
  disabled = self.config.get("disabled_providers", [])
558
-
589
+
559
590
  if provider not in disabled:
560
591
  disabled.append(provider)
561
592
  self.config["disabled_providers"] = disabled
@@ -564,11 +595,13 @@ Provide your critical analysis:"""
564
595
  else:
565
596
  return f"{provider} is already disabled"
566
597
 
567
- async def _handle_test(self, tool_ctx, model: Optional[str], provider: Optional[str]) -> str:
598
+ async def _handle_test(
599
+ self, tool_ctx, model: Optional[str], provider: Optional[str]
600
+ ) -> str:
568
601
  """Test a model or provider."""
569
602
  if not model and not provider:
570
603
  return "Error: Either model or provider is required for test action"
571
-
604
+
572
605
  # If provider specified, test its default model
573
606
  if provider and not model:
574
607
  provider = provider.lower()
@@ -582,24 +615,24 @@ Provide your critical analysis:"""
582
615
  model = "groq/llama3-8b-8192"
583
616
  else:
584
617
  model = f"{provider}/default"
585
-
618
+
586
619
  # Test the model
587
620
  test_prompt = "Say 'Hello from Hanzo AI!' in exactly 5 words."
588
-
621
+
589
622
  try:
590
623
  if tool_ctx:
591
624
  await tool_ctx.info(f"Testing {model}...")
592
-
625
+
593
626
  response = await litellm.acompletion(
594
627
  model=model,
595
628
  messages=[{"role": "user", "content": test_prompt}],
596
629
  temperature=0,
597
- max_tokens=20
630
+ max_tokens=20,
598
631
  )
599
-
632
+
600
633
  result = response.choices[0].message.content
601
634
  return f"✅ {model} is working!\nResponse: {result}"
602
-
635
+
603
636
  except Exception as e:
604
637
  return f"❌ {model} failed: {str(e)}"
605
638
 
@@ -609,78 +642,95 @@ Provide your critical analysis:"""
609
642
  configured = self.config.get("consensus_models")
610
643
  if configured:
611
644
  return configured[:size]
612
-
645
+
613
646
  # Otherwise, build list from available providers
614
647
  models = []
615
648
  disabled = self.config.get("disabled_providers", [])
616
-
649
+
617
650
  # Try default models first
618
651
  for model in self.DEFAULT_CONSENSUS_MODELS:
619
652
  if len(models) >= size:
620
653
  break
621
-
654
+
622
655
  provider = self._get_provider_for_model(model)
623
- if provider and provider in self.available_providers and provider not in disabled:
656
+ if (
657
+ provider
658
+ and provider in self.available_providers
659
+ and provider not in disabled
660
+ ):
624
661
  models.append(model)
625
-
662
+
626
663
  # If still need more, add from available providers
627
664
  if len(models) < size:
628
665
  for provider in self.available_providers:
629
666
  if provider in disabled:
630
667
  continue
631
-
668
+
632
669
  if provider == "openai" and "gpt-4o" not in models:
633
670
  models.append("gpt-4o")
634
671
  elif provider == "anthropic" and "claude-3-opus-20240229" not in models:
635
672
  models.append("claude-3-opus-20240229")
636
673
  elif provider == "google" and "gemini/gemini-1.5-pro" not in models:
637
674
  models.append("gemini/gemini-1.5-pro")
638
-
675
+
639
676
  if len(models) >= size:
640
677
  break
641
-
678
+
642
679
  return models
643
680
 
644
681
  async def _query_models_parallel(
645
- self, models: List[str], prompt: str, system_prompt: Optional[str],
646
- temperature: float, max_tokens: Optional[int], tool_ctx
682
+ self,
683
+ models: List[str],
684
+ prompt: str,
685
+ system_prompt: Optional[str],
686
+ temperature: float,
687
+ max_tokens: Optional[int],
688
+ tool_ctx,
647
689
  ) -> List[Dict[str, Any]]:
648
690
  """Query multiple models in parallel."""
691
+
649
692
  async def query_with_info(model: str) -> Dict[str, Any]:
650
- result = await self._query_single_model(model, prompt, system_prompt, temperature, max_tokens)
693
+ result = await self._query_single_model(
694
+ model, prompt, system_prompt, temperature, max_tokens
695
+ )
651
696
  return {
652
- 'model': model,
653
- 'response': result.get('response'),
654
- 'success': result.get('success', False),
655
- 'error': result.get('error'),
656
- 'time_ms': result.get('time_ms', 0)
697
+ "model": model,
698
+ "response": result.get("response"),
699
+ "success": result.get("success", False),
700
+ "error": result.get("error"),
701
+ "time_ms": result.get("time_ms", 0),
657
702
  }
658
-
703
+
659
704
  # Run all queries in parallel
660
705
  tasks = [query_with_info(model) for model in models]
661
706
  results = await asyncio.gather(*tasks)
662
-
707
+
663
708
  # Report results
664
- successful = sum(1 for r in results if r['success'])
709
+ successful = sum(1 for r in results if r["success"])
665
710
  if tool_ctx:
666
711
  await tool_ctx.info(f"Completed {successful}/{len(models)} model queries")
667
-
712
+
668
713
  return results
669
714
 
670
715
  async def _query_single_model(
671
- self, model: str, prompt: str, system_prompt: Optional[str],
672
- temperature: float, max_tokens: Optional[int]
716
+ self,
717
+ model: str,
718
+ prompt: str,
719
+ system_prompt: Optional[str],
720
+ temperature: float,
721
+ max_tokens: Optional[int],
673
722
  ) -> Dict[str, Any]:
674
723
  """Query a single model and return result with metadata."""
675
724
  import time
725
+
676
726
  start_time = time.time()
677
-
727
+
678
728
  try:
679
729
  messages = []
680
730
  if system_prompt:
681
731
  messages.append({"role": "system", "content": system_prompt})
682
732
  messages.append({"role": "user", "content": prompt})
683
-
733
+
684
734
  kwargs = {
685
735
  "model": model,
686
736
  "messages": messages,
@@ -688,43 +738,49 @@ Provide your critical analysis:"""
688
738
  }
689
739
  if max_tokens:
690
740
  kwargs["max_tokens"] = max_tokens
691
-
741
+
692
742
  response = await litellm.acompletion(**kwargs)
693
-
743
+
694
744
  return {
695
- 'success': True,
696
- 'response': response.choices[0].message.content,
697
- 'time_ms': int((time.time() - start_time) * 1000)
745
+ "success": True,
746
+ "response": response.choices[0].message.content,
747
+ "time_ms": int((time.time() - start_time) * 1000),
698
748
  }
699
-
749
+
700
750
  except Exception as e:
701
751
  return {
702
- 'success': False,
703
- 'error': str(e),
704
- 'time_ms': int((time.time() - start_time) * 1000)
752
+ "success": False,
753
+ "error": str(e),
754
+ "time_ms": int((time.time() - start_time) * 1000),
705
755
  }
706
756
 
707
757
  async def _aggregate_consensus(
708
- self, responses: List[Dict[str, Any]], original_prompt: str,
709
- judge_model: str, include_raw: bool, devil_response: Optional[Dict[str, Any]],
710
- tool_ctx
758
+ self,
759
+ responses: List[Dict[str, Any]],
760
+ original_prompt: str,
761
+ judge_model: str,
762
+ include_raw: bool,
763
+ devil_response: Optional[Dict[str, Any]],
764
+ tool_ctx,
711
765
  ) -> str:
712
766
  """Aggregate consensus responses using a judge model."""
713
767
  # Prepare response data
714
- successful_responses = [r for r in responses if r['success']]
715
-
768
+ successful_responses = [r for r in responses if r["success"]]
769
+
716
770
  if not successful_responses:
717
771
  return "Error: All models failed to respond"
718
-
772
+
719
773
  # Format responses for aggregation
720
- responses_text = "\n\n".join([
721
- f"Model: {r['model']}\nResponse: {r['response']}"
722
- for r in successful_responses
723
- ])
724
-
774
+ responses_text = "\n\n".join(
775
+ [
776
+ f"Model: {r['model']}\nResponse: {r['response']}"
777
+ for r in successful_responses
778
+ ]
779
+ )
780
+
725
781
  if devil_response:
726
782
  responses_text += f"\n\nDevil's Advocate ({devil_response['model']}):\n{devil_response['response']}"
727
-
783
+
728
784
  # Create aggregation prompt
729
785
  aggregation_prompt = f"""Analyze the following responses from multiple AI models to this question:
730
786
 
@@ -743,58 +799,62 @@ Please provide:
743
799
  {f"4. Evaluation of the devil's advocate critique" if devil_response else ""}
744
800
 
745
801
  Be concise and highlight the most important findings."""
746
-
802
+
747
803
  # Get aggregation
748
804
  try:
749
805
  if tool_ctx:
750
806
  await tool_ctx.info(f"Aggregating responses with {judge_model}...")
751
-
807
+
752
808
  judge_result = await self._query_single_model(
753
809
  judge_model, aggregation_prompt, None, 0.3, None
754
810
  )
755
-
756
- if not judge_result['success']:
811
+
812
+ if not judge_result["success"]:
757
813
  return f"Error: Judge model failed: {judge_result.get('error', 'Unknown error')}"
758
-
814
+
759
815
  # Format output
760
- output = [f"=== Consensus Analysis ({len(successful_responses)} models) ===\n"]
761
- output.append(judge_result['response'])
762
-
816
+ output = [
817
+ f"=== Consensus Analysis ({len(successful_responses)} models) ===\n"
818
+ ]
819
+ output.append(judge_result["response"])
820
+
763
821
  # Add model list
764
- output.append(f"\nModels consulted: {', '.join([r['model'] for r in successful_responses])}")
822
+ output.append(
823
+ f"\nModels consulted: {', '.join([r['model'] for r in successful_responses])}"
824
+ )
765
825
  if devil_response:
766
826
  output.append(f"Devil's Advocate: {devil_response['model']}")
767
-
827
+
768
828
  # Add timing info
769
- avg_time = sum(r['time_ms'] for r in responses) / len(responses)
829
+ avg_time = sum(r["time_ms"] for r in responses) / len(responses)
770
830
  output.append(f"\nAverage response time: {avg_time:.0f}ms")
771
-
831
+
772
832
  # Include raw responses if requested
773
833
  if include_raw:
774
834
  output.append("\n\n=== Raw Responses ===")
775
835
  for r in successful_responses:
776
836
  output.append(f"\n{r['model']}:")
777
837
  output.append("-" * 40)
778
- output.append(r['response'])
779
-
838
+ output.append(r["response"])
839
+
780
840
  if devil_response:
781
841
  output.append(f"\nDevil's Advocate ({devil_response['model']}):")
782
842
  output.append("-" * 40)
783
- output.append(devil_response['response'])
784
-
843
+ output.append(devil_response["response"])
844
+
785
845
  return "\n".join(output)
786
-
846
+
787
847
  except Exception as e:
788
848
  return f"Error during aggregation: {str(e)}"
789
849
 
790
850
  def _get_provider_for_model(self, model: str) -> Optional[str]:
791
851
  """Determine the provider for a given model."""
792
852
  model_lower = model.lower()
793
-
853
+
794
854
  # Check explicit provider prefix
795
855
  if "/" in model:
796
856
  return model.split("/")[0]
797
-
857
+
798
858
  # Check model prefixes
799
859
  if model_lower.startswith("gpt"):
800
860
  return "openai"
@@ -804,7 +864,7 @@ Be concise and highlight the most important findings."""
804
864
  return "google"
805
865
  elif model_lower.startswith("command"):
806
866
  return "cohere"
807
-
867
+
808
868
  # Default to OpenAI
809
869
  return "openai"
810
870
 
@@ -812,13 +872,13 @@ Be concise and highlight the most important findings."""
812
872
  """Get all available models from LiteLLM."""
813
873
  try:
814
874
  import litellm
815
-
875
+
816
876
  # Get all models
817
877
  all_models = litellm.model_list
818
-
878
+
819
879
  # Organize by provider
820
880
  providers = {}
821
-
881
+
822
882
  for model in all_models:
823
883
  # Extract provider
824
884
  if "/" in model:
@@ -833,19 +893,19 @@ Be concise and highlight the most important findings."""
833
893
  provider = "cohere"
834
894
  else:
835
895
  provider = "other"
836
-
896
+
837
897
  if provider not in providers:
838
898
  providers[provider] = []
839
899
  providers[provider].append(model)
840
-
900
+
841
901
  # Sort models within each provider
842
902
  for provider in providers:
843
903
  providers[provider] = sorted(providers[provider])
844
-
904
+
845
905
  return providers
846
906
  except Exception:
847
907
  return {}
848
908
 
849
909
  def register(self, mcp_server) -> None:
850
910
  """Register this tool with the MCP server."""
851
- pass
911
+ pass