mcp-rubber-duck 1.1.0
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.
- package/.dockerignore +19 -0
- package/.env.desktop.example +145 -0
- package/.env.example +45 -0
- package/.env.pi.example +106 -0
- package/.env.template +165 -0
- package/.eslintrc.json +40 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +65 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +58 -0
- package/.github/ISSUE_TEMPLATE/question.md +67 -0
- package/.github/pull_request_template.md +111 -0
- package/.github/workflows/docker-build.yml +138 -0
- package/.github/workflows/release.yml +182 -0
- package/.github/workflows/security.yml +141 -0
- package/.github/workflows/semantic-release.yml +89 -0
- package/.prettierrc +10 -0
- package/.releaserc.json +66 -0
- package/CHANGELOG.md +95 -0
- package/CONTRIBUTING.md +242 -0
- package/Dockerfile +62 -0
- package/LICENSE +21 -0
- package/README.md +803 -0
- package/audit-ci.json +8 -0
- package/config/claude_desktop.json +14 -0
- package/config/config.example.json +91 -0
- package/dist/config/config.d.ts +51 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +301 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/types.d.ts +356 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +41 -0
- package/dist/config/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/duck-provider-enhanced.d.ts +29 -0
- package/dist/providers/duck-provider-enhanced.d.ts.map +1 -0
- package/dist/providers/duck-provider-enhanced.js +230 -0
- package/dist/providers/duck-provider-enhanced.js.map +1 -0
- package/dist/providers/enhanced-manager.d.ts +54 -0
- package/dist/providers/enhanced-manager.d.ts.map +1 -0
- package/dist/providers/enhanced-manager.js +217 -0
- package/dist/providers/enhanced-manager.js.map +1 -0
- package/dist/providers/manager.d.ts +28 -0
- package/dist/providers/manager.d.ts.map +1 -0
- package/dist/providers/manager.js +204 -0
- package/dist/providers/manager.js.map +1 -0
- package/dist/providers/provider.d.ts +29 -0
- package/dist/providers/provider.d.ts.map +1 -0
- package/dist/providers/provider.js +179 -0
- package/dist/providers/provider.js.map +1 -0
- package/dist/providers/types.d.ts +69 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +501 -0
- package/dist/server.js.map +1 -0
- package/dist/services/approval.d.ts +44 -0
- package/dist/services/approval.d.ts.map +1 -0
- package/dist/services/approval.js +159 -0
- package/dist/services/approval.js.map +1 -0
- package/dist/services/cache.d.ts +21 -0
- package/dist/services/cache.d.ts.map +1 -0
- package/dist/services/cache.js +63 -0
- package/dist/services/cache.js.map +1 -0
- package/dist/services/conversation.d.ts +24 -0
- package/dist/services/conversation.d.ts.map +1 -0
- package/dist/services/conversation.js +108 -0
- package/dist/services/conversation.js.map +1 -0
- package/dist/services/function-bridge.d.ts +41 -0
- package/dist/services/function-bridge.d.ts.map +1 -0
- package/dist/services/function-bridge.js +259 -0
- package/dist/services/function-bridge.js.map +1 -0
- package/dist/services/health.d.ts +17 -0
- package/dist/services/health.d.ts.map +1 -0
- package/dist/services/health.js +77 -0
- package/dist/services/health.js.map +1 -0
- package/dist/services/mcp-client-manager.d.ts +49 -0
- package/dist/services/mcp-client-manager.d.ts.map +1 -0
- package/dist/services/mcp-client-manager.js +279 -0
- package/dist/services/mcp-client-manager.js.map +1 -0
- package/dist/tools/approve-mcp-request.d.ts +9 -0
- package/dist/tools/approve-mcp-request.d.ts.map +1 -0
- package/dist/tools/approve-mcp-request.js +111 -0
- package/dist/tools/approve-mcp-request.js.map +1 -0
- package/dist/tools/ask-duck.d.ts +9 -0
- package/dist/tools/ask-duck.d.ts.map +1 -0
- package/dist/tools/ask-duck.js +43 -0
- package/dist/tools/ask-duck.js.map +1 -0
- package/dist/tools/chat-duck.d.ts +9 -0
- package/dist/tools/chat-duck.d.ts.map +1 -0
- package/dist/tools/chat-duck.js +57 -0
- package/dist/tools/chat-duck.js.map +1 -0
- package/dist/tools/clear-conversations.d.ts +8 -0
- package/dist/tools/clear-conversations.d.ts.map +1 -0
- package/dist/tools/clear-conversations.js +17 -0
- package/dist/tools/clear-conversations.js.map +1 -0
- package/dist/tools/compare-ducks.d.ts +8 -0
- package/dist/tools/compare-ducks.d.ts.map +1 -0
- package/dist/tools/compare-ducks.js +49 -0
- package/dist/tools/compare-ducks.js.map +1 -0
- package/dist/tools/duck-council.d.ts +8 -0
- package/dist/tools/duck-council.d.ts.map +1 -0
- package/dist/tools/duck-council.js +69 -0
- package/dist/tools/duck-council.js.map +1 -0
- package/dist/tools/get-pending-approvals.d.ts +15 -0
- package/dist/tools/get-pending-approvals.d.ts.map +1 -0
- package/dist/tools/get-pending-approvals.js +74 -0
- package/dist/tools/get-pending-approvals.js.map +1 -0
- package/dist/tools/list-ducks.d.ts +9 -0
- package/dist/tools/list-ducks.d.ts.map +1 -0
- package/dist/tools/list-ducks.js +47 -0
- package/dist/tools/list-ducks.js.map +1 -0
- package/dist/tools/list-models.d.ts +8 -0
- package/dist/tools/list-models.d.ts.map +1 -0
- package/dist/tools/list-models.js +72 -0
- package/dist/tools/list-models.js.map +1 -0
- package/dist/tools/mcp-status.d.ts +17 -0
- package/dist/tools/mcp-status.d.ts.map +1 -0
- package/dist/tools/mcp-status.js +100 -0
- package/dist/tools/mcp-status.js.map +1 -0
- package/dist/utils/ascii-art.d.ts +19 -0
- package/dist/utils/ascii-art.d.ts.map +1 -0
- package/dist/utils/ascii-art.js +73 -0
- package/dist/utils/ascii-art.js.map +1 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +86 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/safe-logger.d.ts +23 -0
- package/dist/utils/safe-logger.d.ts.map +1 -0
- package/dist/utils/safe-logger.js +145 -0
- package/dist/utils/safe-logger.js.map +1 -0
- package/docker-compose.yml +161 -0
- package/jest.config.js +26 -0
- package/package.json +65 -0
- package/scripts/build-multiarch.sh +290 -0
- package/scripts/deploy-raspbian.sh +410 -0
- package/scripts/deploy.sh +322 -0
- package/scripts/gh-deploy.sh +343 -0
- package/scripts/setup-docker-raspbian.sh +530 -0
- package/server.json +8 -0
- package/src/config/config.ts +357 -0
- package/src/config/types.ts +89 -0
- package/src/index.ts +114 -0
- package/src/providers/duck-provider-enhanced.ts +294 -0
- package/src/providers/enhanced-manager.ts +290 -0
- package/src/providers/manager.ts +257 -0
- package/src/providers/provider.ts +207 -0
- package/src/providers/types.ts +78 -0
- package/src/server.ts +603 -0
- package/src/services/approval.ts +225 -0
- package/src/services/cache.ts +79 -0
- package/src/services/conversation.ts +146 -0
- package/src/services/function-bridge.ts +329 -0
- package/src/services/health.ts +107 -0
- package/src/services/mcp-client-manager.ts +362 -0
- package/src/tools/approve-mcp-request.ts +126 -0
- package/src/tools/ask-duck.ts +74 -0
- package/src/tools/chat-duck.ts +82 -0
- package/src/tools/clear-conversations.ts +24 -0
- package/src/tools/compare-ducks.ts +67 -0
- package/src/tools/duck-council.ts +88 -0
- package/src/tools/get-pending-approvals.ts +90 -0
- package/src/tools/list-ducks.ts +65 -0
- package/src/tools/list-models.ts +101 -0
- package/src/tools/mcp-status.ts +117 -0
- package/src/utils/ascii-art.ts +85 -0
- package/src/utils/logger.ts +116 -0
- package/src/utils/safe-logger.ts +165 -0
- package/systemd/mcp-rubber-duck-with-ollama.service +55 -0
- package/systemd/mcp-rubber-duck.service +58 -0
- package/test-functionality.js +147 -0
- package/test-mcp-interface.js +221 -0
- package/tests/ascii-art.test.ts +36 -0
- package/tests/config.test.ts +239 -0
- package/tests/conversation.test.ts +308 -0
- package/tests/mcp-bridge.test.ts +291 -0
- package/tests/providers.test.ts +269 -0
- package/tests/tools/clear-conversations.test.ts +163 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { DuckProvider } from './provider.js';
|
|
2
|
+
import { ChatOptions, ChatResponse, ProviderOptions, OpenAIChatParams, OpenAIMessage, MCPResult, OpenAIToolCall } from './types.js';
|
|
3
|
+
import { FunctionBridge } from '../services/function-bridge.js';
|
|
4
|
+
import { ConversationMessage } from '../config/types.js';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
import { SafeLogger } from '../utils/safe-logger.js';
|
|
7
|
+
|
|
8
|
+
export interface EnhancedChatResponse extends ChatResponse {
|
|
9
|
+
pendingApprovals?: {
|
|
10
|
+
id: string;
|
|
11
|
+
message: string;
|
|
12
|
+
}[];
|
|
13
|
+
mcpResults?: MCPResult[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class EnhancedDuckProvider extends DuckProvider {
|
|
17
|
+
private functionBridge: FunctionBridge;
|
|
18
|
+
private mcpEnabled: boolean;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
name: string,
|
|
22
|
+
nickname: string,
|
|
23
|
+
options: ProviderOptions,
|
|
24
|
+
functionBridge: FunctionBridge,
|
|
25
|
+
mcpEnabled: boolean = true
|
|
26
|
+
) {
|
|
27
|
+
super(name, nickname, options);
|
|
28
|
+
this.functionBridge = functionBridge;
|
|
29
|
+
this.mcpEnabled = mcpEnabled;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async chat(options: ChatOptions): Promise<EnhancedChatResponse> {
|
|
33
|
+
try {
|
|
34
|
+
// If MCP is enabled, add function definitions
|
|
35
|
+
if (this.mcpEnabled) {
|
|
36
|
+
const functions = await this.functionBridge.getFunctionDefinitions();
|
|
37
|
+
if (functions.length > 0) {
|
|
38
|
+
options.tools = functions;
|
|
39
|
+
options.toolChoice = 'auto';
|
|
40
|
+
logger.debug(`Added ${functions.length} MCP functions for ${this.nickname}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Prepare messages for function calling
|
|
45
|
+
const messages = this.prepareMessages(options.messages, options.systemPrompt);
|
|
46
|
+
const modelToUse = options.model || this.options.model;
|
|
47
|
+
|
|
48
|
+
const baseParams: Partial<OpenAIChatParams> = {
|
|
49
|
+
model: modelToUse,
|
|
50
|
+
messages: messages as OpenAIMessage[],
|
|
51
|
+
stream: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Add temperature if model supports it
|
|
55
|
+
if (this.supportsTemperature(modelToUse)) {
|
|
56
|
+
baseParams.temperature = options.temperature ?? this.options.temperature ?? 0.7;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Add tools if available
|
|
60
|
+
if (options.tools && options.tools.length > 0) {
|
|
61
|
+
baseParams.tools = options.tools.map(tool => ({
|
|
62
|
+
type: 'function',
|
|
63
|
+
function: {
|
|
64
|
+
name: tool.name,
|
|
65
|
+
description: tool.description,
|
|
66
|
+
parameters: tool.parameters,
|
|
67
|
+
},
|
|
68
|
+
}));
|
|
69
|
+
baseParams.tool_choice = options.toolChoice || 'auto';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// First API call
|
|
73
|
+
const response = await this.createChatCompletion(baseParams);
|
|
74
|
+
const choice = response.choices[0];
|
|
75
|
+
|
|
76
|
+
// Check if the model wants to call functions
|
|
77
|
+
if (choice.message?.tool_calls && choice.message.tool_calls.length > 0) {
|
|
78
|
+
return await this.handleToolCalls(
|
|
79
|
+
choice.message.tool_calls,
|
|
80
|
+
messages as OpenAIMessage[],
|
|
81
|
+
baseParams,
|
|
82
|
+
modelToUse
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// No tool calls, return regular response
|
|
87
|
+
return {
|
|
88
|
+
content: choice.message?.content || '',
|
|
89
|
+
usage: response.usage ? {
|
|
90
|
+
promptTokens: response.usage.prompt_tokens,
|
|
91
|
+
completionTokens: response.usage.completion_tokens,
|
|
92
|
+
totalTokens: response.usage.total_tokens,
|
|
93
|
+
} : undefined,
|
|
94
|
+
model: modelToUse,
|
|
95
|
+
finishReason: choice.finish_reason || undefined,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
} catch (error: unknown) {
|
|
99
|
+
logger.error(`Enhanced provider ${this.name} chat error:`, error);
|
|
100
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
101
|
+
throw new Error(`Duck ${this.nickname} couldn't respond: ${errorMessage}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async handleToolCalls(
|
|
106
|
+
toolCalls: OpenAIToolCall[],
|
|
107
|
+
messages: OpenAIMessage[],
|
|
108
|
+
baseParams: Partial<OpenAIChatParams>,
|
|
109
|
+
modelToUse: string
|
|
110
|
+
): Promise<EnhancedChatResponse> {
|
|
111
|
+
const pendingApprovals: { id: string; message: string }[] = [];
|
|
112
|
+
const toolMessages: OpenAIMessage[] = [];
|
|
113
|
+
let hasExecutedTools = false;
|
|
114
|
+
|
|
115
|
+
// Add the assistant message with tool calls
|
|
116
|
+
const assistantMessage: OpenAIMessage = {
|
|
117
|
+
role: 'assistant' as const,
|
|
118
|
+
content: null,
|
|
119
|
+
tool_calls: toolCalls,
|
|
120
|
+
};
|
|
121
|
+
messages.push(assistantMessage);
|
|
122
|
+
|
|
123
|
+
// Process each tool call
|
|
124
|
+
for (const toolCall of toolCalls) {
|
|
125
|
+
try {
|
|
126
|
+
const functionName = toolCall.function.name;
|
|
127
|
+
const args = JSON.parse(toolCall.function.arguments) as Record<string, unknown>;
|
|
128
|
+
|
|
129
|
+
logger.info(`${this.nickname} wants to call function: ${functionName}`);
|
|
130
|
+
SafeLogger.debug(`Function call arguments for ${functionName}:`, args);
|
|
131
|
+
|
|
132
|
+
const result = await this.functionBridge.handleFunctionCall(
|
|
133
|
+
this.nickname,
|
|
134
|
+
functionName,
|
|
135
|
+
args
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (result.needsApproval && result.approvalId) {
|
|
139
|
+
// Function needs approval
|
|
140
|
+
pendingApprovals.push({
|
|
141
|
+
id: result.approvalId,
|
|
142
|
+
message: result.message || `Approval needed for ${functionName}`,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Add a tool message indicating approval is needed
|
|
146
|
+
toolMessages.push({
|
|
147
|
+
role: 'tool',
|
|
148
|
+
tool_call_id: toolCall.id,
|
|
149
|
+
content: JSON.stringify({
|
|
150
|
+
status: 'approval_needed',
|
|
151
|
+
approval_id: result.approvalId,
|
|
152
|
+
message: result.message,
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
} else if (result.success && result.data) {
|
|
157
|
+
// Function executed successfully
|
|
158
|
+
hasExecutedTools = true;
|
|
159
|
+
toolMessages.push({
|
|
160
|
+
role: 'tool',
|
|
161
|
+
tool_call_id: toolCall.id,
|
|
162
|
+
content: typeof result.data === 'string'
|
|
163
|
+
? result.data
|
|
164
|
+
: JSON.stringify(result.data),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
} else {
|
|
168
|
+
// Function failed
|
|
169
|
+
toolMessages.push({
|
|
170
|
+
role: 'tool',
|
|
171
|
+
tool_call_id: toolCall.id,
|
|
172
|
+
content: JSON.stringify({
|
|
173
|
+
error: result.error || 'Unknown error',
|
|
174
|
+
}),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
} catch (error: unknown) {
|
|
179
|
+
logger.error(`Error processing tool call ${toolCall.id}:`, error);
|
|
180
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
181
|
+
toolMessages.push({
|
|
182
|
+
role: 'tool',
|
|
183
|
+
tool_call_id: toolCall.id,
|
|
184
|
+
content: JSON.stringify({
|
|
185
|
+
error: `Tool execution failed: ${errorMessage}`,
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// If we have pending approvals, return them without calling the model again
|
|
192
|
+
if (pendingApprovals.length > 0) {
|
|
193
|
+
const approvalMessage = pendingApprovals.length === 1
|
|
194
|
+
? pendingApprovals[0].message
|
|
195
|
+
: `Multiple approvals needed: ${pendingApprovals.map(a => a.id).join(', ')}`;
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
content: `⏳ ${approvalMessage}`,
|
|
199
|
+
model: modelToUse,
|
|
200
|
+
pendingApprovals,
|
|
201
|
+
finishReason: 'tool_calls',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Add tool messages and call model again for final response
|
|
206
|
+
messages.push(...toolMessages);
|
|
207
|
+
|
|
208
|
+
// Remove tools from the follow-up call to get a natural language response
|
|
209
|
+
const followUpParams = {
|
|
210
|
+
...baseParams,
|
|
211
|
+
messages,
|
|
212
|
+
};
|
|
213
|
+
delete followUpParams.tools;
|
|
214
|
+
delete followUpParams.tool_choice;
|
|
215
|
+
|
|
216
|
+
const finalResponse = await this.createChatCompletion(followUpParams);
|
|
217
|
+
const finalChoice = finalResponse.choices[0];
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
content: finalChoice.message?.content || '',
|
|
221
|
+
usage: finalResponse.usage ? {
|
|
222
|
+
promptTokens: finalResponse.usage.prompt_tokens,
|
|
223
|
+
completionTokens: finalResponse.usage.completion_tokens,
|
|
224
|
+
totalTokens: finalResponse.usage.total_tokens,
|
|
225
|
+
} : undefined,
|
|
226
|
+
model: modelToUse,
|
|
227
|
+
finishReason: finalChoice.finish_reason || undefined,
|
|
228
|
+
mcpResults: hasExecutedTools ? (toolMessages as unknown as MCPResult[]) : undefined,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Method to retry with approval
|
|
233
|
+
async retryWithApproval(
|
|
234
|
+
approvalId: string,
|
|
235
|
+
originalMessages: ConversationMessage[],
|
|
236
|
+
options: ChatOptions
|
|
237
|
+
): Promise<EnhancedChatResponse> {
|
|
238
|
+
// Add approval ID to the tool arguments
|
|
239
|
+
if (options.tools) {
|
|
240
|
+
options.tools = options.tools.map(tool => ({
|
|
241
|
+
...tool,
|
|
242
|
+
parameters: {
|
|
243
|
+
...tool.parameters,
|
|
244
|
+
properties: {
|
|
245
|
+
...(tool.parameters.properties as Record<string, unknown>),
|
|
246
|
+
_approval_id: {
|
|
247
|
+
type: 'string',
|
|
248
|
+
default: approvalId,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Retry the chat with the approval ID
|
|
256
|
+
return this.chat({
|
|
257
|
+
...options,
|
|
258
|
+
messages: originalMessages,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check if MCP functions are available
|
|
263
|
+
async getMCPFunctionCount(): Promise<number> {
|
|
264
|
+
if (!this.mcpEnabled) {
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const functions = await this.functionBridge.getFunctionDefinitions();
|
|
270
|
+
return functions.length;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Get MCP bridge statistics
|
|
277
|
+
getMCPStats() {
|
|
278
|
+
if (!this.mcpEnabled) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return this.functionBridge.getStats();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Enable/disable MCP for this provider
|
|
286
|
+
setMCPEnabled(enabled: boolean): void {
|
|
287
|
+
this.mcpEnabled = enabled;
|
|
288
|
+
logger.info(`MCP ${enabled ? 'enabled' : 'disabled'} for ${this.nickname}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
isMCPEnabled(): boolean {
|
|
292
|
+
return this.mcpEnabled;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { EnhancedDuckProvider } from './duck-provider-enhanced.js';
|
|
2
|
+
import { ProviderManager } from './manager.js';
|
|
3
|
+
import { ConfigManager } from '../config/config.js';
|
|
4
|
+
import { FunctionBridge } from '../services/function-bridge.js';
|
|
5
|
+
import { DuckResponse } from '../config/types.js';
|
|
6
|
+
import { ChatOptions, MCPResult } from './types.js';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
|
|
9
|
+
export class EnhancedProviderManager extends ProviderManager {
|
|
10
|
+
private enhancedProviders: Map<string, EnhancedDuckProvider> = new Map();
|
|
11
|
+
private functionBridge?: FunctionBridge;
|
|
12
|
+
private mcpEnabled: boolean = false;
|
|
13
|
+
|
|
14
|
+
constructor(configManager: ConfigManager, functionBridge?: FunctionBridge) {
|
|
15
|
+
super(configManager);
|
|
16
|
+
this.functionBridge = functionBridge;
|
|
17
|
+
this.mcpEnabled = !!functionBridge &&
|
|
18
|
+
(configManager.getConfig().mcp_bridge?.enabled || false);
|
|
19
|
+
|
|
20
|
+
if (this.mcpEnabled) {
|
|
21
|
+
this.initializeEnhancedProviders();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private initializeEnhancedProviders() {
|
|
26
|
+
if (!this.functionBridge) {
|
|
27
|
+
logger.warn('Function bridge not available, skipping enhanced providers');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const config = this.configManager.getConfig();
|
|
32
|
+
const allProviders = config.providers;
|
|
33
|
+
|
|
34
|
+
for (const [name, providerConfig] of Object.entries(allProviders)) {
|
|
35
|
+
try {
|
|
36
|
+
// Create enhanced provider if MCP is enabled
|
|
37
|
+
const enhancedProvider = new EnhancedDuckProvider(
|
|
38
|
+
name,
|
|
39
|
+
providerConfig.nickname,
|
|
40
|
+
{
|
|
41
|
+
apiKey: providerConfig.api_key,
|
|
42
|
+
baseURL: providerConfig.base_url,
|
|
43
|
+
model: providerConfig.default_model,
|
|
44
|
+
availableModels: providerConfig.models,
|
|
45
|
+
temperature: providerConfig.temperature,
|
|
46
|
+
timeout: providerConfig.timeout,
|
|
47
|
+
maxRetries: providerConfig.max_retries,
|
|
48
|
+
systemPrompt: providerConfig.system_prompt,
|
|
49
|
+
},
|
|
50
|
+
this.functionBridge,
|
|
51
|
+
this.mcpEnabled
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
this.enhancedProviders.set(name, enhancedProvider);
|
|
55
|
+
logger.info(`Initialized enhanced provider: ${name} (${providerConfig.nickname}) with MCP support`);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
logger.error(`Failed to initialize enhanced provider ${name}:`, error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getEnhancedProvider(name?: string): EnhancedDuckProvider {
|
|
63
|
+
if (!this.mcpEnabled) {
|
|
64
|
+
throw new Error('MCP bridge is not enabled');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const providerName = name || this.configManager.getConfig().default_provider;
|
|
68
|
+
|
|
69
|
+
if (!providerName) {
|
|
70
|
+
throw new Error('No provider specified and no default provider configured');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const provider = this.enhancedProviders.get(providerName);
|
|
74
|
+
|
|
75
|
+
if (!provider) {
|
|
76
|
+
throw new Error(`Enhanced duck "${providerName}" not found in the pond`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return provider;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async askDuckWithMCP(
|
|
83
|
+
providerName: string | undefined,
|
|
84
|
+
prompt: string,
|
|
85
|
+
options?: Partial<ChatOptions>
|
|
86
|
+
): Promise<DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[] }> {
|
|
87
|
+
if (!this.mcpEnabled) {
|
|
88
|
+
// Fall back to regular provider
|
|
89
|
+
return this.askDuck(providerName, prompt, options);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const provider = this.getEnhancedProvider(providerName);
|
|
93
|
+
const startTime = Date.now();
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const response = await provider.chat({
|
|
97
|
+
messages: [{ role: 'user', content: prompt, timestamp: new Date() }],
|
|
98
|
+
...options,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
provider: provider.name,
|
|
103
|
+
nickname: provider.nickname,
|
|
104
|
+
model: response.model,
|
|
105
|
+
content: response.content,
|
|
106
|
+
usage: response.usage ? {
|
|
107
|
+
prompt_tokens: response.usage.promptTokens,
|
|
108
|
+
completion_tokens: response.usage.completionTokens,
|
|
109
|
+
total_tokens: response.usage.totalTokens,
|
|
110
|
+
promptTokens: response.usage.promptTokens,
|
|
111
|
+
completionTokens: response.usage.completionTokens,
|
|
112
|
+
totalTokens: response.usage.totalTokens,
|
|
113
|
+
} : undefined,
|
|
114
|
+
latency: Date.now() - startTime,
|
|
115
|
+
cached: false,
|
|
116
|
+
pendingApprovals: response.pendingApprovals,
|
|
117
|
+
mcpResults: response.mcpResults,
|
|
118
|
+
};
|
|
119
|
+
} catch (error: unknown) {
|
|
120
|
+
// Try failover if enabled
|
|
121
|
+
if (this.configManager.getConfig().enable_failover && providerName === undefined) {
|
|
122
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
123
|
+
logger.warn(`Primary enhanced provider failed, attempting failover: ${errorMessage}`);
|
|
124
|
+
return this.askDuckWithMCPFailover(prompt, options, provider.name);
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async askDuckWithMCPFailover(
|
|
131
|
+
prompt: string,
|
|
132
|
+
options: Partial<ChatOptions> | undefined,
|
|
133
|
+
failedProvider: string
|
|
134
|
+
): Promise<DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[] }> {
|
|
135
|
+
const availableProviders = Array.from(this.enhancedProviders.keys()).filter(
|
|
136
|
+
name => name !== failedProvider
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
for (const providerName of availableProviders) {
|
|
140
|
+
try {
|
|
141
|
+
logger.info(`Trying enhanced failover to ${providerName}...`);
|
|
142
|
+
return await this.askDuckWithMCP(providerName, prompt, options);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
logger.warn(`Enhanced failover to ${providerName} failed:`, error);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throw new Error('All enhanced ducks have flown away! No providers available.');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async compareDucksWithMCP(
|
|
153
|
+
prompt: string,
|
|
154
|
+
providerNames?: string[],
|
|
155
|
+
options?: Partial<ChatOptions>
|
|
156
|
+
): Promise<Array<DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[] }>> {
|
|
157
|
+
if (!this.mcpEnabled) {
|
|
158
|
+
// Fall back to regular comparison
|
|
159
|
+
return this.compareDucks(prompt, providerNames, options);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const providersToUse = providerNames
|
|
163
|
+
? providerNames.map(name => this.enhancedProviders.get(name)).filter(Boolean)
|
|
164
|
+
: Array.from(this.enhancedProviders.values());
|
|
165
|
+
|
|
166
|
+
if (providersToUse.length === 0) {
|
|
167
|
+
throw new Error('No valid enhanced providers specified');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const promises = providersToUse.map(provider =>
|
|
171
|
+
provider ? this.askDuckWithMCP(provider.name, prompt, options).catch(error => ({
|
|
172
|
+
provider: provider.name,
|
|
173
|
+
nickname: provider.nickname,
|
|
174
|
+
model: '',
|
|
175
|
+
content: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
176
|
+
latency: 0,
|
|
177
|
+
cached: false,
|
|
178
|
+
})) : Promise.resolve({
|
|
179
|
+
provider: 'unknown',
|
|
180
|
+
nickname: 'Unknown',
|
|
181
|
+
model: '',
|
|
182
|
+
content: 'Error: Invalid provider',
|
|
183
|
+
latency: 0,
|
|
184
|
+
cached: false,
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
return Promise.all(promises);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async duckCouncilWithMCP(
|
|
192
|
+
prompt: string,
|
|
193
|
+
options?: Partial<ChatOptions>
|
|
194
|
+
): Promise<Array<DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[] }>> {
|
|
195
|
+
return this.compareDucksWithMCP(prompt, undefined, options);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Method to retry with approval
|
|
199
|
+
async retryWithApproval(
|
|
200
|
+
approvalId: string,
|
|
201
|
+
providerName: string | undefined,
|
|
202
|
+
prompt: string,
|
|
203
|
+
options?: Partial<ChatOptions>
|
|
204
|
+
): Promise<DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[] }> {
|
|
205
|
+
if (!this.mcpEnabled) {
|
|
206
|
+
throw new Error('MCP bridge is not enabled');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const provider = this.getEnhancedProvider(providerName);
|
|
210
|
+
const startTime = Date.now();
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const response = await provider.retryWithApproval(
|
|
214
|
+
approvalId,
|
|
215
|
+
[{ role: 'user', content: prompt, timestamp: new Date() }],
|
|
216
|
+
{
|
|
217
|
+
messages: [{ role: 'user', content: prompt, timestamp: new Date() }],
|
|
218
|
+
...options,
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
provider: provider.name,
|
|
224
|
+
nickname: provider.nickname,
|
|
225
|
+
model: response.model,
|
|
226
|
+
content: response.content,
|
|
227
|
+
usage: response.usage ? {
|
|
228
|
+
prompt_tokens: response.usage.promptTokens,
|
|
229
|
+
completion_tokens: response.usage.completionTokens,
|
|
230
|
+
total_tokens: response.usage.totalTokens,
|
|
231
|
+
promptTokens: response.usage.promptTokens,
|
|
232
|
+
completionTokens: response.usage.completionTokens,
|
|
233
|
+
totalTokens: response.usage.totalTokens,
|
|
234
|
+
} : undefined,
|
|
235
|
+
latency: Date.now() - startTime,
|
|
236
|
+
cached: false,
|
|
237
|
+
pendingApprovals: response.pendingApprovals,
|
|
238
|
+
mcpResults: response.mcpResults,
|
|
239
|
+
};
|
|
240
|
+
} catch (error: unknown) {
|
|
241
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
242
|
+
throw new Error(`Failed to retry with approval: ${errorMessage}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Get enhanced provider statistics
|
|
247
|
+
getAllEnhancedProviders(): Array<{
|
|
248
|
+
name: string;
|
|
249
|
+
info: ReturnType<EnhancedDuckProvider['getInfo']>;
|
|
250
|
+
mcpEnabled: boolean;
|
|
251
|
+
mcpStats?: ReturnType<EnhancedDuckProvider['getMCPStats']>;
|
|
252
|
+
functionCount?: number;
|
|
253
|
+
}> {
|
|
254
|
+
return Array.from(this.enhancedProviders.entries()).map(([name, provider]) => ({
|
|
255
|
+
name,
|
|
256
|
+
info: provider.getInfo(),
|
|
257
|
+
mcpEnabled: provider.isMCPEnabled(),
|
|
258
|
+
mcpStats: provider.getMCPStats(),
|
|
259
|
+
functionCount: 0, // Will be populated when functions are loaded
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Check if MCP is enabled
|
|
264
|
+
isMCPEnabled(): boolean {
|
|
265
|
+
return this.mcpEnabled;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Enable/disable MCP for all providers
|
|
269
|
+
setMCPEnabled(enabled: boolean): void {
|
|
270
|
+
this.mcpEnabled = enabled;
|
|
271
|
+
for (const provider of this.enhancedProviders.values()) {
|
|
272
|
+
provider.setMCPEnabled(enabled);
|
|
273
|
+
}
|
|
274
|
+
logger.info(`MCP ${enabled ? 'enabled' : 'disabled'} for all providers`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Get MCP function count for a provider
|
|
278
|
+
async getMCPFunctionCount(providerName?: string): Promise<number> {
|
|
279
|
+
if (!this.mcpEnabled) {
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const provider = this.getEnhancedProvider(providerName);
|
|
285
|
+
return await provider.getMCPFunctionCount();
|
|
286
|
+
} catch (error) {
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|