diff --git a/mycoder.config.js b/mycoder.config.js index e6877d4..ead98ee 100644 --- a/mycoder.config.js +++ b/mycoder.config.js @@ -9,12 +9,13 @@ export default { pageFilter: 'none', // 'simple', 'none', or 'readability' // Model settings - provider: 'anthropic', - model: 'claude-3-7-sonnet-20250219', + //provider: 'anthropic', + //model: 'claude-3-7-sonnet-20250219', //provider: 'openai', //model: 'gpt-4o', //provider: 'ollama', //model: 'medragondot/Sky-T1-32B-Preview:latest', + //model: 'llama3.2:3b', maxTokens: 4096, temperature: 0.7, diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 6606f57..57ec9be 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,3 +1,21 @@ +# [mycoder-agent-v1.3.0](https://github.com/drivecore/mycoder/compare/mycoder-agent-v1.2.0...mycoder-agent-v1.3.0) (2025-03-12) + +### Features + +- implement MCP tools support ([2d99ac8](https://github.com/drivecore/mycoder/commit/2d99ac8cefaa770e368d469355a509739aafe6a3)) + +# [mycoder-agent-v1.2.0](https://github.com/drivecore/mycoder/compare/mycoder-agent-v1.1.0...mycoder-agent-v1.2.0) (2025-03-12) + +### Bug Fixes + +- Fix TypeScript errors in MCP implementation ([f5837d3](https://github.com/drivecore/mycoder/commit/f5837d3a5dd219efc8e1d811e467f4bb695a1d94)) + +### Features + +- Add basic Model Context Protocol (MCP) support ([8ec9619](https://github.com/drivecore/mycoder/commit/8ec9619c3cc63df8f14222762f5da0bcabe273a5)) +- **agent:** implement incremental resource cleanup for agent lifecycle ([576436e](https://github.com/drivecore/mycoder/commit/576436ef2c7c5f234f088b7dba2e7fd65590738f)), closes [#236](https://github.com/drivecore/mycoder/issues/236) +- background tools is now scope to agents ([e55817f](https://github.com/drivecore/mycoder/commit/e55817f32b373fdbff8bb1ac90105b272044d33f)) + # [mycoder-agent-v1.1.0](https://github.com/drivecore/mycoder/compare/mycoder-agent-v1.0.0...mycoder-agent-v1.1.0) (2025-03-12) ### Bug Fixes diff --git a/packages/agent/package.json b/packages/agent/package.json index 6da2eef..48e871f 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "mycoder-agent", - "version": "1.1.0", + "version": "1.3.0", "description": "Agent module for mycoder - an AI-powered software development assistant", "type": "module", "main": "dist/index.js", @@ -52,6 +52,7 @@ "chalk": "^5.4.1", "dotenv": "^16", "jsdom": "^26.0.0", + "ollama": "^0.5.14", "openai": "^4.87.3", "playwright": "^1.50.1", "uuid": "^11", diff --git a/packages/agent/src/core/executeToolCall.ts b/packages/agent/src/core/executeToolCall.ts index 077ac90..2828d03 100644 --- a/packages/agent/src/core/executeToolCall.ts +++ b/packages/agent/src/core/executeToolCall.ts @@ -14,7 +14,10 @@ export const executeToolCall = async ( ): Promise => { const tool = tools.find((t) => t.name === toolCall.name); if (!tool) { - throw new Error(`No tool with the name '${toolCall.name}' exists.`); + return JSON.stringify({ + error: true, + message: `No tool with the name '${toolCall.name}' exists.`, + }); } const logger = new Logger({ diff --git a/packages/agent/src/core/llm/providers/ollama.ts b/packages/agent/src/core/llm/providers/ollama.ts index c3a4869..df9ebcb 100644 --- a/packages/agent/src/core/llm/providers/ollama.ts +++ b/packages/agent/src/core/llm/providers/ollama.ts @@ -1,14 +1,25 @@ /** - * Ollama provider implementation + * Ollama provider implementation using the official Ollama npm package */ +import { + ChatRequest as OllamaChatRequest, + ChatResponse as OllamaChatResponse, + Ollama, + ToolCall as OllamaTooCall, + Tool as OllamaTool, + Message as OllamaMessage, +} from 'ollama'; + import { TokenUsage } from '../../tokens.js'; +import { ToolCall } from '../../types.js'; import { LLMProvider } from '../provider.js'; import { GenerateOptions, LLMResponse, Message, ProviderOptions, + FunctionDefinition, } from '../types.js'; /** @@ -19,29 +30,26 @@ export interface OllamaOptions extends ProviderOptions { } /** - * Ollama provider implementation + * Ollama provider implementation using the official Ollama npm package */ export class OllamaProvider implements LLMProvider { name: string = 'ollama'; provider: string = 'ollama.chat'; model: string; - private baseUrl: string; + private client: Ollama; constructor(model: string, options: OllamaOptions = {}) { this.model = model; - this.baseUrl = + const baseUrl = options.baseUrl || process.env.OLLAMA_BASE_URL || 'http://localhost:11434'; - // Ensure baseUrl doesn't end with a slash - if (this.baseUrl.endsWith('/')) { - this.baseUrl = this.baseUrl.slice(0, -1); - } + this.client = new Ollama({ host: baseUrl }); } /** - * Generate text using Ollama API + * Generate text using Ollama API via the official npm package */ async generateText(options: GenerateOptions): Promise { const { @@ -52,126 +60,171 @@ export class OllamaProvider implements LLMProvider { topP, frequencyPenalty, presencePenalty, + stopSequences, } = options; // Format messages for Ollama API const formattedMessages = this.formatMessages(messages); - try { - // Prepare request options - const requestOptions: any = { - model: this.model, - messages: formattedMessages, - stream: false, - options: { - temperature: temperature, - // Ollama uses top_k instead of top_p, but we'll include top_p if provided - ...(topP !== undefined && { top_p: topP }), - ...(frequencyPenalty !== undefined && { - frequency_penalty: frequencyPenalty, - }), - ...(presencePenalty !== undefined && { - presence_penalty: presencePenalty, - }), - }, + // Prepare request options + const requestOptions: OllamaChatRequest = { + model: this.model, + messages: formattedMessages, + stream: false, + options: { + temperature: temperature, + ...(topP !== undefined && { top_p: topP }), + ...(frequencyPenalty !== undefined && { + frequency_penalty: frequencyPenalty, + }), + ...(presencePenalty !== undefined && { + presence_penalty: presencePenalty, + }), + ...(stopSequences && + stopSequences.length > 0 && { stop: stopSequences }), + }, + }; + + // Add max_tokens if provided + if (maxTokens !== undefined) { + requestOptions.options = { + ...requestOptions.options, + num_predict: maxTokens, }; + } - // Add max_tokens if provided - if (maxTokens !== undefined) { - requestOptions.options.num_predict = maxTokens; - } + // Add functions/tools if provided + if (functions && functions.length > 0) { + requestOptions.tools = this.convertFunctionsToTools(functions); + } - // Add functions/tools if provided - if (functions && functions.length > 0) { - requestOptions.tools = functions.map((fn) => ({ - name: fn.name, - description: fn.description, - parameters: fn.parameters, - })); - } + // Make the API request using the Ollama client + const response: OllamaChatResponse = await this.client.chat({ + ...requestOptions, + stream: false, + }); - // Make the API request - const response = await fetch(`${this.baseUrl}/api/chat`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestOptions), - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Ollama API error: ${response.status} ${errorText}`); - } + // Extract content and tool calls + const content = response.message?.content || ''; + + // Handle tool calls if present + const toolCalls = this.extractToolCalls(response); + + // Create token usage from response data + const tokenUsage = new TokenUsage(); + tokenUsage.output = response.eval_count || 0; + tokenUsage.input = response.prompt_eval_count || 0; - const data = await response.json(); + return { + text: content, + toolCalls: toolCalls, + tokenUsage: tokenUsage, + }; + } + + /* + interface Tool { + type: string; + function: { + name: string; + description: string; + parameters: { + type: string; + required: string[]; + properties: { + [key: string]: { + type: string; + description: string; + enum?: string[]; + }; + }; + }; + }; +}*/ - // Extract content and tool calls - const content = data.message?.content || ''; - const toolCalls = - data.message?.tool_calls?.map((toolCall: any) => ({ - id: - toolCall.id || - `tool-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`, - name: toolCall.name, - content: JSON.stringify(toolCall.args || toolCall.arguments || {}), - })) || []; + /** + * Convert our function definitions to Ollama tool format + */ + private convertFunctionsToTools( + functions: FunctionDefinition[], + ): OllamaTool[] { + return functions.map( + (fn) => + ({ + type: 'function', + function: { + name: fn.name, + description: fn.description, + parameters: fn.parameters, + }, + }) as OllamaTool, + ); + } - // Create token usage from response data - const tokenUsage = new TokenUsage(); - tokenUsage.input = data.prompt_eval_count || 0; - tokenUsage.output = data.eval_count || 0; + /** + * Extract tool calls from Ollama response + */ + private extractToolCalls(response: OllamaChatResponse): ToolCall[] { + if (!response.message?.tool_calls) { + return []; + } + return response.message.tool_calls.map((toolCall: OllamaTooCall) => { + //console.log('ollama tool call', toolCall); return { - text: content, - toolCalls: toolCalls, - tokenUsage: tokenUsage, + id: `tool-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`, + name: toolCall.function?.name, + content: + typeof toolCall.function?.arguments === 'string' + ? toolCall.function.arguments + : JSON.stringify(toolCall.function?.arguments || {}), }; - } catch (error) { - throw new Error(`Error calling Ollama API: ${(error as Error).message}`); - } + }); } /** * Format messages for Ollama API */ - private formatMessages(messages: Message[]): any[] { - return messages.map((msg) => { - if ( - msg.role === 'user' || - msg.role === 'assistant' || - msg.role === 'system' - ) { - return { - role: msg.role, - content: msg.content, - }; - } else if (msg.role === 'tool_result') { - // Ollama expects tool results as a 'tool' role - return { - role: 'tool', - content: msg.content, - tool_call_id: msg.tool_use_id, - }; - } else if (msg.role === 'tool_use') { - // We'll convert tool_use to assistant messages with tool_calls - return { - role: 'assistant', - content: '', - tool_calls: [ + private formatMessages(messages: Message[]): OllamaMessage[] { + const output: OllamaMessage[] = []; + + messages.forEach((msg) => { + switch (msg.role) { + case 'user': + case 'assistant': + case 'system': + output.push({ + role: msg.role, + content: msg.content, + } satisfies OllamaMessage); + break; + case 'tool_result': + // Ollama expects tool results as a 'tool' role + output.push({ + role: 'tool', + content: + typeof msg.content === 'string' + ? msg.content + : JSON.stringify(msg.content), + } as OllamaMessage); + break; + case 'tool_use': { + // So there is an issue here is that ollama expects tool calls to be part of the assistant message + // get last message and add tool call to it + const lastMessage: OllamaMessage = output[output.length - 1]!; + lastMessage.tool_calls = [ { - id: msg.id, - name: msg.name, - arguments: msg.content, + function: { + name: msg.name, + arguments: JSON.parse(msg.content), + }, }, - ], - }; + ]; + break; + } } - // Default fallback for unknown message types - return { - role: 'user', - content: (msg as any).content || '', - }; }); + + return output; } } diff --git a/packages/agent/src/core/toolAgent.test.ts b/packages/agent/src/core/toolAgent.test.ts index 5bf787b..48f23c6 100644 --- a/packages/agent/src/core/toolAgent.test.ts +++ b/packages/agent/src/core/toolAgent.test.ts @@ -75,17 +75,21 @@ describe('toolAgent', () => { }); it('should handle unknown tools', async () => { - await expect( - executeToolCall( - { - id: '1', - name: 'nonexistentTool', - content: JSON.stringify({}), - }, - [mockTool], - toolContext, - ), - ).rejects.toThrow("No tool with the name 'nonexistentTool' exists."); + const result = await executeToolCall( + { + id: '1', + name: 'nonexistentTool', + content: JSON.stringify({}), + }, + [mockTool], + toolContext, + ); + + // Parse the result as JSON + const parsedResult = JSON.parse(result); + + // Check that it contains the expected error properties + expect(parsedResult.error).toBe(true); }); it('should handle tool execution errors', async () => { diff --git a/packages/agent/src/tools/getTools.ts b/packages/agent/src/tools/getTools.ts index fd77c0b..38fb49e 100644 --- a/packages/agent/src/tools/getTools.ts +++ b/packages/agent/src/tools/getTools.ts @@ -4,8 +4,7 @@ import { Tool } from '../core/types.js'; // Import tools import { browseMessageTool } from './browser/browseMessage.js'; import { browseStartTool } from './browser/browseStart.js'; -import { agentMessageTool } from './interaction/agentMessage.js'; -import { agentStartTool } from './interaction/agentStart.js'; +import { subAgentTool } from './interaction/subAgent.js'; import { userPromptTool } from './interaction/userPrompt.js'; import { fetchTool } from './io/fetch.js'; import { textEditorTool } from './io/textEditor.js'; @@ -31,8 +30,9 @@ export function getTools(options?: GetToolsOptions): Tool[] { // Force cast to Tool type to avoid TypeScript issues const tools: Tool[] = [ textEditorTool as unknown as Tool, - agentStartTool as unknown as Tool, - agentMessageTool as unknown as Tool, + subAgentTool as unknown as Tool, + /*agentStartTool as unknown as Tool, + agentMessageTool as unknown as Tool,*/ sequenceCompleteTool as unknown as Tool, fetchTool as unknown as Tool, shellStartTool as unknown as Tool, diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 204485d..4ef92b2 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,3 +1,17 @@ +# [mycoder-v1.3.0](https://github.com/drivecore/mycoder/compare/mycoder-v1.2.0...mycoder-v1.3.0) (2025-03-12) + +### Features + +- implement MCP tools support ([2d99ac8](https://github.com/drivecore/mycoder/commit/2d99ac8cefaa770e368d469355a509739aafe6a3)) + +# [mycoder-v1.2.0](https://github.com/drivecore/mycoder/compare/mycoder-v1.1.1...mycoder-v1.2.0) (2025-03-12) + +### Features + +- Add basic Model Context Protocol (MCP) support ([8ec9619](https://github.com/drivecore/mycoder/commit/8ec9619c3cc63df8f14222762f5da0bcabe273a5)) +- **agent:** implement incremental resource cleanup for agent lifecycle ([576436e](https://github.com/drivecore/mycoder/commit/576436ef2c7c5f234f088b7dba2e7fd65590738f)), closes [#236](https://github.com/drivecore/mycoder/issues/236) +- background tools is now scope to agents ([e55817f](https://github.com/drivecore/mycoder/commit/e55817f32b373fdbff8bb1ac90105b272044d33f)) + # [mycoder-v1.1.1](https://github.com/drivecore/mycoder/compare/mycoder-v1.1.0...mycoder-v1.1.1) (2025-03-12) ### Bug Fixes diff --git a/packages/cli/package.json b/packages/cli/package.json index 79374fb..60e698a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "mycoder", "description": "A command line tool using agent that can do arbitrary tasks, including coding tasks", - "version": "1.1.1", + "version": "1.3.0", "type": "module", "bin": "./bin/cli.js", "main": "./dist/index.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65a9a06..3ead4bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,6 +111,9 @@ importers: jsdom: specifier: ^26.0.0 version: 26.0.0 + ollama: + specifier: ^0.5.14 + version: 0.5.14 openai: specifier: ^4.87.3 version: 4.87.3(ws@8.18.1)(zod@3.24.2) @@ -3251,6 +3254,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + ollama@0.5.14: + resolution: {integrity: sha512-pvOuEYa2WkkAumxzJP0RdEYHkbZ64AYyyUszXVX7ruLvk5L+EiO2G71da2GqEQ4IAk4j6eLoUbGk5arzFT1wJA==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -4395,6 +4401,9 @@ packages: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} @@ -7833,6 +7842,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + ollama@0.5.14: + dependencies: + whatwg-fetch: 3.6.20 + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -9018,6 +9031,8 @@ snapshots: dependencies: iconv-lite: 0.6.3 + whatwg-fetch@3.6.20: {} + whatwg-mimetype@4.0.0: {} whatwg-url@14.1.1: