This is an automated email from the ASF dual-hosted git repository.

gongchao pushed a commit to branch ai-enhance
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git

commit 73481b7d3ed480b5623ded80298f99e718725f67
Author: tomsun28 <[email protected]>
AuthorDate: Wed Oct 22 21:59:17 2025 +0800

    [chore] refactor ai service
    
    Signed-off-by: tomsun28 <[email protected]>
---
 .../hertzbeat/ai/agent/config/LlmConfig.java       | 132 +++++++++++++++++----
 .../impl/ChatClientProviderServiceImpl.java        |  15 ++-
 web-app/src/app/pojo/ModelProviderConfig.ts        |  34 +++++-
 .../shared/components/ai-chat/ai-chat.module.ts    |   2 +
 .../shared/components/ai-chat/chat.component.html  |  58 ++++++++-
 .../shared/components/ai-chat/chat.component.less  |  35 ++++++
 .../shared/components/ai-chat/chat.component.ts    |  72 ++++++++++-
 7 files changed, 314 insertions(+), 34 deletions(-)

diff --git 
a/hertzbeat-ai-agent/src/main/java/org/apache/hertzbeat/ai/agent/config/LlmConfig.java
 
b/hertzbeat-ai-agent/src/main/java/org/apache/hertzbeat/ai/agent/config/LlmConfig.java
index ebe38342a..ff3b2742a 100644
--- 
a/hertzbeat-ai-agent/src/main/java/org/apache/hertzbeat/ai/agent/config/LlmConfig.java
+++ 
b/hertzbeat-ai-agent/src/main/java/org/apache/hertzbeat/ai/agent/config/LlmConfig.java
@@ -18,55 +18,145 @@
 
 package org.apache.hertzbeat.ai.agent.config;
 
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.ai.agent.event.AiProviderConfigChangeEvent;
+import org.apache.hertzbeat.ai.agent.pojo.dto.ModelProviderConfig;
+import org.apache.hertzbeat.base.dao.GeneralConfigDao;
+import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
+import org.apache.hertzbeat.common.util.JsonUtil;
 import org.springframework.ai.chat.client.ChatClient;
 import org.springframework.ai.openai.OpenAiChatModel;
 import org.springframework.ai.openai.OpenAiChatOptions;
 import org.springframework.ai.openai.api.OpenAiApi;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.event.EventListener;
 
 /**
  * Configuration class for Large Language Model (LLM) settings.
  */
 
 @Configuration
+@Slf4j
 public class LlmConfig {
 
-    /**
-     * Create OpenAI API instance with dynamic API key
-     */
-    @Bean
-    public OpenAiApi openAiApi(DynamicOpenAiApiKey dynamicApiKey) {
-        return OpenAiApi.builder()
-                .apiKey(dynamicApiKey)
-                .build();
+    private final GeneralConfigDao generalConfigDao;
+    
+    private ApplicationContext applicationContext;
+
+    public LlmConfig(GeneralConfigDao generalConfigDao, ApplicationContext 
applicationContext) {
+        this.generalConfigDao = generalConfigDao;
+        this.applicationContext = applicationContext;
     }
 
     /**
-     * Create OpenAI Chat Options with custom settings
+     * Create ChatClient bean with all dependencies created internally
      */
     @Bean
-    public OpenAiChatOptions openAiChatOptions() {
-        return OpenAiChatOptions.builder()
-                .model("model")
-                .temperature(0.3)
-                .build();
+    public ChatClient openAiChatClient() {
+        return createChatClient();
     }
 
     /**
-     * Create OpenAI Chat Model with custom API configuration
+     * Create ChatClient with all necessary components
      */
-    @Bean
-    public OpenAiChatModel openAiChatModel(OpenAiApi openAiApi, 
OpenAiChatOptions openAiChatOptions) {
-        return OpenAiChatModel.builder()
+    private ChatClient createChatClient() {
+
+        GeneralConfig providerConfig = generalConfigDao.findByType("provider");
+        if (providerConfig == null || providerConfig.getContent() == null) {
+            log.warn("LLM Provider is not set, ChatClient bean will not be 
created");
+            return null;
+        }
+        ModelProviderConfig modelProviderConfig = 
JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
+
+        if (!modelProviderConfig.isEnable() || 
!modelProviderConfig.isStatus()) {
+            log.warn("LLM Provider is not enabled or status is not valid, 
ChatClient bean will not be created");
+            return null;
+        }
+
+        if (modelProviderConfig.getApiKey() == null) {
+            log.warn("LLM Provider configuration is incomplete, ChatClient 
bean will not be created");
+            return null;
+        }
+
+        if (modelProviderConfig.getBaseUrl() == null) {
+            if ("openai".equals(modelProviderConfig.getCode())) {
+                modelProviderConfig.setBaseUrl("https://api.openai.com/v1";);
+            } else if ("zhipu".equals(modelProviderConfig.getCode())) {
+                
modelProviderConfig.setBaseUrl("https://open.bigmodel.cn/api/paas/v4";);
+            } else if ("zai".equals(modelProviderConfig.getCode())) {
+                modelProviderConfig.setBaseUrl("https://api.z.ai/api/paas/v4";);
+            } else {
+                modelProviderConfig.setBaseUrl("https://api.openai.com/v1";);
+            }
+        }
+
+        if (modelProviderConfig.getModel() == null) {
+            if ("openai".equals(modelProviderConfig.getCode())) {
+                modelProviderConfig.setModel("gpt-5");
+            } else if ("zhipu".equals(modelProviderConfig.getCode())) {
+                modelProviderConfig.setModel("glm-4.6");
+            } else if ("zai".equals(modelProviderConfig.getCode())) {
+                modelProviderConfig.setModel("glm-4.6");
+            } else {
+                modelProviderConfig.setModel("gpt-5");
+            }
+        }
+
+        // Create OpenAI API instance
+        OpenAiApi openAiApi = OpenAiApi.builder()
+                .baseUrl(modelProviderConfig.getBaseUrl())
+                .apiKey(modelProviderConfig.getApiKey())
+                .build();
+        
+        // Create OpenAI Chat Options
+        OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder()
+                .model(modelProviderConfig.getModel())
+                .temperature(0.3)
+                .build();
+        
+        // Create OpenAI Chat Model
+        OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
                 .openAiApi(openAiApi)
                 .defaultOptions(openAiChatOptions)
                 .build();
+        
+        // Create and return ChatClient
+        return ChatClient.create(openAiChatModel);
     }
 
-    @Bean
-    public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel) {
-        return ChatClient.create(openAiChatModel);
+    /**
+     * AI configuration change event listener
+     * Uses ApplicationContext to unregister and re-register the ChatClient 
bean
+     */
+    @EventListener(AiProviderConfigChangeEvent.class)
+    public void onAiProviderConfigChange(AiProviderConfigChangeEvent event) {
+        log.info("Provider configuration change event received, refreshing 
ChatClient bean");
+        
+        try {
+            ConfigurableApplicationContext configurableContext = 
(ConfigurableApplicationContext) applicationContext;
+            DefaultListableBeanFactory beanFactory = 
(DefaultListableBeanFactory) configurableContext.getBeanFactory();
+            
+            // Remove the existing ChatClient bean
+            if (beanFactory.containsSingleton("openAiChatClient")) {
+                beanFactory.destroySingleton("openAiChatClient");
+                log.info("Existing ChatClient bean destroyed");
+            }
+                        
+            // Create new ChatClient with updated configuration
+            ChatClient newChatClient = createChatClient();
+            
+            // Register the new ChatClient bean
+            beanFactory.registerSingleton("openAiChatClient", newChatClient);
+            
+            log.info("ChatClient bean refreshed successfully with new AI 
provider configuration");
+            
+        } catch (Exception e) {
+            log.error("Failed to refresh ChatClient bean after configuration 
change", e);
+        }
     }
 
 }
diff --git 
a/hertzbeat-ai-agent/src/main/java/org/apache/hertzbeat/ai/agent/service/impl/ChatClientProviderServiceImpl.java
 
b/hertzbeat-ai-agent/src/main/java/org/apache/hertzbeat/ai/agent/service/impl/ChatClientProviderServiceImpl.java
index 51d73b5b9..933334df9 100644
--- 
a/hertzbeat-ai-agent/src/main/java/org/apache/hertzbeat/ai/agent/service/impl/ChatClientProviderServiceImpl.java
+++ 
b/hertzbeat-ai-agent/src/main/java/org/apache/hertzbeat/ai/agent/service/impl/ChatClientProviderServiceImpl.java
@@ -31,6 +31,7 @@ import org.springframework.ai.chat.messages.UserMessage;
 import org.springframework.ai.tool.ToolCallbackProvider;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.ApplicationContext;
 import reactor.core.publisher.Flux;
 
 import java.util.ArrayList;
@@ -45,19 +46,20 @@ import java.util.List;
 @Service
 public class ChatClientProviderServiceImpl implements 
ChatClientProviderService {
 
-    private final ChatClient chatClient;
+    private final ApplicationContext applicationContext;
 
     @Qualifier("hertzbeatTools")
     @Autowired
     private ToolCallbackProvider toolCallbackProvider;
 
     @Autowired
-    public ChatClientProviderServiceImpl(ChatClient openAiChatClient) {
-        this.chatClient = openAiChatClient;
+    public ChatClientProviderServiceImpl(ApplicationContext 
applicationContext) {
+        this.applicationContext = applicationContext;
     }
 
     public String complete(String message) {
-        return this.chatClient.prompt()
+        ChatClient chatClient = applicationContext.getBean("openAiChatClient", 
ChatClient.class);
+        return chatClient.prompt()
                 .user(message)
                 .call()
                 .content();
@@ -66,6 +68,9 @@ public class ChatClientProviderServiceImpl implements 
ChatClientProviderService
     @Override
     public Flux<String> streamChat(ChatRequestContext context) {
         try {
+            // Get the current (potentially refreshed) ChatClient instance
+            ChatClient chatClient = 
applicationContext.getBean("openAiChatClient", ChatClient.class);
+            
             List<Message> messages = new ArrayList<>();
 
             // Add conversation history if available
@@ -83,7 +88,7 @@ public class ChatClientProviderServiceImpl implements 
ChatClientProviderService
 
             log.info("Starting streaming chat for conversation: {}", 
context.getConversationId());
 
-            return this.chatClient.prompt()
+            return chatClient.prompt()
                     .messages(messages)
                     .system(PromptProvider.HERTZBEAT_SYSTEM_PROMPT)
                     .toolCallbacks(toolCallbackProvider)
diff --git a/web-app/src/app/pojo/ModelProviderConfig.ts 
b/web-app/src/app/pojo/ModelProviderConfig.ts
index 717409609..93879d934 100644
--- a/web-app/src/app/pojo/ModelProviderConfig.ts
+++ b/web-app/src/app/pojo/ModelProviderConfig.ts
@@ -22,8 +22,36 @@ export class ModelProviderConfig {
   status: boolean = false;
   error!: string;
   type!: string;
-  code!: string;
-  baseUrl!: string;
-  model!: string;
+  code: string = 'openai';
+  baseUrl: string = '';
+  model: string = '';
   apiKey!: string;
 }
+
+export interface ProviderOption {
+  value: string;
+  label: string;
+  defaultBaseUrl: string;
+  defaultModel: string;
+}
+
+export const PROVIDER_OPTIONS: ProviderOption[] = [
+  {
+    value: 'openai',
+    label: 'OpenAI',
+    defaultBaseUrl: 'https://api.openai.com/v1',
+    defaultModel: 'gpt-4'
+  },
+  {
+    value: 'zhipu',
+    label: 'ZhiPu (智谱)',
+    defaultBaseUrl: 'https://open.bigmodel.cn/api/paas/v4',
+    defaultModel: 'glm-4'
+  },
+  {
+    value: 'zai',
+    label: 'ZAI',
+    defaultBaseUrl: 'https://api.z.ai/api/paas/v4',
+    defaultModel: 'glm-4'
+  }
+];
diff --git a/web-app/src/app/shared/components/ai-chat/ai-chat.module.ts 
b/web-app/src/app/shared/components/ai-chat/ai-chat.module.ts
index 0f3ac1a0e..ca16c108b 100644
--- a/web-app/src/app/shared/components/ai-chat/ai-chat.module.ts
+++ b/web-app/src/app/shared/components/ai-chat/ai-chat.module.ts
@@ -28,6 +28,7 @@ import { NzIconModule } from 'ng-zorro-antd/icon';
 import { NzInputModule } from 'ng-zorro-antd/input';
 import { NzMessageModule } from 'ng-zorro-antd/message';
 import { NzModalModule } from 'ng-zorro-antd/modal';
+import { NzSelectModule } from 'ng-zorro-antd/select';
 import { NzSpinModule } from 'ng-zorro-antd/spin';
 import { MarkdownPipe } from 'ngx-markdown';
 
@@ -47,6 +48,7 @@ import { ChatComponent } from './chat.component';
     NzInputModule,
     NzMessageModule,
     NzModalModule,
+    NzSelectModule,
     NzSpinModule,
     MarkdownPipe
   ],
diff --git a/web-app/src/app/shared/components/ai-chat/chat.component.html 
b/web-app/src/app/shared/components/ai-chat/chat.component.html
index f55bd9387..1ae68dff2 100644
--- a/web-app/src/app/shared/components/ai-chat/chat.component.html
+++ b/web-app/src/app/shared/components/ai-chat/chat.component.html
@@ -160,18 +160,40 @@
 <!-- OpenAI Configuration Modal -->
 <nz-modal
   [(nzVisible)]="showConfigModal"
-  nzTitle="Ai Provider Configuration"
+  nzTitle="AI Provider Configuration"
   (nzOnCancel)="onCloseConfigModal()"
   (nzOnOk)="onSaveAiProviderConfig()"
   nzMaskClosable="false"
   [nzClosable]="false"
-  nzWidth="600px"
+  nzWidth="700px"
   [nzOkLoading]="configLoading"
   nzOkText="Validate & Save"
   nzCancelText="Cancel"
 >
   <div *nzModalContent class="-inner-content">
     <form nz-form nzLayout="vertical">
+      <!-- Provider Selection -->
+      <nz-form-item>
+        <nz-form-label nzRequired="true">AI Provider</nz-form-label>
+        <nz-form-control nzErrorTip="Please select an AI provider">
+          <nz-select 
+            [(ngModel)]="aiProviderConfig.code" 
+            name="provider"
+            nzPlaceHolder="Select AI Provider"
+            (ngModelChange)="onProviderChange($event)"
+            style="width: 100%"
+          >
+            <nz-option 
+              *ngFor="let option of providerOptions" 
+              [nzValue]="option.value" 
+              [nzLabel]="option.label"
+            ></nz-option>
+          </nz-select>
+          <p class="form-help">Choose your AI provider (OpenAI, ZhiPu, or 
ZAI)</p>
+        </nz-form-control>
+      </nz-form-item>
+
+      <!-- API Key -->
       <nz-form-item>
         <nz-form-label nzRequired="true">API Key</nz-form-label>
         <nz-form-control nzErrorTip="API Key is required">
@@ -179,6 +201,38 @@
           <p class="form-help">Your Provider API key. The key will be 
validated when saved.</p>
         </nz-form-control>
       </nz-form-item>
+
+      <!-- Base URL -->
+      <nz-form-item>
+        <nz-form-label>Base URL</nz-form-label>
+        <nz-form-control>
+          <nz-input-group nzSuffix="resetBtn">
+            <input nz-input [(ngModel)]="aiProviderConfig.baseUrl" 
name="baseUrl" placeholder="https://api.openai.com/v1"; />
+            <ng-template #resetBtn>
+              <button 
+                nz-button 
+                nzType="link" 
+                nzSize="small" 
+                (click)="resetToDefaults()" 
+                nzTooltipTitle="Reset to default values"
+                nz-tooltip
+              >
+                <i nz-icon nzType="reload"></i>
+              </button>
+            </ng-template>
+          </nz-input-group>
+          <p class="form-help">Custom API endpoint URL. Leave empty to use 
default for selected provider.</p>
+        </nz-form-control>
+      </nz-form-item>
+
+      <!-- Model -->
+      <nz-form-item>
+        <nz-form-label>Model</nz-form-label>
+        <nz-form-control>
+          <input nz-input [(ngModel)]="aiProviderConfig.model" name="model" 
placeholder="gpt-4" />
+          <p class="form-help">Model name to use. Leave empty to use default 
for selected provider.</p>
+        </nz-form-control>
+      </nz-form-item>
     </form>
   </div>
 </nz-modal>
diff --git a/web-app/src/app/shared/components/ai-chat/chat.component.less 
b/web-app/src/app/shared/components/ai-chat/chat.component.less
index 91e6c0005..8edbb3a1c 100644
--- a/web-app/src/app/shared/components/ai-chat/chat.component.less
+++ b/web-app/src/app/shared/components/ai-chat/chat.component.less
@@ -634,6 +634,41 @@ body[data-theme='dark'] .chat-container {
   }
 }
 
+// Provider configuration form styles
+.provider-config-form {
+  .provider-select-row {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    
+    nz-select {
+      flex: 1;
+    }
+  }
+  
+  .baseurl-input-row {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    
+    nz-input-group {
+      flex: 1;
+    }
+    
+    .reset-btn {
+      flex-shrink: 0;
+    }
+  }
+  
+  .form-item-with-help {
+    margin-bottom: 8px;
+    
+    + .form-help {
+      margin-bottom: 16px;
+    }
+  }
+}
+
 nz-modal {
   .ant-modal-body {
     padding: 24px;
diff --git a/web-app/src/app/shared/components/ai-chat/chat.component.ts 
b/web-app/src/app/shared/components/ai-chat/chat.component.ts
index c7f8f1436..1c4ed5eac 100644
--- a/web-app/src/app/shared/components/ai-chat/chat.component.ts
+++ b/web-app/src/app/shared/components/ai-chat/chat.component.ts
@@ -22,7 +22,7 @@ import { I18NService } from '@core';
 import { NzMessageService } from 'ng-zorro-antd/message';
 import { NzModalService } from 'ng-zorro-antd/modal';
 
-import { ModelProviderConfig } from '../../../pojo/ModelProviderConfig';
+import { ModelProviderConfig, PROVIDER_OPTIONS, ProviderOption } from 
'../../../pojo/ModelProviderConfig';
 import { AiChatService, ChatMessage, ConversationDto } from 
'../../../service/ai-chat.service';
 import { GeneralConfigService } from '../../../service/general-config.service';
 import { ThemeService } from '../../../service/theme.service';
@@ -48,6 +48,7 @@ export class ChatComponent implements OnInit, 
AfterViewChecked {
   showConfigModal = false;
   configLoading = false;
   aiProviderConfig: ModelProviderConfig = new ModelProviderConfig();
+  providerOptions: ProviderOption[] = PROVIDER_OPTIONS;
 
   constructor(
     private aiChatService: AiChatService,
@@ -433,15 +434,37 @@ export class ChatComponent implements OnInit, 
AfterViewChecked {
       next: response => {
         if (response.code === 0 && response.data) {
           this.aiProviderConfig = response.data;
+          // Ensure default values are set if not present
+          if (!this.aiProviderConfig.code) {
+            this.aiProviderConfig.code = 'openai';
+          }
+          if (!this.aiProviderConfig.baseUrl) {
+            const defaultProvider = this.providerOptions.find(p => p.value === 
this.aiProviderConfig.code);
+            if (defaultProvider) {
+              this.aiProviderConfig.baseUrl = defaultProvider.defaultBaseUrl;
+            }
+          }
+          if (!this.aiProviderConfig.model) {
+            const defaultProvider = this.providerOptions.find(p => p.value === 
this.aiProviderConfig.code);
+            if (defaultProvider) {
+              this.aiProviderConfig.model = defaultProvider.defaultModel;
+            }
+          }
+
           if (!response.data.enable) {
             this.loadConversations();
           } else {
             this.showAiProviderConfigDialog(response.data.error);
           }
+        } else {
+          // Initialize with default values if no config exists
+          this.aiProviderConfig = new ModelProviderConfig();
+          this.showAiProviderConfigDialog();
         }
       },
       error: error => {
         console.error('Failed to load model provider config:', error);
+        this.aiProviderConfig = new ModelProviderConfig();
         this.showAiProviderConfigDialog();
       }
     });
@@ -502,8 +525,23 @@ export class ChatComponent implements OnInit, 
AfterViewChecked {
    * Save OpenAI configuration
    */
   onSaveAiProviderConfig(): void {
-    if (!this.aiProviderConfig.apiKey.trim()) {
-      this.message.error('API Key is required');
+    if (!this.aiProviderConfig.apiKey?.trim()) {
+      this.message.error('Please enter API Key');
+      return;
+    }
+
+    if (!this.aiProviderConfig.code?.trim()) {
+      this.message.error('Please select a provider');
+      return;
+    }
+
+    if (!this.aiProviderConfig.baseUrl?.trim()) {
+      this.message.error('Please enter Base URL');
+      return;
+    }
+
+    if (!this.aiProviderConfig.model?.trim()) {
+      this.message.error('Please enter Model');
       return;
     }
 
@@ -536,4 +574,32 @@ export class ChatComponent implements OnInit, 
AfterViewChecked {
       }
     });
   }
+
+  /**
+   * Handle provider selection change
+   */
+  onProviderChange(provider: string): void {
+    const selectedProvider = this.providerOptions.find(p => p.value === 
provider);
+    if (selectedProvider) {
+      this.aiProviderConfig.code = provider;
+      // Auto-fill default values if current values are empty
+      if (!this.aiProviderConfig.baseUrl) {
+        this.aiProviderConfig.baseUrl = selectedProvider.defaultBaseUrl;
+      }
+      if (!this.aiProviderConfig.model) {
+        this.aiProviderConfig.model = selectedProvider.defaultModel;
+      }
+    }
+  }
+
+  /**
+   * Reset to default values for selected provider
+   */
+  resetToDefaults(): void {
+    const selectedProvider = this.providerOptions.find(p => p.value === 
this.aiProviderConfig.code);
+    if (selectedProvider) {
+      this.aiProviderConfig.baseUrl = selectedProvider.defaultBaseUrl;
+      this.aiProviderConfig.model = selectedProvider.defaultModel;
+    }
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to