hanzo-mcp 0.6.10__py3-none-any.whl → 0.6.13__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.
- hanzo_mcp/__init__.py +11 -2
- hanzo_mcp/cli.py +69 -19
- hanzo_mcp/cli_enhanced.py +15 -12
- hanzo_mcp/cli_plugin.py +91 -0
- hanzo_mcp/config/__init__.py +1 -1
- hanzo_mcp/config/settings.py +75 -8
- hanzo_mcp/config/tool_config.py +2 -2
- hanzo_mcp/dev_server.py +20 -15
- hanzo_mcp/prompts/project_system.py +1 -1
- hanzo_mcp/server.py +18 -4
- hanzo_mcp/server_enhanced.py +69 -0
- hanzo_mcp/tools/__init__.py +78 -30
- hanzo_mcp/tools/agent/__init__.py +1 -1
- hanzo_mcp/tools/agent/agent_tool.py +2 -2
- hanzo_mcp/tools/common/__init__.py +15 -1
- hanzo_mcp/tools/common/base.py +4 -4
- hanzo_mcp/tools/common/batch_tool.py +1 -1
- hanzo_mcp/tools/common/config_tool.py +2 -2
- hanzo_mcp/tools/common/context.py +2 -2
- hanzo_mcp/tools/common/context_fix.py +26 -0
- hanzo_mcp/tools/common/critic_tool.py +196 -0
- hanzo_mcp/tools/common/decorators.py +208 -0
- hanzo_mcp/tools/common/enhanced_base.py +106 -0
- hanzo_mcp/tools/common/mode.py +116 -0
- hanzo_mcp/tools/common/mode_loader.py +105 -0
- hanzo_mcp/tools/common/permissions.py +1 -1
- hanzo_mcp/tools/common/personality.py +936 -0
- hanzo_mcp/tools/common/plugin_loader.py +287 -0
- hanzo_mcp/tools/common/stats.py +4 -4
- hanzo_mcp/tools/common/tool_list.py +1 -1
- hanzo_mcp/tools/common/validation.py +1 -1
- hanzo_mcp/tools/config/__init__.py +3 -1
- hanzo_mcp/tools/config/config_tool.py +1 -1
- hanzo_mcp/tools/config/mode_tool.py +209 -0
- hanzo_mcp/tools/database/__init__.py +1 -1
- hanzo_mcp/tools/editor/__init__.py +1 -1
- hanzo_mcp/tools/filesystem/__init__.py +19 -14
- hanzo_mcp/tools/filesystem/batch_search.py +3 -3
- hanzo_mcp/tools/filesystem/diff.py +2 -2
- hanzo_mcp/tools/filesystem/rules_tool.py +235 -0
- hanzo_mcp/tools/filesystem/{unified_search.py → search_tool.py} +12 -12
- hanzo_mcp/tools/filesystem/{symbols_unified.py → symbols_tool.py} +104 -5
- hanzo_mcp/tools/filesystem/watch.py +3 -2
- hanzo_mcp/tools/jupyter/__init__.py +2 -2
- hanzo_mcp/tools/jupyter/jupyter.py +1 -1
- hanzo_mcp/tools/llm/__init__.py +3 -3
- hanzo_mcp/tools/llm/llm_tool.py +648 -143
- hanzo_mcp/tools/mcp/__init__.py +2 -2
- hanzo_mcp/tools/mcp/{mcp_unified.py → mcp_tool.py} +3 -3
- hanzo_mcp/tools/shell/__init__.py +6 -6
- hanzo_mcp/tools/shell/base_process.py +4 -2
- hanzo_mcp/tools/shell/bash_session_executor.py +8 -5
- hanzo_mcp/tools/shell/{bash_unified.py → bash_tool.py} +1 -1
- hanzo_mcp/tools/shell/command_executor.py +8 -6
- hanzo_mcp/tools/shell/{npx_unified.py → npx_tool.py} +1 -1
- hanzo_mcp/tools/shell/open.py +2 -2
- hanzo_mcp/tools/shell/{process_unified.py → process_tool.py} +1 -1
- hanzo_mcp/tools/shell/run_command_windows.py +1 -1
- hanzo_mcp/tools/shell/uvx.py +47 -2
- hanzo_mcp/tools/shell/uvx_background.py +47 -2
- hanzo_mcp/tools/shell/{uvx_unified.py → uvx_tool.py} +1 -1
- hanzo_mcp/tools/todo/__init__.py +14 -19
- hanzo_mcp/tools/todo/todo.py +22 -1
- hanzo_mcp/tools/vector/__init__.py +7 -3
- hanzo_mcp/tools/vector/ast_analyzer.py +12 -4
- hanzo_mcp/tools/vector/infinity_store.py +11 -5
- hanzo_mcp/tools/vector/project_manager.py +4 -2
- hanzo_mcp-0.6.13.dist-info/METADATA +359 -0
- {hanzo_mcp-0.6.10.dist-info → hanzo_mcp-0.6.13.dist-info}/RECORD +73 -65
- {hanzo_mcp-0.6.10.dist-info → hanzo_mcp-0.6.13.dist-info}/entry_points.txt +1 -0
- hanzo_mcp/tools/common/palette.py +0 -344
- hanzo_mcp/tools/common/palette_loader.py +0 -108
- hanzo_mcp/tools/config/palette_tool.py +0 -179
- hanzo_mcp/tools/llm/llm_unified.py +0 -851
- hanzo_mcp-0.6.10.dist-info/METADATA +0 -339
- {hanzo_mcp-0.6.10.dist-info → hanzo_mcp-0.6.13.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.6.10.dist-info → hanzo_mcp-0.6.13.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.6.10.dist-info → hanzo_mcp-0.6.13.dist-info}/top_level.txt +0 -0
hanzo_mcp/tools/llm/llm_tool.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
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
|
|
3
5
|
import os
|
|
4
6
|
import json
|
|
5
|
-
from
|
|
6
|
-
import asyncio
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
from mcp.server.fastmcp import Context as MCPContext
|
|
9
10
|
from pydantic import Field
|
|
@@ -11,27 +12,44 @@ from pydantic import Field
|
|
|
11
12
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
12
13
|
from hanzo_mcp.tools.common.context import create_tool_context
|
|
13
14
|
|
|
15
|
+
# Check if litellm is available
|
|
14
16
|
try:
|
|
15
17
|
import litellm
|
|
16
|
-
from litellm import completion, acompletion
|
|
17
18
|
LITELLM_AVAILABLE = True
|
|
18
19
|
except ImportError:
|
|
19
20
|
LITELLM_AVAILABLE = False
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
# Parameter types
|
|
24
|
+
Action = Annotated[
|
|
23
25
|
str,
|
|
24
26
|
Field(
|
|
25
|
-
description="
|
|
26
|
-
|
|
27
|
+
description="Action to perform: query, consensus, list, models, enable, disable, test",
|
|
28
|
+
default="query",
|
|
29
|
+
),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
Model = Annotated[
|
|
33
|
+
Optional[str],
|
|
34
|
+
Field(
|
|
35
|
+
description="Model name (e.g., gpt-4, claude-3-opus-20240229)",
|
|
36
|
+
default=None,
|
|
37
|
+
),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
Models = Annotated[
|
|
41
|
+
Optional[List[str]],
|
|
42
|
+
Field(
|
|
43
|
+
description="List of models for consensus mode",
|
|
44
|
+
default=None,
|
|
27
45
|
),
|
|
28
46
|
]
|
|
29
47
|
|
|
30
48
|
Prompt = Annotated[
|
|
31
|
-
str,
|
|
49
|
+
Optional[str],
|
|
32
50
|
Field(
|
|
33
|
-
description="The prompt
|
|
34
|
-
|
|
51
|
+
description="The prompt to send to the LLM",
|
|
52
|
+
default=None,
|
|
35
53
|
),
|
|
36
54
|
]
|
|
37
55
|
|
|
@@ -46,7 +64,7 @@ SystemPrompt = Annotated[
|
|
|
46
64
|
Temperature = Annotated[
|
|
47
65
|
float,
|
|
48
66
|
Field(
|
|
49
|
-
description="Temperature for response randomness (0
|
|
67
|
+
description="Temperature for response randomness (0-2)",
|
|
50
68
|
default=0.7,
|
|
51
69
|
),
|
|
52
70
|
]
|
|
@@ -75,82 +93,140 @@ Stream = Annotated[
|
|
|
75
93
|
),
|
|
76
94
|
]
|
|
77
95
|
|
|
96
|
+
Provider = Annotated[
|
|
97
|
+
Optional[str],
|
|
98
|
+
Field(
|
|
99
|
+
description="Provider name for list/enable/disable actions",
|
|
100
|
+
default=None,
|
|
101
|
+
),
|
|
102
|
+
]
|
|
78
103
|
|
|
79
|
-
|
|
80
|
-
|
|
104
|
+
IncludeRaw = Annotated[
|
|
105
|
+
bool,
|
|
106
|
+
Field(
|
|
107
|
+
description="Include raw responses in consensus mode",
|
|
108
|
+
default=False,
|
|
109
|
+
),
|
|
110
|
+
]
|
|
81
111
|
|
|
82
|
-
|
|
83
|
-
|
|
112
|
+
JudgeModel = Annotated[
|
|
113
|
+
Optional[str],
|
|
114
|
+
Field(
|
|
115
|
+
description="Model to use as judge/aggregator in consensus",
|
|
116
|
+
default=None,
|
|
117
|
+
),
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
DevilsAdvocate = Annotated[
|
|
121
|
+
bool,
|
|
122
|
+
Field(
|
|
123
|
+
description="Enable devil's advocate mode (10th model critiques others)",
|
|
124
|
+
default=False,
|
|
125
|
+
),
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
ConsensusSize = Annotated[
|
|
129
|
+
Optional[int],
|
|
130
|
+
Field(
|
|
131
|
+
description="Number of models to use in consensus (default: 3)",
|
|
132
|
+
default=None,
|
|
133
|
+
),
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class LLMParams(TypedDict, total=False):
|
|
138
|
+
"""Parameters for LLM tool."""
|
|
139
|
+
action: str
|
|
140
|
+
model: Optional[str]
|
|
141
|
+
models: Optional[List[str]]
|
|
142
|
+
prompt: Optional[str]
|
|
84
143
|
system_prompt: Optional[str]
|
|
85
144
|
temperature: float
|
|
86
145
|
max_tokens: Optional[int]
|
|
87
146
|
json_mode: bool
|
|
88
147
|
stream: bool
|
|
148
|
+
provider: Optional[str]
|
|
149
|
+
include_raw: bool
|
|
150
|
+
judge_model: Optional[str]
|
|
151
|
+
devils_advocate: bool
|
|
152
|
+
consensus_size: Optional[int]
|
|
89
153
|
|
|
90
154
|
|
|
91
155
|
@final
|
|
92
156
|
class LLMTool(BaseTool):
|
|
93
|
-
"""
|
|
157
|
+
"""Unified LLM tool with multiple actions."""
|
|
158
|
+
|
|
159
|
+
# Config file for settings
|
|
160
|
+
CONFIG_FILE = Path.home() / ".hanzo" / "mcp" / "llm_config.json"
|
|
94
161
|
|
|
95
|
-
#
|
|
162
|
+
# Default consensus models in order of preference
|
|
163
|
+
DEFAULT_CONSENSUS_MODELS = [
|
|
164
|
+
"gpt-4o", # OpenAI's latest
|
|
165
|
+
"claude-3-opus-20240229", # Claude's most capable
|
|
166
|
+
"gemini/gemini-1.5-pro", # Google's best
|
|
167
|
+
"groq/llama3-70b-8192", # Fast Groq
|
|
168
|
+
"mistral/mistral-large-latest", # Mistral's best
|
|
169
|
+
"perplexity/llama-3.1-sonar-large-128k-chat", # Perplexity with search
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
# API key environment variables
|
|
96
173
|
API_KEY_ENV_VARS = {
|
|
97
174
|
"openai": ["OPENAI_API_KEY"],
|
|
98
175
|
"anthropic": ["ANTHROPIC_API_KEY", "CLAUDE_API_KEY"],
|
|
99
|
-
"google": ["GOOGLE_API_KEY", "GEMINI_API_KEY"
|
|
176
|
+
"google": ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
|
|
100
177
|
"groq": ["GROQ_API_KEY"],
|
|
178
|
+
"mistral": ["MISTRAL_API_KEY"],
|
|
179
|
+
"perplexity": ["PERPLEXITY_API_KEY", "PERPLEXITYAI_API_KEY"],
|
|
180
|
+
"together": ["TOGETHER_API_KEY", "TOGETHERAI_API_KEY"],
|
|
101
181
|
"cohere": ["COHERE_API_KEY"],
|
|
102
182
|
"replicate": ["REPLICATE_API_KEY"],
|
|
103
183
|
"huggingface": ["HUGGINGFACE_API_KEY", "HF_TOKEN"],
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"anyscale": ["ANYSCALE_API_KEY"],
|
|
108
|
-
"deepinfra": ["DEEPINFRA_API_KEY"],
|
|
109
|
-
"ai21": ["AI21_API_KEY"],
|
|
110
|
-
"nvidia": ["NVIDIA_API_KEY"],
|
|
184
|
+
"bedrock": ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
|
|
185
|
+
"vertex": ["GOOGLE_APPLICATION_CREDENTIALS"],
|
|
186
|
+
"azure": ["AZURE_API_KEY"],
|
|
111
187
|
"voyage": ["VOYAGE_API_KEY"],
|
|
112
|
-
"
|
|
113
|
-
"azure": ["AZURE_API_KEY", "AZURE_OPENAI_API_KEY"],
|
|
188
|
+
"deepseek": ["DEEPSEEK_API_KEY"],
|
|
114
189
|
}
|
|
115
190
|
|
|
116
|
-
# Model prefixes for each provider
|
|
117
|
-
PROVIDER_MODELS = {
|
|
118
|
-
"openai": ["gpt-4", "gpt-3.5", "o1", "davinci", "curie", "babbage", "ada"],
|
|
119
|
-
"anthropic": ["claude-3", "claude-2", "claude-instant"],
|
|
120
|
-
"google": ["gemini", "palm", "bison", "gecko"],
|
|
121
|
-
"groq": ["mixtral", "llama2", "llama3"],
|
|
122
|
-
"cohere": ["command", "command-light"],
|
|
123
|
-
"mistral": ["mistral-tiny", "mistral-small", "mistral-medium", "mistral-large"],
|
|
124
|
-
"perplexity": ["pplx", "sonar"],
|
|
125
|
-
"together": ["together"],
|
|
126
|
-
"bedrock": ["bedrock/"],
|
|
127
|
-
"azure": ["azure/"],
|
|
128
|
-
}
|
|
129
|
-
|
|
130
191
|
def __init__(self):
|
|
131
|
-
"""Initialize the LLM tool."""
|
|
192
|
+
"""Initialize the unified LLM tool."""
|
|
132
193
|
self.available_providers = self._detect_available_providers()
|
|
194
|
+
self.config = self._load_config()
|
|
133
195
|
|
|
134
|
-
# Configure LiteLLM settings
|
|
135
|
-
if LITELLM_AVAILABLE:
|
|
136
|
-
# Enable verbose logging for debugging
|
|
137
|
-
litellm.set_verbose = False
|
|
138
|
-
# Set default timeout
|
|
139
|
-
litellm.request_timeout = 120
|
|
140
|
-
|
|
141
196
|
def _detect_available_providers(self) -> Dict[str, List[str]]:
|
|
142
|
-
"""Detect which
|
|
197
|
+
"""Detect which providers have API keys configured."""
|
|
143
198
|
available = {}
|
|
144
199
|
|
|
145
200
|
for provider, env_vars in self.API_KEY_ENV_VARS.items():
|
|
146
201
|
for var in env_vars:
|
|
147
202
|
if os.getenv(var):
|
|
148
|
-
|
|
149
|
-
available[provider] = []
|
|
150
|
-
available[provider].append(var)
|
|
203
|
+
available[provider] = env_vars
|
|
151
204
|
break
|
|
152
205
|
|
|
153
206
|
return available
|
|
207
|
+
|
|
208
|
+
def _load_config(self) -> Dict[str, Any]:
|
|
209
|
+
"""Load configuration from file."""
|
|
210
|
+
if self.CONFIG_FILE.exists():
|
|
211
|
+
try:
|
|
212
|
+
with open(self.CONFIG_FILE, 'r') as f:
|
|
213
|
+
return json.load(f)
|
|
214
|
+
except:
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
# Default config
|
|
218
|
+
return {
|
|
219
|
+
"disabled_providers": [],
|
|
220
|
+
"consensus_models": None, # Use defaults if None
|
|
221
|
+
"default_judge_model": "gpt-4o",
|
|
222
|
+
"consensus_size": 3,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
def _save_config(self):
|
|
226
|
+
"""Save configuration to file."""
|
|
227
|
+
self.CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
228
|
+
with open(self.CONFIG_FILE, 'w') as f:
|
|
229
|
+
json.dump(self.config, f, indent=2)
|
|
154
230
|
|
|
155
231
|
@property
|
|
156
232
|
@override
|
|
@@ -162,149 +238,578 @@ class LLMTool(BaseTool):
|
|
|
162
238
|
@override
|
|
163
239
|
def description(self) -> str:
|
|
164
240
|
"""Get the tool description."""
|
|
165
|
-
|
|
241
|
+
available = list(self.available_providers.keys())
|
|
166
242
|
|
|
167
|
-
return f"""Query
|
|
168
|
-
|
|
169
|
-
Supports 100+ models from various providers through a single interface.
|
|
170
|
-
Automatically uses API keys from environment variables.
|
|
243
|
+
return f"""Query LLMs. Default: single query. Actions: consensus, list, models, test.
|
|
171
244
|
|
|
172
|
-
|
|
245
|
+
Usage:
|
|
246
|
+
llm "What is the capital of France?"
|
|
247
|
+
llm "Explain this code" --model gpt-4o
|
|
248
|
+
llm --action consensus "Is this approach correct?" --devils-advocate
|
|
249
|
+
llm --action models --provider openai
|
|
173
250
|
|
|
174
|
-
|
|
175
|
-
- OpenAI: gpt-4o, gpt-4, gpt-3.5-turbo, o1-preview, o1-mini
|
|
176
|
-
- Anthropic: claude-3-opus-20240229, claude-3-sonnet-20240229, claude-3-haiku-20240307
|
|
177
|
-
- Google: gemini/gemini-pro, gemini/gemini-1.5-pro, gemini/gemini-1.5-flash
|
|
178
|
-
- Groq: groq/mixtral-8x7b-32768, groq/llama3-70b-8192
|
|
179
|
-
- Mistral: mistral/mistral-large-latest, mistral/mistral-medium
|
|
180
|
-
- Perplexity: perplexity/sonar-medium-online
|
|
181
|
-
- Together: together/mixtral-8x22b
|
|
182
|
-
|
|
183
|
-
Examples:
|
|
184
|
-
- llm --model "gpt-4" --prompt "Explain quantum computing"
|
|
185
|
-
- llm --model "claude-3-opus-20240229" --prompt "Write a haiku about coding"
|
|
186
|
-
- llm --model "gemini/gemini-pro" --prompt "What is the meaning of life?" --temperature 0.9
|
|
187
|
-
- llm --model "groq/mixtral-8x7b-32768" --prompt "Generate a JSON schema" --json-mode
|
|
188
|
-
|
|
189
|
-
For provider-specific tools, use: openai, anthropic, gemini, groq, etc.
|
|
190
|
-
For consensus across models, use: consensus
|
|
191
|
-
"""
|
|
251
|
+
Available: {', '.join(available) if available else 'None'}"""
|
|
192
252
|
|
|
193
253
|
@override
|
|
194
254
|
async def call(
|
|
195
255
|
self,
|
|
196
256
|
ctx: MCPContext,
|
|
197
|
-
**params: Unpack[
|
|
257
|
+
**params: Unpack[LLMParams],
|
|
198
258
|
) -> str:
|
|
199
|
-
"""
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
259
|
+
"""Execute LLM action."""
|
|
260
|
+
# Create tool context only if we have a proper MCP context
|
|
261
|
+
tool_ctx = None
|
|
262
|
+
try:
|
|
263
|
+
if hasattr(ctx, 'client') and ctx.client and hasattr(ctx.client, 'server'):
|
|
264
|
+
tool_ctx = create_tool_context(ctx)
|
|
265
|
+
if tool_ctx:
|
|
266
|
+
await tool_ctx.set_tool_info(self.name)
|
|
267
|
+
except:
|
|
268
|
+
# Running in test mode without MCP context
|
|
269
|
+
pass
|
|
210
270
|
|
|
211
271
|
if not LITELLM_AVAILABLE:
|
|
212
272
|
return "Error: LiteLLM is not installed. Install it with: pip install litellm"
|
|
213
273
|
|
|
214
|
-
# Extract
|
|
274
|
+
# Extract action
|
|
275
|
+
action = params.get("action", "query")
|
|
276
|
+
|
|
277
|
+
# Route to appropriate handler
|
|
278
|
+
if action == "query":
|
|
279
|
+
return await self._handle_query(tool_ctx, params)
|
|
280
|
+
elif action == "consensus":
|
|
281
|
+
return await self._handle_consensus(tool_ctx, params)
|
|
282
|
+
elif action == "list":
|
|
283
|
+
return self._handle_list()
|
|
284
|
+
elif action == "models":
|
|
285
|
+
return self._handle_models(params.get("provider"))
|
|
286
|
+
elif action == "enable":
|
|
287
|
+
return self._handle_enable(params.get("provider"))
|
|
288
|
+
elif action == "disable":
|
|
289
|
+
return self._handle_disable(params.get("provider"))
|
|
290
|
+
elif action == "test":
|
|
291
|
+
return await self._handle_test(tool_ctx, params.get("model"), params.get("provider"))
|
|
292
|
+
else:
|
|
293
|
+
return f"Error: Unknown action '{action}'. Valid actions: query, consensus, list, models, enable, disable, test"
|
|
294
|
+
|
|
295
|
+
async def _handle_query(self, tool_ctx, params: Dict[str, Any]) -> str:
|
|
296
|
+
"""Handle single model query."""
|
|
215
297
|
model = params.get("model")
|
|
216
|
-
if not model:
|
|
217
|
-
return "Error: model is required"
|
|
218
|
-
|
|
219
298
|
prompt = params.get("prompt")
|
|
299
|
+
|
|
220
300
|
if not prompt:
|
|
221
|
-
return "Error: prompt is required"
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
301
|
+
return "Error: prompt is required for query action"
|
|
302
|
+
|
|
303
|
+
# Auto-select model if not specified
|
|
304
|
+
if not model:
|
|
305
|
+
if self.available_providers:
|
|
306
|
+
# Use first available model
|
|
307
|
+
if "openai" in self.available_providers:
|
|
308
|
+
model = "gpt-4o-mini"
|
|
309
|
+
elif "anthropic" in self.available_providers:
|
|
310
|
+
model = "claude-3-haiku-20240307"
|
|
311
|
+
elif "google" in self.available_providers:
|
|
312
|
+
model = "gemini/gemini-1.5-flash"
|
|
313
|
+
else:
|
|
314
|
+
# Use first provider's default
|
|
315
|
+
provider = list(self.available_providers.keys())[0]
|
|
316
|
+
model = f"{provider}/default"
|
|
317
|
+
else:
|
|
318
|
+
return "Error: No model specified and no API keys found"
|
|
319
|
+
|
|
229
320
|
# Check if we have API key for this model
|
|
230
321
|
provider = self._get_provider_for_model(model)
|
|
231
322
|
if provider and provider not in self.available_providers:
|
|
232
323
|
env_vars = self.API_KEY_ENV_VARS.get(provider, [])
|
|
233
324
|
return f"Error: No API key found for {provider}. Set one of: {', '.join(env_vars)}"
|
|
234
|
-
|
|
325
|
+
|
|
235
326
|
# Build messages
|
|
236
327
|
messages = []
|
|
237
|
-
if system_prompt:
|
|
238
|
-
messages.append({"role": "system", "content": system_prompt})
|
|
328
|
+
if params.get("system_prompt"):
|
|
329
|
+
messages.append({"role": "system", "content": params["system_prompt"]})
|
|
239
330
|
messages.append({"role": "user", "content": prompt})
|
|
240
|
-
|
|
331
|
+
|
|
241
332
|
# Build kwargs
|
|
242
333
|
kwargs = {
|
|
243
334
|
"model": model,
|
|
244
335
|
"messages": messages,
|
|
245
|
-
"temperature": temperature,
|
|
336
|
+
"temperature": params.get("temperature", 0.7),
|
|
246
337
|
}
|
|
247
338
|
|
|
248
|
-
if max_tokens:
|
|
249
|
-
kwargs["max_tokens"] = max_tokens
|
|
339
|
+
if params.get("max_tokens"):
|
|
340
|
+
kwargs["max_tokens"] = params["max_tokens"]
|
|
250
341
|
|
|
251
|
-
if json_mode:
|
|
342
|
+
if params.get("json_mode"):
|
|
252
343
|
kwargs["response_format"] = {"type": "json_object"}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
344
|
+
|
|
345
|
+
if params.get("stream"):
|
|
346
|
+
kwargs["stream"] = True
|
|
347
|
+
|
|
256
348
|
try:
|
|
257
|
-
if
|
|
258
|
-
|
|
349
|
+
if tool_ctx:
|
|
350
|
+
await tool_ctx.info(f"Querying {model}...")
|
|
351
|
+
|
|
352
|
+
if kwargs.get("stream"):
|
|
353
|
+
# Handle streaming response
|
|
259
354
|
response_text = ""
|
|
260
|
-
async for chunk in await acompletion(**kwargs
|
|
355
|
+
async for chunk in await litellm.acompletion(**kwargs):
|
|
261
356
|
if chunk.choices[0].delta.content:
|
|
262
357
|
response_text += chunk.choices[0].delta.content
|
|
263
|
-
# Could emit progress here if needed
|
|
264
|
-
|
|
265
358
|
return response_text
|
|
266
359
|
else:
|
|
267
|
-
#
|
|
268
|
-
response = await acompletion(**kwargs)
|
|
360
|
+
# Regular response
|
|
361
|
+
response = await litellm.acompletion(**kwargs)
|
|
269
362
|
return response.choices[0].message.content
|
|
270
|
-
|
|
363
|
+
|
|
271
364
|
except Exception as e:
|
|
272
365
|
error_msg = str(e)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if "api_key" in error_msg.lower():
|
|
276
|
-
provider = self._get_provider_for_model(model)
|
|
277
|
-
env_vars = self.API_KEY_ENV_VARS.get(provider, [])
|
|
278
|
-
return f"Error: API key issue for {provider}. Make sure one of these is set: {', '.join(env_vars)}\n\nOriginal error: {error_msg}"
|
|
279
|
-
elif "model" in error_msg.lower() and "not found" in error_msg.lower():
|
|
280
|
-
return f"Error: Model '{model}' not found or not accessible. Check the model name and your API permissions.\n\nOriginal error: {error_msg}"
|
|
366
|
+
if "model_not_found" in error_msg or "does not exist" in error_msg:
|
|
367
|
+
return f"Error: Model '{model}' not found. Use 'llm --action models' to see available models."
|
|
281
368
|
else:
|
|
282
369
|
return f"Error calling LLM: {error_msg}"
|
|
283
370
|
|
|
371
|
+
async def _handle_consensus(self, tool_ctx, params: Dict[str, Any]) -> str:
|
|
372
|
+
"""Handle consensus mode with multiple models."""
|
|
373
|
+
prompt = params.get("prompt")
|
|
374
|
+
if not prompt:
|
|
375
|
+
return "Error: prompt is required for consensus action"
|
|
376
|
+
|
|
377
|
+
# Determine models to use
|
|
378
|
+
models = params.get("models")
|
|
379
|
+
if not models:
|
|
380
|
+
# Use configured or default models
|
|
381
|
+
consensus_size = params.get("consensus_size") or self.config.get("consensus_size", 3)
|
|
382
|
+
models = self._get_consensus_models(consensus_size)
|
|
383
|
+
|
|
384
|
+
if not models:
|
|
385
|
+
return "Error: No models available for consensus. Set API keys for at least 2 providers."
|
|
386
|
+
|
|
387
|
+
if len(models) < 2:
|
|
388
|
+
return "Error: Consensus requires at least 2 models"
|
|
389
|
+
|
|
390
|
+
# Check for devil's advocate mode
|
|
391
|
+
devils_advocate = params.get("devils_advocate", False)
|
|
392
|
+
if devils_advocate and len(models) < 3:
|
|
393
|
+
return "Error: Devil's advocate mode requires at least 3 models"
|
|
394
|
+
|
|
395
|
+
if tool_ctx:
|
|
396
|
+
await tool_ctx.info(f"Running consensus with {len(models)} models...")
|
|
397
|
+
|
|
398
|
+
# Query models in parallel
|
|
399
|
+
system_prompt = params.get("system_prompt")
|
|
400
|
+
temperature = params.get("temperature", 0.7)
|
|
401
|
+
max_tokens = params.get("max_tokens")
|
|
402
|
+
|
|
403
|
+
# Split models if using devil's advocate
|
|
404
|
+
if devils_advocate:
|
|
405
|
+
consensus_models = models[:-1]
|
|
406
|
+
devil_model = models[-1]
|
|
407
|
+
else:
|
|
408
|
+
consensus_models = models
|
|
409
|
+
devil_model = None
|
|
410
|
+
|
|
411
|
+
# Query consensus models
|
|
412
|
+
responses = await self._query_models_parallel(
|
|
413
|
+
consensus_models, prompt, system_prompt, temperature, max_tokens, tool_ctx
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Get devil's advocate response if enabled
|
|
417
|
+
devil_response = None
|
|
418
|
+
if devil_model:
|
|
419
|
+
# 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
|
+
|
|
425
|
+
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
|
+
|
|
427
|
+
Original Question: {prompt}
|
|
428
|
+
|
|
429
|
+
Responses from other models:
|
|
430
|
+
{responses_text}
|
|
431
|
+
|
|
432
|
+
Provide your critical analysis:"""
|
|
433
|
+
|
|
434
|
+
devil_result = await self._query_single_model(
|
|
435
|
+
devil_model, devil_prompt, system_prompt, temperature, max_tokens
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
if devil_result['success']:
|
|
439
|
+
devil_response = {
|
|
440
|
+
'model': devil_model,
|
|
441
|
+
'response': devil_result['response'],
|
|
442
|
+
'time_ms': devil_result['time_ms']
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
# Aggregate responses
|
|
446
|
+
judge_model = params.get("judge_model") or self.config.get("default_judge_model", "gpt-4o")
|
|
447
|
+
include_raw = params.get("include_raw", False)
|
|
448
|
+
|
|
449
|
+
return await self._aggregate_consensus(
|
|
450
|
+
responses, prompt, judge_model, include_raw, devil_response, tool_ctx
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
def _handle_list(self) -> str:
|
|
454
|
+
"""List available providers."""
|
|
455
|
+
output = ["=== LLM Providers ==="]
|
|
456
|
+
|
|
457
|
+
# Get all possible providers
|
|
458
|
+
all_providers = sorted(self.API_KEY_ENV_VARS.keys())
|
|
459
|
+
disabled = self.config.get("disabled_providers", [])
|
|
460
|
+
|
|
461
|
+
output.append(f"Total providers: {len(all_providers)}")
|
|
462
|
+
output.append(f"Available: {len(self.available_providers)}")
|
|
463
|
+
output.append(f"Disabled: {len(disabled)}\n")
|
|
464
|
+
|
|
465
|
+
for provider in all_providers:
|
|
466
|
+
status_parts = []
|
|
467
|
+
|
|
468
|
+
# Check if API key exists
|
|
469
|
+
if provider in self.available_providers:
|
|
470
|
+
status_parts.append("✅ API key found")
|
|
471
|
+
else:
|
|
472
|
+
status_parts.append("❌ No API key")
|
|
473
|
+
|
|
474
|
+
# Check if disabled
|
|
475
|
+
if provider in disabled:
|
|
476
|
+
status_parts.append("🚫 Disabled")
|
|
477
|
+
|
|
478
|
+
# Show environment variables
|
|
479
|
+
env_vars = self.API_KEY_ENV_VARS.get(provider, [])
|
|
480
|
+
status = " | ".join(status_parts)
|
|
481
|
+
|
|
482
|
+
output.append(f"{provider}: {status}")
|
|
483
|
+
output.append(f" Environment variables: {', '.join(env_vars)}")
|
|
484
|
+
|
|
485
|
+
output.append("\nUse 'llm --action enable/disable --provider <name>' to manage providers")
|
|
486
|
+
|
|
487
|
+
return "\n".join(output)
|
|
488
|
+
|
|
489
|
+
def _handle_models(self, provider: Optional[str] = None) -> str:
|
|
490
|
+
"""List available models."""
|
|
491
|
+
try:
|
|
492
|
+
all_models = self._get_all_models()
|
|
493
|
+
|
|
494
|
+
if not all_models:
|
|
495
|
+
return "No models available or LiteLLM not properly initialized"
|
|
496
|
+
|
|
497
|
+
output = ["=== Available LLM Models ==="]
|
|
498
|
+
|
|
499
|
+
if provider:
|
|
500
|
+
# Show models for specific provider
|
|
501
|
+
provider_lower = provider.lower()
|
|
502
|
+
models = all_models.get(provider_lower, [])
|
|
503
|
+
|
|
504
|
+
if not models:
|
|
505
|
+
return f"No models found for provider '{provider}'"
|
|
506
|
+
|
|
507
|
+
output.append(f"\n{provider.upper()} ({len(models)} models):")
|
|
508
|
+
output.append("-" * 40)
|
|
509
|
+
|
|
510
|
+
# Show first 50 models
|
|
511
|
+
for model in models[:50]:
|
|
512
|
+
output.append(f" {model}")
|
|
513
|
+
|
|
514
|
+
if len(models) > 50:
|
|
515
|
+
output.append(f" ... and {len(models) - 50} more")
|
|
516
|
+
else:
|
|
517
|
+
# Show summary of all providers
|
|
518
|
+
total_models = sum(len(models) for models in all_models.values())
|
|
519
|
+
output.append(f"Total models available: {total_models}")
|
|
520
|
+
output.append("")
|
|
521
|
+
|
|
522
|
+
# Show providers with counts
|
|
523
|
+
for provider_name, models in sorted(all_models.items()):
|
|
524
|
+
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
|
+
|
|
530
|
+
return "\n".join(output)
|
|
531
|
+
|
|
532
|
+
except Exception as e:
|
|
533
|
+
return f"Error listing models: {str(e)}"
|
|
534
|
+
|
|
535
|
+
def _handle_enable(self, provider: Optional[str]) -> str:
|
|
536
|
+
"""Enable a provider."""
|
|
537
|
+
if not provider:
|
|
538
|
+
return "Error: provider is required for enable action"
|
|
539
|
+
|
|
540
|
+
provider = provider.lower()
|
|
541
|
+
disabled = self.config.get("disabled_providers", [])
|
|
542
|
+
|
|
543
|
+
if provider in disabled:
|
|
544
|
+
disabled.remove(provider)
|
|
545
|
+
self.config["disabled_providers"] = disabled
|
|
546
|
+
self._save_config()
|
|
547
|
+
return f"Successfully enabled {provider}"
|
|
548
|
+
else:
|
|
549
|
+
return f"{provider} is already enabled"
|
|
550
|
+
|
|
551
|
+
def _handle_disable(self, provider: Optional[str]) -> str:
|
|
552
|
+
"""Disable a provider."""
|
|
553
|
+
if not provider:
|
|
554
|
+
return "Error: provider is required for disable action"
|
|
555
|
+
|
|
556
|
+
provider = provider.lower()
|
|
557
|
+
disabled = self.config.get("disabled_providers", [])
|
|
558
|
+
|
|
559
|
+
if provider not in disabled:
|
|
560
|
+
disabled.append(provider)
|
|
561
|
+
self.config["disabled_providers"] = disabled
|
|
562
|
+
self._save_config()
|
|
563
|
+
return f"Successfully disabled {provider}"
|
|
564
|
+
else:
|
|
565
|
+
return f"{provider} is already disabled"
|
|
566
|
+
|
|
567
|
+
async def _handle_test(self, tool_ctx, model: Optional[str], provider: Optional[str]) -> str:
|
|
568
|
+
"""Test a model or provider."""
|
|
569
|
+
if not model and not provider:
|
|
570
|
+
return "Error: Either model or provider is required for test action"
|
|
571
|
+
|
|
572
|
+
# If provider specified, test its default model
|
|
573
|
+
if provider and not model:
|
|
574
|
+
provider = provider.lower()
|
|
575
|
+
if provider == "openai":
|
|
576
|
+
model = "gpt-3.5-turbo"
|
|
577
|
+
elif provider == "anthropic":
|
|
578
|
+
model = "claude-3-haiku-20240307"
|
|
579
|
+
elif provider == "google":
|
|
580
|
+
model = "gemini/gemini-1.5-flash"
|
|
581
|
+
elif provider == "groq":
|
|
582
|
+
model = "groq/llama3-8b-8192"
|
|
583
|
+
else:
|
|
584
|
+
model = f"{provider}/default"
|
|
585
|
+
|
|
586
|
+
# Test the model
|
|
587
|
+
test_prompt = "Say 'Hello from Hanzo AI!' in exactly 5 words."
|
|
588
|
+
|
|
589
|
+
try:
|
|
590
|
+
if tool_ctx:
|
|
591
|
+
await tool_ctx.info(f"Testing {model}...")
|
|
592
|
+
|
|
593
|
+
response = await litellm.acompletion(
|
|
594
|
+
model=model,
|
|
595
|
+
messages=[{"role": "user", "content": test_prompt}],
|
|
596
|
+
temperature=0,
|
|
597
|
+
max_tokens=20
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
result = response.choices[0].message.content
|
|
601
|
+
return f"✅ {model} is working!\nResponse: {result}"
|
|
602
|
+
|
|
603
|
+
except Exception as e:
|
|
604
|
+
return f"❌ {model} failed: {str(e)}"
|
|
605
|
+
|
|
606
|
+
def _get_consensus_models(self, size: int) -> List[str]:
|
|
607
|
+
"""Get models for consensus based on availability."""
|
|
608
|
+
# Use configured models if set
|
|
609
|
+
configured = self.config.get("consensus_models")
|
|
610
|
+
if configured:
|
|
611
|
+
return configured[:size]
|
|
612
|
+
|
|
613
|
+
# Otherwise, build list from available providers
|
|
614
|
+
models = []
|
|
615
|
+
disabled = self.config.get("disabled_providers", [])
|
|
616
|
+
|
|
617
|
+
# Try default models first
|
|
618
|
+
for model in self.DEFAULT_CONSENSUS_MODELS:
|
|
619
|
+
if len(models) >= size:
|
|
620
|
+
break
|
|
621
|
+
|
|
622
|
+
provider = self._get_provider_for_model(model)
|
|
623
|
+
if provider and provider in self.available_providers and provider not in disabled:
|
|
624
|
+
models.append(model)
|
|
625
|
+
|
|
626
|
+
# If still need more, add from available providers
|
|
627
|
+
if len(models) < size:
|
|
628
|
+
for provider in self.available_providers:
|
|
629
|
+
if provider in disabled:
|
|
630
|
+
continue
|
|
631
|
+
|
|
632
|
+
if provider == "openai" and "gpt-4o" not in models:
|
|
633
|
+
models.append("gpt-4o")
|
|
634
|
+
elif provider == "anthropic" and "claude-3-opus-20240229" not in models:
|
|
635
|
+
models.append("claude-3-opus-20240229")
|
|
636
|
+
elif provider == "google" and "gemini/gemini-1.5-pro" not in models:
|
|
637
|
+
models.append("gemini/gemini-1.5-pro")
|
|
638
|
+
|
|
639
|
+
if len(models) >= size:
|
|
640
|
+
break
|
|
641
|
+
|
|
642
|
+
return models
|
|
643
|
+
|
|
644
|
+
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
|
|
647
|
+
) -> List[Dict[str, Any]]:
|
|
648
|
+
"""Query multiple models in parallel."""
|
|
649
|
+
async def query_with_info(model: str) -> Dict[str, Any]:
|
|
650
|
+
result = await self._query_single_model(model, prompt, system_prompt, temperature, max_tokens)
|
|
651
|
+
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)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
# Run all queries in parallel
|
|
660
|
+
tasks = [query_with_info(model) for model in models]
|
|
661
|
+
results = await asyncio.gather(*tasks)
|
|
662
|
+
|
|
663
|
+
# Report results
|
|
664
|
+
successful = sum(1 for r in results if r['success'])
|
|
665
|
+
if tool_ctx:
|
|
666
|
+
await tool_ctx.info(f"Completed {successful}/{len(models)} model queries")
|
|
667
|
+
|
|
668
|
+
return results
|
|
669
|
+
|
|
670
|
+
async def _query_single_model(
|
|
671
|
+
self, model: str, prompt: str, system_prompt: Optional[str],
|
|
672
|
+
temperature: float, max_tokens: Optional[int]
|
|
673
|
+
) -> Dict[str, Any]:
|
|
674
|
+
"""Query a single model and return result with metadata."""
|
|
675
|
+
import time
|
|
676
|
+
start_time = time.time()
|
|
677
|
+
|
|
678
|
+
try:
|
|
679
|
+
messages = []
|
|
680
|
+
if system_prompt:
|
|
681
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
682
|
+
messages.append({"role": "user", "content": prompt})
|
|
683
|
+
|
|
684
|
+
kwargs = {
|
|
685
|
+
"model": model,
|
|
686
|
+
"messages": messages,
|
|
687
|
+
"temperature": temperature,
|
|
688
|
+
}
|
|
689
|
+
if max_tokens:
|
|
690
|
+
kwargs["max_tokens"] = max_tokens
|
|
691
|
+
|
|
692
|
+
response = await litellm.acompletion(**kwargs)
|
|
693
|
+
|
|
694
|
+
return {
|
|
695
|
+
'success': True,
|
|
696
|
+
'response': response.choices[0].message.content,
|
|
697
|
+
'time_ms': int((time.time() - start_time) * 1000)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
except Exception as e:
|
|
701
|
+
return {
|
|
702
|
+
'success': False,
|
|
703
|
+
'error': str(e),
|
|
704
|
+
'time_ms': int((time.time() - start_time) * 1000)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
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
|
|
711
|
+
) -> str:
|
|
712
|
+
"""Aggregate consensus responses using a judge model."""
|
|
713
|
+
# Prepare response data
|
|
714
|
+
successful_responses = [r for r in responses if r['success']]
|
|
715
|
+
|
|
716
|
+
if not successful_responses:
|
|
717
|
+
return "Error: All models failed to respond"
|
|
718
|
+
|
|
719
|
+
# 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
|
+
|
|
725
|
+
if devil_response:
|
|
726
|
+
responses_text += f"\n\nDevil's Advocate ({devil_response['model']}):\n{devil_response['response']}"
|
|
727
|
+
|
|
728
|
+
# Create aggregation prompt
|
|
729
|
+
aggregation_prompt = f"""Analyze the following responses from multiple AI models to this question:
|
|
730
|
+
|
|
731
|
+
<original_question>
|
|
732
|
+
{original_prompt}
|
|
733
|
+
</original_question>
|
|
734
|
+
|
|
735
|
+
<model_responses>
|
|
736
|
+
{responses_text}
|
|
737
|
+
</model_responses>
|
|
738
|
+
|
|
739
|
+
Please provide:
|
|
740
|
+
1. A synthesis of the key points where models agree
|
|
741
|
+
2. Notable differences or disagreements between responses
|
|
742
|
+
3. A balanced conclusion incorporating the best insights
|
|
743
|
+
{f"4. Evaluation of the devil's advocate critique" if devil_response else ""}
|
|
744
|
+
|
|
745
|
+
Be concise and highlight the most important findings."""
|
|
746
|
+
|
|
747
|
+
# Get aggregation
|
|
748
|
+
try:
|
|
749
|
+
if tool_ctx:
|
|
750
|
+
await tool_ctx.info(f"Aggregating responses with {judge_model}...")
|
|
751
|
+
|
|
752
|
+
judge_result = await self._query_single_model(
|
|
753
|
+
judge_model, aggregation_prompt, None, 0.3, None
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
if not judge_result['success']:
|
|
757
|
+
return f"Error: Judge model failed: {judge_result.get('error', 'Unknown error')}"
|
|
758
|
+
|
|
759
|
+
# Format output
|
|
760
|
+
output = [f"=== Consensus Analysis ({len(successful_responses)} models) ===\n"]
|
|
761
|
+
output.append(judge_result['response'])
|
|
762
|
+
|
|
763
|
+
# Add model list
|
|
764
|
+
output.append(f"\nModels consulted: {', '.join([r['model'] for r in successful_responses])}")
|
|
765
|
+
if devil_response:
|
|
766
|
+
output.append(f"Devil's Advocate: {devil_response['model']}")
|
|
767
|
+
|
|
768
|
+
# Add timing info
|
|
769
|
+
avg_time = sum(r['time_ms'] for r in responses) / len(responses)
|
|
770
|
+
output.append(f"\nAverage response time: {avg_time:.0f}ms")
|
|
771
|
+
|
|
772
|
+
# Include raw responses if requested
|
|
773
|
+
if include_raw:
|
|
774
|
+
output.append("\n\n=== Raw Responses ===")
|
|
775
|
+
for r in successful_responses:
|
|
776
|
+
output.append(f"\n{r['model']}:")
|
|
777
|
+
output.append("-" * 40)
|
|
778
|
+
output.append(r['response'])
|
|
779
|
+
|
|
780
|
+
if devil_response:
|
|
781
|
+
output.append(f"\nDevil's Advocate ({devil_response['model']}):")
|
|
782
|
+
output.append("-" * 40)
|
|
783
|
+
output.append(devil_response['response'])
|
|
784
|
+
|
|
785
|
+
return "\n".join(output)
|
|
786
|
+
|
|
787
|
+
except Exception as e:
|
|
788
|
+
return f"Error during aggregation: {str(e)}"
|
|
789
|
+
|
|
284
790
|
def _get_provider_for_model(self, model: str) -> Optional[str]:
|
|
285
791
|
"""Determine the provider for a given model."""
|
|
286
792
|
model_lower = model.lower()
|
|
287
793
|
|
|
288
|
-
# Check explicit provider prefix
|
|
794
|
+
# Check explicit provider prefix
|
|
289
795
|
if "/" in model:
|
|
290
|
-
|
|
291
|
-
return provider
|
|
796
|
+
return model.split("/")[0]
|
|
292
797
|
|
|
293
798
|
# Check model prefixes
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
799
|
+
if model_lower.startswith("gpt"):
|
|
800
|
+
return "openai"
|
|
801
|
+
elif model_lower.startswith("claude"):
|
|
802
|
+
return "anthropic"
|
|
803
|
+
elif model_lower.startswith("gemini"):
|
|
804
|
+
return "google"
|
|
805
|
+
elif model_lower.startswith("command"):
|
|
806
|
+
return "cohere"
|
|
298
807
|
|
|
299
|
-
# Default to OpenAI
|
|
808
|
+
# Default to OpenAI
|
|
300
809
|
return "openai"
|
|
301
810
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
"""Get all available models from LiteLLM organized by provider."""
|
|
305
|
-
if not LITELLM_AVAILABLE:
|
|
306
|
-
return {}
|
|
307
|
-
|
|
811
|
+
def _get_all_models(self) -> Dict[str, List[str]]:
|
|
812
|
+
"""Get all available models from LiteLLM."""
|
|
308
813
|
try:
|
|
309
814
|
import litellm
|
|
310
815
|
|
|
@@ -343,4 +848,4 @@ For consensus across models, use: consensus
|
|
|
343
848
|
|
|
344
849
|
def register(self, mcp_server) -> None:
|
|
345
850
|
"""Register this tool with the MCP server."""
|
|
346
|
-
pass
|
|
851
|
+
pass
|