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>
This commit is contained in:
parent
5891dfd6b7
commit
74a2d179b9
@ -1,5 +1,6 @@
|
||||
import { Router } from "express";
|
||||
import claudeRouter from "./claude.js";
|
||||
import ollamaRouter from "./ollama.js";
|
||||
import sessionRouter from "./session.js";
|
||||
|
||||
const router = Router();
|
||||
@ -10,6 +11,9 @@ router.use("/session", sessionRouter);
|
||||
// Claude API proxy
|
||||
router.use("/claude", claudeRouter);
|
||||
|
||||
// Ollama API proxy
|
||||
router.use("/ollama", ollamaRouter);
|
||||
|
||||
// Health check
|
||||
router.get("/health", (req, res) => {
|
||||
res.json({ status: "ok" });
|
||||
|
||||
178
server/api/ollama.js
Normal file
178
server/api/ollama.js
Normal file
@ -0,0 +1,178 @@
|
||||
import { Router } from "express";
|
||||
import { getSession, addMessage, getConversationForAPI } from "../services/sessionStore.js";
|
||||
import { getOllamaUrl } from "../services/providerConfig.js";
|
||||
import {
|
||||
claudeToolsToOllama,
|
||||
claudeMessagesToOllama,
|
||||
ollamaResponseToClaude
|
||||
} from "../services/toolConverter.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* Build entity context string for the system prompt
|
||||
*/
|
||||
function buildEntityContext(entities) {
|
||||
if (!entities || entities.length === 0) {
|
||||
return "\n\nThe diagram is currently empty.";
|
||||
}
|
||||
|
||||
const entityList = entities.map(e => {
|
||||
const shape = e.template?.replace('#', '').replace('-template', '') || 'unknown';
|
||||
const pos = e.position || { x: 0, y: 0, z: 0 };
|
||||
return `- ${e.text || '(no label)'} (${shape}, ${e.color || 'unknown'}) at (${pos.x?.toFixed(1)}, ${pos.y?.toFixed(1)}, ${pos.z?.toFixed(1)})`;
|
||||
}).join('\n');
|
||||
|
||||
return `\n\n## Current Diagram State\nThe diagram currently contains ${entities.length} entities:\n${entityList}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Ollama chat requests
|
||||
* Accepts Claude-format requests and converts them to Ollama format
|
||||
*/
|
||||
router.post("/*path", async (req, res) => {
|
||||
const requestStart = Date.now();
|
||||
console.log(`[Ollama API] ========== REQUEST START ==========`);
|
||||
|
||||
const ollamaUrl = getOllamaUrl();
|
||||
console.log(`[Ollama API] Using Ollama at: ${ollamaUrl}`);
|
||||
|
||||
// Extract request body (Claude format)
|
||||
const { sessionId, model, max_tokens, system, tools, messages } = req.body;
|
||||
|
||||
console.log(`[Ollama API] Session ID: ${sessionId || 'none'}`);
|
||||
console.log(`[Ollama API] Model: ${model}`);
|
||||
console.log(`[Ollama API] Messages count: ${messages?.length || 0}`);
|
||||
|
||||
// Build system prompt with entity context
|
||||
let systemPrompt = system || '';
|
||||
|
||||
if (sessionId) {
|
||||
const session = getSession(sessionId);
|
||||
if (session) {
|
||||
console.log(`[Ollama API] Session found: ${session.entities.length} entities, ${session.conversationHistory.length} messages in history`);
|
||||
|
||||
// Inject entity context into system prompt
|
||||
const entityContext = buildEntityContext(session.entities);
|
||||
console.log(`[Ollama API] Entity context added (${entityContext.length} chars)`);
|
||||
systemPrompt += entityContext;
|
||||
|
||||
// Get conversation history and merge with current messages
|
||||
const historyMessages = getConversationForAPI(sessionId);
|
||||
if (historyMessages.length > 0 && messages) {
|
||||
const currentContent = messages[messages.length - 1]?.content;
|
||||
const filteredHistory = historyMessages.filter(msg => msg.content !== currentContent);
|
||||
messages.unshift(...filteredHistory);
|
||||
console.log(`[Ollama API] Merged ${filteredHistory.length} history messages`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[Ollama API] WARNING: Session ${sessionId} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to Ollama format
|
||||
const ollamaMessages = claudeMessagesToOllama(messages || [], systemPrompt);
|
||||
const ollamaTools = claudeToolsToOllama(tools);
|
||||
|
||||
const ollamaRequest = {
|
||||
model: model,
|
||||
messages: ollamaMessages,
|
||||
stream: false,
|
||||
options: {
|
||||
num_predict: max_tokens || 1024
|
||||
}
|
||||
};
|
||||
|
||||
// Only add tools if there are any
|
||||
if (ollamaTools.length > 0) {
|
||||
ollamaRequest.tools = ollamaTools;
|
||||
}
|
||||
|
||||
console.log(`[Ollama API] Converted to Ollama format: ${ollamaMessages.length} messages, ${ollamaTools.length} tools`);
|
||||
|
||||
try {
|
||||
console.log(`[Ollama API] Sending request to Ollama...`);
|
||||
const fetchStart = Date.now();
|
||||
|
||||
const response = await fetch(`${ollamaUrl}/api/chat`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(ollamaRequest)
|
||||
});
|
||||
|
||||
const fetchDuration = Date.now() - fetchStart;
|
||||
console.log(`[Ollama API] Response received in ${fetchDuration}ms, status: ${response.status}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`[Ollama API] Error response:`, errorText);
|
||||
return res.status(response.status).json({
|
||||
error: `Ollama API error: ${response.status}`,
|
||||
details: errorText
|
||||
});
|
||||
}
|
||||
|
||||
const ollamaData = await response.json();
|
||||
console.log(`[Ollama API] Response parsed. Done: ${ollamaData.done}, model: ${ollamaData.model}`);
|
||||
|
||||
// Convert response back to Claude format
|
||||
const claudeResponse = ollamaResponseToClaude(ollamaData);
|
||||
console.log(`[Ollama API] Converted to Claude format. Stop reason: ${claudeResponse.stop_reason}, content blocks: ${claudeResponse.content.length}`);
|
||||
|
||||
// Store messages to session if applicable
|
||||
if (sessionId && claudeResponse.content) {
|
||||
const session = getSession(sessionId);
|
||||
if (session) {
|
||||
// Store the user message if it was new
|
||||
const userMessage = messages?.[messages.length - 1];
|
||||
if (userMessage && userMessage.role === 'user' && typeof userMessage.content === 'string') {
|
||||
addMessage(sessionId, {
|
||||
role: 'user',
|
||||
content: userMessage.content
|
||||
});
|
||||
console.log(`[Ollama API] Stored user message to session`);
|
||||
}
|
||||
|
||||
// Store the assistant response (text only)
|
||||
const assistantContent = claudeResponse.content
|
||||
.filter(c => c.type === 'text')
|
||||
.map(c => c.text)
|
||||
.join('\n');
|
||||
|
||||
if (assistantContent) {
|
||||
addMessage(sessionId, {
|
||||
role: 'assistant',
|
||||
content: assistantContent
|
||||
});
|
||||
console.log(`[Ollama API] Stored assistant response to session (${assistantContent.length} chars)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalDuration = Date.now() - requestStart;
|
||||
console.log(`[Ollama API] ========== REQUEST COMPLETE (${totalDuration}ms) ==========`);
|
||||
res.json(claudeResponse);
|
||||
|
||||
} catch (error) {
|
||||
const totalDuration = Date.now() - requestStart;
|
||||
console.error(`[Ollama API] ========== REQUEST FAILED (${totalDuration}ms) ==========`);
|
||||
console.error(`[Ollama API] Error:`, error);
|
||||
|
||||
// Check if it's a connection error
|
||||
if (error.cause?.code === 'ECONNREFUSED') {
|
||||
return res.status(503).json({
|
||||
error: "Ollama is not running",
|
||||
details: `Could not connect to Ollama at ${ollamaUrl}. Make sure Ollama is installed and running.`
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: "Failed to proxy request to Ollama",
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
97
server/services/providerConfig.js
Normal file
97
server/services/providerConfig.js
Normal file
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* AI Provider Configuration
|
||||
* Manages configuration for different AI providers (Claude, Ollama)
|
||||
*/
|
||||
|
||||
// Default configuration
|
||||
const DEFAULT_PROVIDER = 'claude';
|
||||
// Use 127.0.0.1 instead of localhost to avoid IPv6 resolution issues
|
||||
const DEFAULT_OLLAMA_URL = 'http://127.0.0.1:11434';
|
||||
|
||||
/**
|
||||
* Get the current AI provider
|
||||
* @returns {string} Provider name ('claude' or 'ollama')
|
||||
*/
|
||||
export function getProvider() {
|
||||
return process.env.AI_PROVIDER || DEFAULT_PROVIDER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Ollama API URL
|
||||
* @returns {string} Ollama base URL
|
||||
*/
|
||||
export function getOllamaUrl() {
|
||||
return process.env.OLLAMA_URL || DEFAULT_OLLAMA_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Anthropic API URL
|
||||
* @returns {string} Anthropic base URL
|
||||
*/
|
||||
export function getAnthropicUrl() {
|
||||
return process.env.ANTHROPIC_API_URL || 'https://api.anthropic.com';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider configuration for a specific provider
|
||||
* @param {string} provider - Provider name
|
||||
* @returns {object} Provider configuration
|
||||
*/
|
||||
export function getProviderConfig(provider) {
|
||||
switch (provider) {
|
||||
case 'ollama':
|
||||
return {
|
||||
name: 'ollama',
|
||||
baseUrl: getOllamaUrl(),
|
||||
chatEndpoint: '/api/chat',
|
||||
requiresAuth: false
|
||||
};
|
||||
case 'claude':
|
||||
default:
|
||||
return {
|
||||
name: 'claude',
|
||||
baseUrl: getAnthropicUrl(),
|
||||
chatEndpoint: '/v1/messages',
|
||||
requiresAuth: true,
|
||||
apiKey: process.env.ANTHROPIC_API_KEY
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine provider from model ID
|
||||
* @param {string} modelId - Model identifier
|
||||
* @returns {string} Provider name
|
||||
*/
|
||||
export function getProviderFromModel(modelId) {
|
||||
if (!modelId) return getProvider();
|
||||
|
||||
// Claude models start with 'claude-'
|
||||
if (modelId.startsWith('claude-')) {
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
// Known Ollama models
|
||||
const ollamaModels = [
|
||||
'llama', 'mistral', 'qwen', 'codellama', 'phi',
|
||||
'gemma', 'neural-chat', 'starling', 'orca', 'vicuna',
|
||||
'deepseek', 'dolphin', 'nous-hermes', 'openhermes'
|
||||
];
|
||||
|
||||
for (const prefix of ollamaModels) {
|
||||
if (modelId.toLowerCase().startsWith(prefix)) {
|
||||
return 'ollama';
|
||||
}
|
||||
}
|
||||
|
||||
// Default to configured provider
|
||||
return getProvider();
|
||||
}
|
||||
|
||||
export default {
|
||||
getProvider,
|
||||
getOllamaUrl,
|
||||
getAnthropicUrl,
|
||||
getProviderConfig,
|
||||
getProviderFromModel
|
||||
};
|
||||
245
server/services/toolConverter.js
Normal file
245
server/services/toolConverter.js
Normal file
@ -0,0 +1,245 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
@ -166,6 +166,48 @@ export class DiagramManager {
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('chatModifyConnection', (event: CustomEvent) => {
|
||||
const {target, updates} = event.detail;
|
||||
this._logger.debug('chatModifyConnection', target, updates);
|
||||
|
||||
let connection: DiagramEntity | undefined;
|
||||
|
||||
// Check if target is a connection:fromId:toId format
|
||||
if (target.startsWith('connection:')) {
|
||||
const parts = target.split(':');
|
||||
if (parts.length === 3) {
|
||||
const fromId = parts[1];
|
||||
const toId = parts[2];
|
||||
// Find connection by from/to
|
||||
connection = Array.from(this._diagramObjects.values())
|
||||
.map(obj => obj.diagramEntity)
|
||||
.find(e => e.template === '#connection-template' && e.from === fromId && e.to === toId);
|
||||
}
|
||||
} else {
|
||||
// Find by label (text)
|
||||
connection = this.findEntityByIdOrLabel(target);
|
||||
// Verify it's a connection
|
||||
if (connection && connection.template !== '#connection-template') {
|
||||
this._logger.warn('chatModifyConnection: found entity is not a connection:', target);
|
||||
connection = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (connection) {
|
||||
const diagramObject = this._diagramObjects.get(connection.id);
|
||||
if (diagramObject) {
|
||||
if (updates.text !== undefined) {
|
||||
diagramObject.text = updates.text;
|
||||
}
|
||||
if (updates.color !== undefined) {
|
||||
diagramObject.color = updates.color;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._logger.warn('chatModifyConnection: connection not found:', target);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('chatListEntities', () => {
|
||||
this._logger.debug('chatListEntities');
|
||||
const entities = Array.from(this._diagramObjects.values()).map(obj => ({
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {ChatMessage, CreateSessionResponse, DiagramSession, DiagramToolCall, SessionEntity, SyncEntitiesResponse, ToolResult} from "../types/chatTypes";
|
||||
import {clearDiagram, connectEntities, createEntity, getCameraPosition, listEntities, modifyEntity, removeEntity} from "./entityBridge";
|
||||
import {clearDiagram, connectEntities, createEntity, getCameraPosition, listEntities, modifyConnection, modifyEntity, removeEntity} from "./entityBridge";
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
import log from 'loglevel';
|
||||
|
||||
@ -10,32 +10,69 @@ logger.setLevel('debug');
|
||||
let currentSessionId: string | null = null;
|
||||
|
||||
// Model management
|
||||
export type AIProvider = 'claude' | 'ollama';
|
||||
|
||||
export interface ModelInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
provider: AIProvider;
|
||||
}
|
||||
|
||||
const AVAILABLE_MODELS: ModelInfo[] = [
|
||||
// Claude models
|
||||
{
|
||||
id: 'claude-sonnet-4-20250514',
|
||||
name: 'Claude Sonnet 4',
|
||||
description: 'Balanced performance and speed (default)'
|
||||
description: 'Balanced performance and speed (default)',
|
||||
provider: 'claude'
|
||||
},
|
||||
{
|
||||
id: 'claude-opus-4-20250514',
|
||||
name: 'Claude Opus 4',
|
||||
description: 'Most capable, best for complex tasks'
|
||||
description: 'Most capable, best for complex tasks',
|
||||
provider: 'claude'
|
||||
},
|
||||
{
|
||||
id: 'claude-haiku-3-5-20241022',
|
||||
name: 'Claude Haiku 3.5',
|
||||
description: 'Fastest responses, good for simple tasks'
|
||||
description: 'Fastest responses, good for simple tasks',
|
||||
provider: 'claude'
|
||||
},
|
||||
// Ollama models (local)
|
||||
{
|
||||
id: 'llama3.1',
|
||||
name: 'Llama 3.1',
|
||||
description: 'Local model with function calling support',
|
||||
provider: 'ollama'
|
||||
},
|
||||
{
|
||||
id: 'mistral',
|
||||
name: 'Mistral',
|
||||
description: 'Fast local model with good tool support',
|
||||
provider: 'ollama'
|
||||
},
|
||||
{
|
||||
id: 'qwen2.5',
|
||||
name: 'Qwen 2.5',
|
||||
description: 'Capable local model with function calling',
|
||||
provider: 'ollama'
|
||||
}
|
||||
];
|
||||
|
||||
let currentModelId: string = 'claude-sonnet-4-20250514';
|
||||
|
||||
/**
|
||||
* Get the API endpoint for the current model's provider
|
||||
*/
|
||||
function getApiEndpoint(): string {
|
||||
const model = getCurrentModel();
|
||||
if (model.provider === 'ollama') {
|
||||
return '/api/ollama/v1/messages';
|
||||
}
|
||||
return '/api/claude/v1/messages';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available models
|
||||
*/
|
||||
@ -259,7 +296,7 @@ const TOOLS = [
|
||||
},
|
||||
label: {
|
||||
type: "string",
|
||||
description: "New label text"
|
||||
description: "New label text. Use empty string \"\" to remove the label."
|
||||
},
|
||||
position: {
|
||||
type: "object",
|
||||
@ -273,6 +310,35 @@ const TOOLS = [
|
||||
required: ["target"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "modify_connection",
|
||||
description: "Modify a connection's label or color. Connections can be identified by their label (e.g., 'Server to Database') or by specifying the from/to entities.",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
target: {
|
||||
type: "string",
|
||||
description: "Label of the connection to modify, or use from/to to identify it"
|
||||
},
|
||||
from: {
|
||||
type: "string",
|
||||
description: "ID or label of the source entity (alternative to target)"
|
||||
},
|
||||
to: {
|
||||
type: "string",
|
||||
description: "ID or label of the destination entity (alternative to target)"
|
||||
},
|
||||
label: {
|
||||
type: "string",
|
||||
description: "New label text for the connection. Use empty string \"\" to remove the label."
|
||||
},
|
||||
color: {
|
||||
type: "string",
|
||||
description: "New color for the connection"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "clear_diagram",
|
||||
description: "DESTRUCTIVE: Permanently delete ALL entities from the diagram and clear the session. This cannot be undone. IMPORTANT: Before calling this tool, you MUST first ask the user to confirm by saying something like 'Are you sure you want to clear the entire diagram? This will permanently delete all entities and cannot be undone.' Only call this tool with confirmed=true AFTER the user explicitly confirms (e.g., says 'yes', 'confirm', 'do it', etc.).",
|
||||
@ -319,7 +385,7 @@ const TOOLS = [
|
||||
properties: {
|
||||
model_id: {
|
||||
type: "string",
|
||||
description: "The model ID to switch to (e.g., 'claude-sonnet-4-20250514', 'claude-opus-4-20250514', 'claude-haiku-3-5-20241022')"
|
||||
description: "The model ID to switch to. Claude models: 'claude-sonnet-4-20250514', 'claude-opus-4-20250514', 'claude-haiku-3-5-20241022'. Ollama models (local): 'llama3.1', 'mistral', 'qwen2.5'"
|
||||
}
|
||||
},
|
||||
required: ["model_id"]
|
||||
@ -364,6 +430,9 @@ async function executeToolCall(toolCall: DiagramToolCall): Promise<ToolResult> {
|
||||
case 'modify_entity':
|
||||
result = modifyEntity(toolCall.input);
|
||||
break;
|
||||
case 'modify_connection':
|
||||
result = await modifyConnection(toolCall.input);
|
||||
break;
|
||||
case 'list_entities':
|
||||
result = await listEntities();
|
||||
break;
|
||||
@ -477,7 +546,10 @@ export async function sendMessage(
|
||||
|
||||
logger.debug('[sendMessage] Request body:', JSON.stringify(requestBody, null, 2).substring(0, 500) + '...');
|
||||
|
||||
const response = await fetch('/api/claude/v1/messages', {
|
||||
const apiEndpoint = getApiEndpoint();
|
||||
logger.debug('[sendMessage] Using API endpoint:', apiEndpoint);
|
||||
|
||||
const response = await fetch(apiEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
COLOR_NAME_TO_HEX,
|
||||
ConnectEntitiesParams,
|
||||
CreateEntityParams,
|
||||
ModifyConnectionParams,
|
||||
ModifyEntityParams,
|
||||
RemoveEntityParams,
|
||||
SHAPE_TO_TEMPLATE,
|
||||
@ -196,6 +197,71 @@ export function modifyEntity(params: ModifyEntityParams): ToolResult {
|
||||
};
|
||||
}
|
||||
|
||||
export async function modifyConnection(params: ModifyConnectionParams): Promise<ToolResult> {
|
||||
logger.debug('[modifyConnection] Modifying connection:', params);
|
||||
|
||||
// Determine how to find the connection
|
||||
let connectionTarget: string | null = null;
|
||||
|
||||
if (params.target) {
|
||||
// Direct target specified (connection label)
|
||||
connectionTarget = params.target;
|
||||
} else if (params.from && params.to) {
|
||||
// Find by from/to entities
|
||||
const fromEntity = await resolveEntity(params.from);
|
||||
const toEntity = await resolveEntity(params.to);
|
||||
|
||||
if (!fromEntity.id) {
|
||||
return {
|
||||
toolName: 'modify_connection',
|
||||
success: false,
|
||||
message: `Could not find source entity "${params.from}"`
|
||||
};
|
||||
}
|
||||
|
||||
if (!toEntity.id) {
|
||||
return {
|
||||
toolName: 'modify_connection',
|
||||
success: false,
|
||||
message: `Could not find destination entity "${params.to}"`
|
||||
};
|
||||
}
|
||||
|
||||
// Use a special format to identify connection by from/to
|
||||
connectionTarget = `connection:${fromEntity.id}:${toEntity.id}`;
|
||||
} else {
|
||||
return {
|
||||
toolName: 'modify_connection',
|
||||
success: false,
|
||||
message: 'Must specify either "target" (connection label) or both "from" and "to" entities'
|
||||
};
|
||||
}
|
||||
|
||||
const updates: Partial<DiagramEntity> = {};
|
||||
|
||||
if (params.color) {
|
||||
updates.color = resolveColor(params.color);
|
||||
}
|
||||
if (params.label !== undefined) {
|
||||
updates.text = params.label;
|
||||
}
|
||||
|
||||
const event = new CustomEvent('chatModifyConnection', {
|
||||
detail: {target: connectionTarget, updates},
|
||||
bubbles: true
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
|
||||
const labelInfo = params.label ? ` with label "${params.label}"` : '';
|
||||
const colorInfo = params.color ? ` with color "${params.color}"` : '';
|
||||
|
||||
return {
|
||||
toolName: 'modify_connection',
|
||||
success: true,
|
||||
message: `Modified connection${labelInfo}${colorInfo}`
|
||||
};
|
||||
}
|
||||
|
||||
export function listEntities(): Promise<ToolResult> {
|
||||
logger.debug('[listEntities] Listing entities...');
|
||||
return new Promise((resolve) => {
|
||||
|
||||
@ -46,6 +46,14 @@ export interface ClearDiagramParams {
|
||||
confirmed: boolean;
|
||||
}
|
||||
|
||||
export interface ModifyConnectionParams {
|
||||
target?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
label?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface SetModelParams {
|
||||
model_id: string;
|
||||
}
|
||||
@ -55,6 +63,7 @@ export type DiagramToolCall =
|
||||
| { name: 'connect_entities'; input: ConnectEntitiesParams }
|
||||
| { name: 'remove_entity'; input: RemoveEntityParams }
|
||||
| { name: 'modify_entity'; input: ModifyEntityParams }
|
||||
| { name: 'modify_connection'; input: ModifyConnectionParams }
|
||||
| { name: 'list_entities'; input: Record<string, never> }
|
||||
| { name: 'clear_diagram'; input: ClearDiagramParams }
|
||||
| { name: 'get_camera_position'; input: Record<string, never> }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user