diff --git a/chat2db-client/src/blocks/Setting/AiSetting/aiTypeConfig.ts b/chat2db-client/src/blocks/Setting/AiSetting/aiTypeConfig.ts index 8867614b1..559dc3cca 100644 --- a/chat2db-client/src/blocks/Setting/AiSetting/aiTypeConfig.ts +++ b/chat2db-client/src/blocks/Setting/AiSetting/aiTypeConfig.ts @@ -15,6 +15,7 @@ const AITypeName = { [AIType.OPENAI]: 'Open AI', [AIType.AZUREAI]: 'Azure AI', [AIType.RESTAI]: i18n('setting.tab.custom'), + [AIType.OLLAMAAI]: i18n('setting.tab.aiType.ollama'), }; const AIFormConfig: Record = { @@ -34,29 +35,33 @@ const AIFormConfig: Record = { }, [AIType.WENXINAI]: { apiKey: true, - apiHost: true, + apiHost: 'https://api.weixin.qq.com', }, [AIType.TONGYIQIANWENAI]: { apiKey: true, - apiHost: true, - model: true, + apiHost: 'https://dashscope.aliyuncs.com/api/v1', + model: 'qwen-turbo', }, [AIType.OPENAI]: { apiKey: true, apiHost: 'https://api.openai.com/', - httpProxyHost: true, - httpProxyPort: true, + httpProxyHost: '127.0.0.1', + httpProxyPort: '8080', // model: 'gpt-3.5-turbo', }, [AIType.AZUREAI]: { apiKey: true, - apiHost: true, - model: true, + apiHost: 'https://your-resource.openai.azure.com', + model: 'gpt-35-turbo', }, [AIType.RESTAI]: { apiKey: true, - apiHost: true, - model: true, + apiHost: 'https://api.openai.com/v1', + model: 'gpt-3.5-turbo', + }, + [AIType.OLLAMAAI]: { + ollamaApiHost: 'http://localhost:11434', + ollamaModel: 'qwen2.5-coder', }, }; diff --git a/chat2db-client/src/blocks/Setting/AiSetting/index.tsx b/chat2db-client/src/blocks/Setting/AiSetting/index.tsx index 54e397ea7..20042d783 100644 --- a/chat2db-client/src/blocks/Setting/AiSetting/index.tsx +++ b/chat2db-client/src/blocks/Setting/AiSetting/index.tsx @@ -45,23 +45,90 @@ export default function SettingAI(props: IProps) { const aiSqlSource = e.target.value; // 查询对应ai类型的配置 - const res = await configService.getAiSystemConfig({ - aiSqlSource, - }); - setAiConfig(res); + try { + const res = await configService.getAiSystemConfig({ + aiSqlSource, + }); + + // Special handling for Ollama AI - set defaults if no config found + if (aiSqlSource === AIType.OLLAMAAI) { + if (!res || !res.apiHost || !res.model) { + const ollamaConfig: IAiConfig = { + aiSqlSource: AIType.OLLAMAAI, + ollamaApiHost: res?.apiHost || 'http://localhost:11434', + ollamaModel: res?.model || 'qwen3-coder', + apiHost: res?.apiHost || 'http://localhost:11434', + model: res?.model || 'qwen3-coder', + }; + setAiConfig(ollamaConfig); + } else { + // Map server response to Ollama config + const ollamaConfig: IAiConfig = { + aiSqlSource: AIType.OLLAMAAI, + ollamaApiHost: res.apiHost, + ollamaModel: res.model, + apiHost: res.apiHost, + model: res.model, + }; + setAiConfig(ollamaConfig); + } + } else { + setAiConfig(res); + } + } catch (error) { + console.error('Failed to get AI config:', error); + // Set default config for other AI types + if (aiSqlSource === AIType.OLLAMAAI) { + setAiConfig({ + aiSqlSource: AIType.OLLAMAAI, + ollamaApiHost: 'http://localhost:11434', + ollamaModel: 'qwen3-coder', + }); + } else { + setAiConfig({ + aiSqlSource: aiSqlSource as AIType, + }); + } + } }; /** 应用Ai配置 */ const handleApplyAiConfig = () => { const newAiConfig = { ...aiConfig }; - /*if (newAiConfig.apiHost && !newAiConfig.apiHost?.endsWith('/')) { - newAiConfig.apiHost = newAiConfig.apiHost + '/'; - }*/ + if (aiConfig?.aiSqlSource === AIType.CHAT2DBAI) { newAiConfig.apiHost = `${window._appGatewayParams.baseUrl || 'http://test.sqlgpt.cn/gateway'}${'/model/'}`; } + + // Special handling for Ollama AI + if (aiConfig?.aiSqlSource === AIType.OLLAMAAI) { + // Ensure Ollama config has proper defaults + if (!newAiConfig.ollamaApiHost) { + newAiConfig.ollamaApiHost = 'http://localhost:11434'; + } + if (!newAiConfig.ollamaModel) { + newAiConfig.ollamaModel = 'deepseek-v3.1:671b-cloud'; + } + + // Map Ollama fields to standard AI config fields for server compatibility + newAiConfig.apiHost = newAiConfig.ollamaApiHost; + newAiConfig.model = newAiConfig.ollamaModel; + + // Create JSON content for server storage + const configContent = JSON.stringify({ + ollamaApiHost: newAiConfig.ollamaApiHost, + ollamaModel: newAiConfig.ollamaModel + }); + + // Update content for server storage + newAiConfig.content = configContent; + + // Debug logging + console.log('DEBUG: Ollama config being sent:', newAiConfig); + } if (props.handleApplyAiConfig) { + console.log('DEBUG: Calling handleApplyAiConfig with:', newAiConfig); props.handleApplyAiConfig(newAiConfig); } }; @@ -82,6 +149,48 @@ export default function SettingAI(props: IProps) { ); } + + // Special handling for Ollama AI + if (aiConfig?.aiSqlSource === AIType.OLLAMAAI) { + return ( + <> +
+ + { + setAiConfig({ ...aiConfig, ollamaApiHost: e.target.value }); + }} + /> + + + { + setAiConfig({ ...aiConfig, ollamaModel: e.target.value }); + }} + /> + +
+
+ +
+ + ); + } + return ( <>
@@ -95,7 +204,11 @@ export default function SettingAI(props: IProps) { { setAiConfig({ ...aiConfig, [key]: e.target.value }); }} diff --git a/chat2db-client/src/components/Loading/LoadingContent/index.tsx b/chat2db-client/src/components/Loading/LoadingContent/index.tsx index 55af793d7..598a30798 100644 --- a/chat2db-client/src/components/Loading/LoadingContent/index.tsx +++ b/chat2db-client/src/components/Loading/LoadingContent/index.tsx @@ -14,11 +14,11 @@ interface IProps extends React.HTMLAttributes { } export default function LoadingContent(props: IProps) { - const { children, className, data = true, handleEmpty = false, empty, isLoading, coverLoading, ...args } = props; + const { children, className, data = true, handleEmpty = false, empty, isLoading, coverLoading } = props; const isEmpty = !isLoading && handleEmpty && !(data as any)?.length; const renderContent = () => { - if ((isLoading || !data) && !coverLoading) { + if ((isLoading || data === null || data === undefined) && !coverLoading) { return ; } diff --git a/chat2db-client/src/i18n/en-us/setting.ts b/chat2db-client/src/i18n/en-us/setting.ts index 01479b18e..b850796e7 100644 --- a/chat2db-client/src/i18n/en-us/setting.ts +++ b/chat2db-client/src/i18n/en-us/setting.ts @@ -21,6 +21,7 @@ export default { 'setting.tab.aiType.baichuan': 'BaiChuan AI', 'setting.tab.aiType.wenxin': 'WenXin AI', 'setting.tab.aiType.tongyiqianwen': 'TongYiQianWen AI', + 'setting.tab.aiType.ollama': 'Ollama AI', 'setting.tab.aiType.custom.tips': 'The API format is consistent with the OpenAI API format', 'setting.label.serviceAddress': 'Service Address', 'setting.button.apply': 'Apply', diff --git a/chat2db-client/src/i18n/zh-cn/setting.ts b/chat2db-client/src/i18n/zh-cn/setting.ts index 121a0007b..fd0b91691 100644 --- a/chat2db-client/src/i18n/zh-cn/setting.ts +++ b/chat2db-client/src/i18n/zh-cn/setting.ts @@ -21,6 +21,7 @@ export default { 'setting.tab.aiType.baichuan': '百川', 'setting.tab.aiType.wenxin': '文心一言', 'setting.tab.aiType.tongyiqianwen': '通义千问', + 'setting.tab.aiType.ollama': 'Ollama AI', 'setting.tab.aiType.custom.tips': '接口格式与OpenAI接口格式一致', 'setting.label.serviceAddress': '服务地址', 'setting.button.apply': '应用', diff --git a/chat2db-client/src/pages/main/workspace/components/SaveList/index.tsx b/chat2db-client/src/pages/main/workspace/components/SaveList/index.tsx index 2a81406dc..6cfb9444e 100644 --- a/chat2db-client/src/pages/main/workspace/components/SaveList/index.tsx +++ b/chat2db-client/src/pages/main/workspace/components/SaveList/index.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import i18n from '@/i18n'; -import { Input, Dropdown, Modal } from 'antd'; +import { Input, Dropdown, Modal, Form } from 'antd'; import Iconfont from '@/components/Iconfont'; import LoadingContent from '@/components/Loading/LoadingContent'; import historyServer from '@/service/history'; @@ -20,6 +20,7 @@ const SaveList = () => { const saveBoxListRef = useRef(null); const consoleList = useWorkspaceStore((state) => state.savedConsoleList); const [editData, setEditData] = useState(null); + const [form] = Form.useForm(); useEffect(() => { getSavedConsoleList(); @@ -173,27 +174,28 @@ const SaveList = () => { title={i18n('common.text.rename')} open={!!editData} onOk={() => { - const params: any = { - id: editData.id, - name: editData.name, - }; - historyServer.updateSavedConsole(params).then(() => { - - getSavedConsoleList(); - setEditData(null); + form.validateFields().then((values) => { + const params: any = { + id: editData.id, + name: values.name, + }; + historyServer.updateSavedConsole(params).then(() => { + getSavedConsoleList(); + setEditData(null); + form.resetFields(); + }); }); }} - onCancel={() => setEditData(null)} + onCancel={() => { + setEditData(null); + form.resetFields(); + }} > - { - setEditData({ - ...editData, - name: e.target.value, - }); - }} - /> + + + + + ); diff --git a/chat2db-client/src/store/setting/index.ts b/chat2db-client/src/store/setting/index.ts index af6a36291..378943bcc 100644 --- a/chat2db-client/src/store/setting/index.ts +++ b/chat2db-client/src/store/setting/index.ts @@ -98,6 +98,3 @@ export const setHoldingService = (holdingService: boolean) => { useSettingStore.setState({ holdingService }); } - - - diff --git a/chat2db-client/src/typings/ai.ts b/chat2db-client/src/typings/ai.ts index da79e4b7f..195197bf0 100644 --- a/chat2db-client/src/typings/ai.ts +++ b/chat2db-client/src/typings/ai.ts @@ -7,6 +7,7 @@ export enum AIType { OPENAI = 'OPENAI', AZUREAI = 'AZUREAI', RESTAI = 'RESTAI', + OLLAMAAI = 'OLLAMAAI', } export interface IRemainingUse { diff --git a/chat2db-client/src/typings/setting.ts b/chat2db-client/src/typings/setting.ts index 3f20fd630..331878f55 100644 --- a/chat2db-client/src/typings/setting.ts +++ b/chat2db-client/src/typings/setting.ts @@ -9,4 +9,7 @@ export interface IAiConfig { stream?: boolean; secretKey?:string; model?: string; + ollamaApiHost?: string; + ollamaModel?: string; + content?: string; } diff --git a/chat2db-server/chat2db-server-domain/chat2db-server-domain-api/src/main/java/ai/chat2db/server/domain/api/enums/AiSqlSourceEnum.java b/chat2db-server/chat2db-server-domain/chat2db-server-domain-api/src/main/java/ai/chat2db/server/domain/api/enums/AiSqlSourceEnum.java index 6e8fd3ef7..3b61f078e 100644 --- a/chat2db-server/chat2db-server-domain/chat2db-server-domain-api/src/main/java/ai/chat2db/server/domain/api/enums/AiSqlSourceEnum.java +++ b/chat2db-server/chat2db-server-domain/chat2db-server-domain-api/src/main/java/ai/chat2db/server/domain/api/enums/AiSqlSourceEnum.java @@ -62,6 +62,11 @@ public enum AiSqlSourceEnum implements BaseEnum { */ FASTCHATAI("FAST CHAT AI"), + /** + * OLLAMA AI + */ + OLLAMAAI("OLLAMA AI"), + ; final String description; @@ -91,4 +96,9 @@ public String getCode() { return this.name(); } + @Override + public String getDescription() { + return this.description; + } + } diff --git a/chat2db-server/chat2db-server-domain/chat2db-server-domain-api/src/main/java/ai/chat2db/server/domain/api/model/ChatGptConfig.java b/chat2db-server/chat2db-server-domain/chat2db-server-domain-api/src/main/java/ai/chat2db/server/domain/api/model/ChatGptConfig.java index 553dcb016..2c9d70dba 100644 --- a/chat2db-server/chat2db-server-domain/chat2db-server-domain-api/src/main/java/ai/chat2db/server/domain/api/model/ChatGptConfig.java +++ b/chat2db-server/chat2db-server-domain/chat2db-server-domain-api/src/main/java/ai/chat2db/server/domain/api/model/ChatGptConfig.java @@ -71,4 +71,14 @@ public class ChatGptConfig { * deploymentId of the deployed model, default gpt-3.5-turbo */ private String azureDeploymentId; + + /** + * Ollama API endpoint, default http://localhost:11434 + */ + private String ollamaApiHost; + + /** + * Ollama model name, e.g., qwen3-coder, deepseek-v3.1 + */ + private String ollamaModel; } diff --git a/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/ChatController.java b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/ChatController.java index 8010a1258..2ac873ff4 100644 --- a/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/ChatController.java +++ b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/ChatController.java @@ -37,6 +37,7 @@ import ai.chat2db.server.web.api.controller.ai.openai.listener.OpenAIEventSourceListener; import ai.chat2db.server.web.api.controller.ai.request.ChatQueryRequest; import ai.chat2db.server.web.api.controller.ai.request.ChatRequest; +import ai.chat2db.server.web.api.controller.ai.response.ChatCompletionResponse; import ai.chat2db.server.web.api.controller.ai.rest.client.RestAIClient; import ai.chat2db.server.web.api.controller.ai.rest.listener.RestAIEventSourceListener; import ai.chat2db.server.web.api.controller.ai.tongyi.client.TongyiChatAIClient; @@ -45,6 +46,7 @@ import ai.chat2db.server.web.api.controller.ai.wenxin.listener.WenxinAIEventSourceListener; import ai.chat2db.server.web.api.controller.ai.zhipu.client.ZhipuChatAIClient; import ai.chat2db.server.web.api.controller.ai.zhipu.listener.ZhipuChatAIEventSourceListener; +import ai.chat2db.server.web.api.controller.ai.ollama.client.OllamaAIClient; import ai.chat2db.server.web.api.http.GatewayClientService; import ai.chat2db.server.web.api.http.model.EsTableSchema; import ai.chat2db.server.web.api.http.model.TableSchema; @@ -250,6 +252,8 @@ public SseEmitter distributeAISql(ChatQueryRequest queryRequest, SseEmitter sseE return chatWithTongyiChatAi(queryRequest, sseEmitter, uid); case ZHIPUAI: return chatWithZhipuChatAi(queryRequest, sseEmitter, uid); + case OLLAMAAI: + return chatWithOllamaAi(queryRequest, sseEmitter, uid); } return chatWithOpenAi(queryRequest, sseEmitter, uid); } @@ -819,4 +823,90 @@ private FastChatEmbeddingResponse embeddingWithChat2dbAi(String input) { return embeddings; } + /** + * Chat with Ollama AI + * + * @param queryRequest + * @param sseEmitter + * @param uid + * @return + */ + private SseEmitter chatWithOllamaAi(ChatQueryRequest queryRequest, SseEmitter sseEmitter, String uid) { + try { + ConfigService configService = ApplicationContextUtil.getBean(ConfigService.class); + Config config = configService.find(AiSqlSourceEnum.OLLAMAAI.getCode()).getData(); + + // Default Ollama settings + String ollamaApiHost = "http://localhost:11434"; + String ollamaModel = "qwen2.5-coder"; + + // Parse config content if available + if (config != null && config.getContent() != null && !config.getContent().isEmpty()) { + try { + // Parse JSON content for Ollama settings + String content = config.getContent(); + if (content.contains("ollamaApiHost")) { + // Simple parsing for ollamaApiHost + String[] parts = content.split("\"ollamaApiHost\":\""); + if (parts.length > 1) { + String hostPart = parts[1].split("\"")[0]; + if (!hostPart.isEmpty()) { + ollamaApiHost = hostPart; + } + } + } + if (content.contains("ollamaModel")) { + // Simple parsing for ollamaModel + String[] parts = content.split("\"ollamaModel\":\""); + if (parts.length > 1) { + String modelPart = parts[1].split("\"")[0]; + if (!modelPart.isEmpty()) { + ollamaModel = modelPart; + } + } + } + } catch (Exception e) { + log.warn("Failed to parse Ollama config, using defaults", e); + } + } + + log.info("Using Ollama config - Host: {}, Model: {}", ollamaApiHost, ollamaModel); + + OllamaAIClient ollamaClient = new OllamaAIClient(ollamaApiHost, ollamaModel); + + // Test connection first + if (!ollamaClient.testConnection()) { + throw new RuntimeException("Cannot connect to Ollama at " + ollamaApiHost); + } + + // Convert ChatQueryRequest to ChatRequest + ChatRequest chatRequest = new ChatRequest(); + chatRequest.setPrompt(queryRequest.getMessage()); + + // Get response from Ollama + ChatCompletionResponse response = ollamaClient.chatCompletion(chatRequest); + + // Send response through SSE + if (response != null && response.getChoices() != null && !response.getChoices().isEmpty()) { + String content = response.getChoices().get(0).getMessage().getContent(); + sseEmitter.send(SseEmitter.event().data(content)); + } else { + sseEmitter.send(SseEmitter.event().data("No response from Ollama")); + } + + sseEmitter.complete(); + return sseEmitter; + + } catch (Exception e) { + log.error("Error chatting with Ollama AI", e); + try { + sseEmitter.send(SseEmitter.event().data("Error: " + e.getMessage())); + sseEmitter.complete(); + } catch (Exception ex) { + log.error("Error sending error response", ex); + } + return sseEmitter; + } + } + } diff --git a/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/ollama/client/OllamaAIClient.java b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/ollama/client/OllamaAIClient.java new file mode 100644 index 000000000..4e0a2764c --- /dev/null +++ b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/ollama/client/OllamaAIClient.java @@ -0,0 +1,205 @@ +package ai.chat2db.server.web.api.controller.ai.ollama.client; + +import ai.chat2db.server.web.api.controller.ai.request.ChatRequest; +import ai.chat2db.server.web.api.controller.ai.response.ChatCompletionResponse; +import ai.chat2db.server.web.api.controller.ai.response.ChatChoice; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.unfbx.chatgpt.entity.chat.Message; +import com.unfbx.chatgpt.entity.common.Usage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Ollama AI Client for local AI models + * + * @author Chat2DB Team + */ +@Slf4j +public class OllamaAIClient { + + private static final String DEFAULT_OLLAMA_HOST = "http://localhost:11434"; + private static final String CHAT_ENDPOINT = "/api/chat"; + + private final RestTemplate restTemplate; + private final String ollamaApiHost; + private final String model; + private final ObjectMapper objectMapper; + + public OllamaAIClient(String ollamaApiHost, String model) { + this.restTemplate = new RestTemplate(); + this.ollamaApiHost = ollamaApiHost != null ? ollamaApiHost : DEFAULT_OLLAMA_HOST; + this.model = model; + this.objectMapper = new ObjectMapper(); + } + + /** + * Send chat request to Ollama + */ + public ChatCompletionResponse chatCompletion(ChatRequest request) { + try { + String url = ollamaApiHost + CHAT_ENDPOINT; + + // Build Ollama request + Map ollamaRequest = buildOllamaRequest(request); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> entity = new HttpEntity<>(ollamaRequest, headers); + + log.info("Sending request to Ollama: {} with model: {}", url, model); + + ResponseEntity response = restTemplate.postForEntity(url, entity, String.class); + + return parseOllamaResponse(response.getBody()); + + } catch (Exception e) { + log.error("Error calling Ollama API", e); + return createErrorResponse(e.getMessage()); + } + } + + /** + * Build Ollama request from ChatRequest + */ + private Map buildOllamaRequest(ChatRequest request) { + Map ollamaRequest = new HashMap<>(); + + // Model + ollamaRequest.put("model", model); + + // Messages + List> messages = new ArrayList<>(); + if (request.getPrompt() != null) { + Map message = new HashMap<>(); + message.put("role", "user"); + message.put("content", request.getPrompt()); + messages.add(message); + } + ollamaRequest.put("messages", messages); + + // Stream (disable for now) + ollamaRequest.put("stream", false); + + // Options + Map options = new HashMap<>(); + options.put("temperature", 0.7); + options.put("top_p", 0.9); + ollamaRequest.put("options", options); + + return ollamaRequest; + } + + /** + * Parse Ollama response to ChatCompletionResponse + */ + private ChatCompletionResponse parseOllamaResponse(String responseBody) { + try { + JsonNode rootNode = objectMapper.readTree(responseBody); + + ChatCompletionResponse response = new ChatCompletionResponse(); + + // Extract message content + if (rootNode.has("message") && rootNode.get("message").has("content")) { + String content = rootNode.get("message").get("content").asText(); + + // Create choice + ChatChoice choice = new ChatChoice(); + choice.setMessage(new Message()); + choice.getMessage().setContent(content); + choice.setIndex(0); + choice.setFinishReason("stop"); + + List choices = new ArrayList<>(); + choices.add(choice); + response.setChoices(choices); + } + + // Set usage info if available + if (rootNode.has("prompt_eval_count") || rootNode.has("eval_count")) { + Usage usage = new Usage(); + if (rootNode.has("prompt_eval_count")) { + usage.setPromptTokens(rootNode.get("prompt_eval_count").asInt()); + } + if (rootNode.has("eval_count")) { + usage.setCompletionTokens(rootNode.get("eval_count").asInt()); + } + usage.setTotalTokens(usage.getPromptTokens() + usage.getCompletionTokens()); + response.setUsage(usage); + } + + return response; + + } catch (Exception e) { + log.error("Error parsing Ollama response", e); + return createErrorResponse("Failed to parse response: " + e.getMessage()); + } + } + + /** + * Create error response + */ + private ChatCompletionResponse createErrorResponse(String errorMessage) { + ChatCompletionResponse response = new ChatCompletionResponse(); + // Create an empty choice with error info + List choices = new ArrayList<>(); + ChatChoice choice = new ChatChoice(); + choice.setMessage(new Message()); + choice.getMessage().setContent("Error: " + errorMessage); + choice.setFinishReason("error"); + choices.add(choice); + response.setChoices(choices); + return response; + } + + /** + * Test Ollama connection + */ + public boolean testConnection() { + try { + String url = ollamaApiHost + "/api/tags"; + ResponseEntity response = restTemplate.getForEntity(url, String.class); + return response.getStatusCode().is2xxSuccessful(); + } catch (Exception e) { + log.error("Ollama connection test failed", e); + return false; + } + } + + /** + * Get available models + */ + public List getAvailableModels() { + try { + String url = ollamaApiHost + "/api/tags"; + ResponseEntity response = restTemplate.getForEntity(url, String.class); + + JsonNode rootNode = objectMapper.readTree(response.getBody()); + List models = new ArrayList<>(); + + if (rootNode.has("models")) { + for (JsonNode modelNode : rootNode.get("models")) { + if (modelNode.has("name")) { + models.add(modelNode.get("name").asText()); + } + } + } + + return models; + + } catch (Exception e) { + log.error("Error getting Ollama models", e); + return new ArrayList<>(); + } + } +} diff --git a/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/config/ConfigController.java b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/config/ConfigController.java index 0ab1daad6..06aadf2c4 100644 --- a/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/config/ConfigController.java +++ b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/config/ConfigController.java @@ -24,6 +24,8 @@ import ai.chat2db.server.web.api.controller.config.request.SystemConfigRequest; import ai.chat2db.server.web.api.controller.ai.openai.client.OpenAIClient; import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -41,6 +43,8 @@ @RestController public class ConfigController { + private static final Logger log = LoggerFactory.getLogger(ConfigController.class); + @Autowired private ConfigService configService; @@ -288,14 +292,74 @@ public DataResult getSystemConfig(@PathVariable("code") String code) { */ @GetMapping("/system_config/ai") public DataResult getChatAiSystemConfig(String aiSqlSource) { - DataResult dbSqlSource = configService.find(RestAIClient.AI_SQL_SOURCE); + // Debug logging + log.info("DEBUG: Received aiSqlSource parameter: " + aiSqlSource); + + DataResult dbSqlSource = configService.find(aiSqlSource); if (StringUtils.isBlank(aiSqlSource)) { if (Objects.nonNull(dbSqlSource.getData())) { aiSqlSource = dbSqlSource.getData().getContent(); } } + + // Debug logging + log.info("DEBUG: Final aiSqlSource: " + aiSqlSource); + AIConfig config = new AIConfig(); + + // Check if Ollama config exists in database + DataResult ollamaConfigCheck = configService.find("OLLAMAAI"); + boolean ollamaConfigExists = Objects.nonNull(ollamaConfigCheck.getData()) && + StringUtils.isNotBlank(ollamaConfigCheck.getData().getContent()); + + // Special handling for Ollama AI - check parameter directly + if ("OLLAMAAI".equals(aiSqlSource) || ollamaConfigExists) { + log.info("DEBUG: Handling OLLAMAAI directly"); + config.setAiSqlSource("OLLAMAAI"); + + // Get Ollama config from database + DataResult ollamaConfig = configService.find("OLLAMAAI"); + if (Objects.nonNull(ollamaConfig.getData()) && StringUtils.isNotBlank(ollamaConfig.getData().getContent())) { + // Parse JSON content for Ollama settings + String content = ollamaConfig.getData().getContent(); + log.info("DEBUG: Found Ollama config content: " + content); + try { + if (content.contains("ollamaApiHost")) { + String[] parts = content.split("\"ollamaApiHost\":\""); + if (parts.length > 1) { + String hostPart = parts[1].split("\"")[0]; + config.setApiHost(hostPart); + log.info("DEBUG: Parsed host: " + hostPart); + } + } + if (content.contains("ollamaModel")) { + String[] parts = content.split("\"ollamaModel\":\""); + if (parts.length > 1) { + String modelPart = parts[1].split("\"")[0]; + config.setModel(modelPart); + log.info("DEBUG: Parsed model: " + modelPart); + } + } + } catch (Exception e) { + log.info("DEBUG: Parse failed, using defaults"); + // Set defaults if parsing fails + config.setApiHost("http://localhost:11434"); + config.setModel("deepseek-v3.1:671b-cloud"); + } + } else { + log.info("DEBUG: No config found, using defaults"); + // Set defaults if no config found + config.setApiHost("http://localhost:11434"); + config.setModel("deepseek-v3.1:671b-cloud"); + } + return DataResult.of(config); + } + AiSqlSourceEnum aiSqlSourceEnum = AiSqlSourceEnum.getByName(aiSqlSource); + + // Debug logging + log.info("DEBUG: AiSqlSourceEnum result: " + (aiSqlSourceEnum != null ? aiSqlSourceEnum.name() : "null")); + if (Objects.isNull(aiSqlSourceEnum)) { aiSqlSource = AiSqlSourceEnum.CHAT2DBAI.getCode(); config.setAiSqlSource(aiSqlSource); @@ -380,6 +444,38 @@ public DataResult getChatAiSystemConfig(String aiSqlSource) { config.setApiHost(Objects.nonNull(zhipuApiHost.getData()) ? zhipuApiHost.getData().getContent() : ""); config.setModel(Objects.nonNull(zhipuModel.getData()) ? zhipuModel.getData().getContent() : ""); break; + case OLLAMAAI: + // Get Ollama config from database + DataResult ollamaConfig = configService.find(aiSqlSource); + if (Objects.nonNull(ollamaConfig.getData()) && StringUtils.isNotBlank(ollamaConfig.getData().getContent())) { + // Parse JSON content for Ollama settings + String content = ollamaConfig.getData().getContent(); + try { + if (content.contains("ollamaApiHost")) { + String[] parts = content.split("\"ollamaApiHost\":\""); + if (parts.length > 1) { + String hostPart = parts[1].split("\"")[0]; + config.setApiHost(hostPart); + } + } + if (content.contains("ollamaModel")) { + String[] parts = content.split("\"ollamaModel\":\""); + if (parts.length > 1) { + String modelPart = parts[1].split("\"")[0]; + config.setModel(modelPart); + } + } + } catch (Exception e) { + // Set defaults if parsing fails + config.setApiHost("http://localhost:11434"); + config.setModel("qwen3-coder"); + } + } else { + // Set defaults if no config found + config.setApiHost("http://localhost:11434"); + config.setModel("qwen3-coder"); + } + break; default: break; }