immersive2/server/services/toolConverter.js
Michael Mainguy 74a2d179b9 Add Ollama as alternative AI provider and modify_connection tool
Ollama Integration:
- Add providerConfig.js for managing AI provider settings
- Add toolConverter.js to convert between Claude and Ollama formats
- Add ollama.js API handler with function calling support
- Update diagramAI.ts with Ollama models (llama3.1, mistral, qwen2.5)
- Route requests to appropriate provider based on selected model
- Use 127.0.0.1 to avoid IPv6 resolution issues

New modify_connection Tool:
- Add modify_connection tool to change connection labels and colors
- Support finding connections by label or by from/to entities
- Add chatModifyConnection event handler in diagramManager
- Clarify in tool descriptions that empty string removes labels

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 12:08:17 -06:00

246 lines
7.2 KiB
JavaScript

/**
* Tool Format Converter
* Converts between Claude and Ollama tool/function formats
*/
/**
* Convert Claude tool definition to Ollama function format
*
* Claude format:
* { name: "...", description: "...", input_schema: { type: "object", properties: {...} } }
*
* Ollama format:
* { type: "function", function: { name: "...", description: "...", parameters: {...} } }
*
* @param {object} claudeTool - Tool in Claude format
* @returns {object} Tool in Ollama format
*/
export function claudeToolToOllama(claudeTool) {
return {
type: "function",
function: {
name: claudeTool.name,
description: claudeTool.description,
parameters: claudeTool.input_schema
}
};
}
/**
* Convert array of Claude tools to Ollama format
* @param {Array} claudeTools - Array of Claude tool definitions
* @returns {Array} Array of Ollama function definitions
*/
export function claudeToolsToOllama(claudeTools) {
if (!claudeTools || !Array.isArray(claudeTools)) {
return [];
}
return claudeTools.map(claudeToolToOllama);
}
/**
* Convert Ollama tool call to Claude format
*
* Ollama format (in message):
* { tool_calls: [{ function: { name: "...", arguments: {...} } }] }
*
* Claude format:
* { type: "tool_use", id: "...", name: "...", input: {...} }
*
* @param {object} ollamaToolCall - Tool call from Ollama response
* @param {number} index - Index for generating unique ID
* @returns {object} Tool call in Claude format
*/
export function ollamaToolCallToClaude(ollamaToolCall, index = 0) {
const func = ollamaToolCall.function;
// Parse arguments if it's a string
let input = func.arguments;
if (typeof input === 'string') {
try {
input = JSON.parse(input);
} catch (e) {
console.warn('[ToolConverter] Failed to parse tool arguments:', e);
input = {};
}
}
return {
type: "tool_use",
id: `toolu_ollama_${Date.now()}_${index}`,
name: func.name,
input: input || {}
};
}
/**
* Convert Claude tool result to Ollama format
*
* Claude format (in messages):
* { role: "user", content: [{ type: "tool_result", tool_use_id: "...", content: "..." }] }
*
* Ollama format:
* { role: "tool", content: "...", name: "..." }
*
* @param {object} claudeToolResult - Tool result in Claude format
* @param {string} toolName - Name of the tool (from previous tool_use)
* @returns {object} Tool result in Ollama message format
*/
export function claudeToolResultToOllama(claudeToolResult, toolName) {
let content = claudeToolResult.content;
// Stringify if it's an object
if (typeof content === 'object') {
content = JSON.stringify(content);
}
return {
role: "tool",
content: content,
name: toolName
};
}
/**
* Convert Claude messages array to Ollama format
* Handles regular messages and tool result messages
*
* @param {Array} claudeMessages - Messages in Claude format
* @param {string} systemPrompt - System prompt to prepend
* @returns {Array} Messages in Ollama format
*/
export function claudeMessagesToOllama(claudeMessages, systemPrompt) {
const ollamaMessages = [];
// Add system message if provided
if (systemPrompt) {
ollamaMessages.push({
role: "system",
content: systemPrompt
});
}
// Track tool names for tool results
const toolNameMap = new Map();
for (const msg of claudeMessages) {
if (msg.role === 'user') {
// Check if it's a tool result message
if (Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === 'tool_result') {
const toolName = toolNameMap.get(block.tool_use_id) || 'unknown';
ollamaMessages.push(claudeToolResultToOllama(block, toolName));
} else if (block.type === 'text') {
ollamaMessages.push({
role: "user",
content: block.text
});
}
}
} else {
ollamaMessages.push({
role: "user",
content: msg.content
});
}
} else if (msg.role === 'assistant') {
// Handle assistant messages with potential tool calls
if (Array.isArray(msg.content)) {
let textContent = '';
const toolCalls = [];
for (const block of msg.content) {
if (block.type === 'text') {
textContent += block.text;
} else if (block.type === 'tool_use') {
// Track tool name for later tool results
toolNameMap.set(block.id, block.name);
toolCalls.push({
function: {
name: block.name,
// Ollama expects arguments as object, not string
arguments: block.input || {}
}
});
}
}
const assistantMsg = {
role: "assistant",
content: textContent || ""
};
if (toolCalls.length > 0) {
assistantMsg.tool_calls = toolCalls;
}
ollamaMessages.push(assistantMsg);
} else {
ollamaMessages.push({
role: "assistant",
content: msg.content
});
}
}
}
return ollamaMessages;
}
/**
* Convert Ollama response to Claude format
*
* @param {object} ollamaResponse - Response from Ollama API
* @returns {object} Response in Claude format
*/
export function ollamaResponseToClaude(ollamaResponse) {
const content = [];
const message = ollamaResponse.message;
// Add text content if present
if (message.content) {
content.push({
type: "text",
text: message.content
});
}
// Add tool calls if present
if (message.tool_calls && message.tool_calls.length > 0) {
for (let i = 0; i < message.tool_calls.length; i++) {
content.push(ollamaToolCallToClaude(message.tool_calls[i], i));
}
}
// Determine stop reason
let stopReason = "end_turn";
if (message.tool_calls && message.tool_calls.length > 0) {
stopReason = "tool_use";
} else if (ollamaResponse.done_reason === "length") {
stopReason = "max_tokens";
}
return {
id: `msg_ollama_${Date.now()}`,
type: "message",
role: "assistant",
content: content,
model: ollamaResponse.model,
stop_reason: stopReason,
usage: {
input_tokens: ollamaResponse.prompt_eval_count || 0,
output_tokens: ollamaResponse.eval_count || 0
}
};
}
export default {
claudeToolToOllama,
claudeToolsToOllama,
ollamaToolCallToClaude,
claudeToolResultToOllama,
claudeMessagesToOllama,
ollamaResponseToClaude
};