Location>code7788 >text

Teaching you to create your own workflows and empowering AI assistants with personalized recommendation superpowers

Popularity:314 ℃/2024-12-12 19:35:14

Previously, we have completed the basic process of workflow and the overall framework design, the next task is to enter the actual operation and implementation stage. If any students are not familiar enough with the overall structure of the workflow, you can first refer to this article to help you better understand and master the various parts of the workflow:

This post is the third in my series of hands-on tutorials on building Agents with Spring AI. Although Spring AI is still in a snapshot version and has not yet been released in an official version, it doesn't prevent us from learning about its latest features and developments. After all, artificial intelligence is one of the core directions for future development. Next, I will go straight to the topic, without further ado, let's start!

Today we will be developing a demo using mainly Spring AI Alibaba to show how to build an AI assistant with this framework. At the end, I will also attach the entire AI Assistant demo video for your reference. Through this demo, you can see that current Java developers no longer need to turn to Python, and can still participate in the trend of AI Agent development to capitalize on this new technology trend!

recap

Well, looking back at the previous work, we have successfully built a personal AI Assistant Agent example that is already equipped with a number of useful features, and next I will briefly review the implementation of these features and their characteristics:

  1. Travel TipsThe process of generating travel tips is fast and efficient, with no need for complex configuration, and can quickly respond to users' needs and provide accurate travel recommendations.
  2. Weather Enquiry: This function can accurately provide the weather forecast of the user's location by querying the code of the current address from the database and then calling the weather API interface for real-time weather query, ensuring that the user gets the latest weather information.
  3. Personal to-do list: The system can automatically generate SQL queries and perform related operations based on user commands, and directly access the database to facilitate users to add, delete or update their personal to-do lists, which greatly improves the efficiency of task management.

These functions have been effectively integrated and applied in the whole AI Assistant Agent, which greatly improves the user experience and operation convenience. Next, the architectural design of the whole system is shown below:

image

Previously, we have been using hunyun-compatible OpenAI interfaces for testing and tuning, and today we continue to develop Spring AI applications, but this time we use the domestic version of Spring AI - Spring AI Alibaba. Unlike the previous OpenAI interface, Spring AI Alibaba is more in line with the domestic technical environment and needs. If you wish to use a native compatible interface, you can currently consider Alibaba's Tongyi Thousand Questions model, which provides more mature and stable API support.

It should be noted that, regarding the situation of third-party chat model interfaces in Spring AI, on the basis of my current understanding, only Alibaba is officially developed and supported, and there is a higher guarantee of stability. While some other interfaces such as WisePop, although functionally able to meet the basic needs, but they are more maintained by individual developers and enthusiasts, stability and technical support is relatively different, the use of which need to pay special attention to its usability.

Personalized Recommendations

In this function module, we mainly rely on the user's historical portrait, through the analysis of their past behavioral data, interest preferences and other information, using AI models to summarize the relevant search keywords, and then recommend some movies, news and other content that the user may be interested in.

I've roughly drawn a simple design diagram showing the architecture of this recommendation process. You can take a look at this design diagram:

image

In order to better demonstrate the parallel processing capabilities of my workflow, I purposely chose two search plugins, Baidu and Bing, to demonstrate.

此外,Spring AI Alibaba框架内部已经开始着手开发插件商店,At present, although only4plug-in (software component),But it's just a starting point.,The future framework will continue to expand and enrich the variety and functionality of plug-ins,Support for more third-party plug-in integration and applications。as shown,The plugin store interface for the current framework is already taking shape:

image

Okay, let's move on to a more in-depth discussion. In this section, Baidu search is performed in real-time, i.e., the system will directly call Baidu's search interface. In contrast, Bing search needs to be invoked using a personal API key. Since the API key has not yet been configured, for the sake of demonstration and testing, I have temporarily written the return value of Bing search as a fixed result without making an actual API call. This is done to simplify the demo process and to ensure that the other parts work properly.

It is important to note that the Spring AI Alibaba framework itself does not currently support full workflow functionality. Although the framework is constantly being iterated and optimized, if you want to implement workflow orchestration functionality, developers need to develop their own functional modules, or choose to wait for the framework to officially add workflow support in a future version. Therefore, in the current workflow orchestration, we will rely on manual coding to realize the sequential execution of tasks and logic control.

Workflow Implementation

What we provided before was just some general framework and ideas, but the specific internal implementation was not expanded in detail. Here, I will share some key code implementations. Everyone may take a different approach in the implementation, and if you have a more efficient or suitable solution, you can certainly adjust and optimize it according to your own understanding.

Next, I will briefly show a schematic of the overall framework to help you understand the overall structure more clearly and avoid confusion in subsequent discussions. As shown in the diagram:

image

Okay, next we'll dive into the main core components. First of all, we need to put the scanning step in thestepmethods are encapsulated, transformed into tasks, and handed over to the workflow for management and saving.

initialContext

I referenced LlamaIndex's workflow for this part of the design by placing the scan class in thestepmethods are integrated into the initialization context for more efficient and flexible task management. In this way, we can ensure that each step is efficiently captured by the system and executed according to the expected flow. Next, let's take a look at the core code implementation, which is shown below:

private WorkflowContext initialContext() {
        WorkflowContext context = new WorkflowContext(false);
        // Get the class object of the current subclass
        Class<?> clazz = ();
        // 获取子类的所有方法
        Method[] methods = ();
        Graph graph = ();
        // 遍历所有方法,检查是否有 StepConfig 注解
        for (Method method : methods) {
            if (()) {
                Step annotation = ();
                String name = ();
                ("Method: {}", name);
                if (!().containsKey(name)){
                     ().put(name, new ArrayBlockingQueue<>(10));
                }
                // 获取方法的参数类型
                Class<?>[] parameterTypes = ();
                List<Class<? extends ToolEvent>> acceptedEventList = (());
                String eventName = name;
                StepConfig stepConfig = ().acceptedEvents(acceptedEventList).eventName(eventName)
                                .returnTypes(()).build();
                ("Adding node: {}", name);
                // 添加节点并设置节点标签和样式
                Node nodeA = (name);

                ("", "text-size: 20;size-mode:fit;fill-color:yellow;size:25px;");
                // 创建线程对象但不启动
                Thread thread = new Thread(() -> {
                    ("Thread started for method: {}", name);
                    //可能有多个事件,需要处理
                    ArrayList<Class<? extends ToolEvent>> events = new ArrayList<>(acceptedEventList);
                    // 获取队列对象
                    ArrayBlockingQueue<ToolEvent> queue = ().get(name);
                    while (true) {
                        try {
                            ToolEvent event = (); // 从队列中取出事件
                            if(!(())){
                                break;
                            }
                            if (isAcceptedEvent(event, acceptedEventList,events,context,name)) {
                                //开始执行时间
                                long startTime = ();
                                (event);
                                Object returnValue = (this, context); // 执行方法
                                //继续发布事件
                                continueSendEvent(context,returnValue,name);
                                // 执行时间
                                long endTime = ();
                                (name).setAttribute("", name + "耗时:" + (endTime - startTime) + "ms");
                            } else {
                                continue;
                            }
                        } catch (InterruptedException e) {
                            ("Thread interrupted for method: {}", name, e);
                            ().interrupt();
                            break;
                        } catch (Exception e) {
                            ("Error executing method: {}", name, e);
                            //继续发布事件
                            continueSendEvent(context,new StopEvent(()),name);
                        }
                    }
                });
                (thread);
            }
        }
        return context;
    }

注释写的基本很清楚了,我再来简单解释一下这段代码的含义,帮助你理解。

  • 创建一个 WorkflowContext 对象,transmitted inwards false parameters,Indicates parallel processing。
  • Get information about the current class: Get the class object of the current subclass and the list of its declared methods.
  • Iterate over methods and look for the Step annotation: Iterate over all methods in the subclass and check if each method is marked with the @Step annotation.
  • Setting up the event queue: for each method annotated with @Step, first get its name and then check the event queue in the context. If it doesn't exist, create an ArrayBlockingQueue for the method name.
  • Add Graph Nodes: Add a node to the Graph for each step and set its visual style (such as text size and fill color).
  • Create and configure threads:
    • Create a new thread for each step. This thread is responsible for reading events from the event queue and executing the step methods according to the event handling logic.
    • In this thread, events in the queue are continuously read through an infinite loop, processing events that match the conditions. The processing records the execution time of the method and updates the node label information, handles exceptions and continues to post events.
  • Handle events and execute methods:
    • After each event is taken out, the current state is first checked to see if execution should continue (e.g., to see if a result has been returned).
    • If the fetched event is accepted by the step, the corresponding method is called to perform processing and continue to post subsequent events based on the return value. If an exception is thrown while the method is executing, it is also caught and handled, while continuing to post a stop event.

The run method

After wrapping the tasks, the next thing to do is how to start and execute all the tasks. The logic in this section is mainly focused on therunmethod, which is responsible for coordinating and controlling the flow of execution of all tasks. Next, let's take a brief look at therunmethod in the core code implementation:

public String run(String jsonString) throws IOException {
    WorkflowContext context = initialContext();
    if (!(jsonString)) {
        //starting parameter
        ().putAll((jsonString));
    }
    WorkflowHandler handler = new WorkflowHandler(context);
    (timeout);
    if (showUI) {
        //todo For local testing,wouldspringbootKill the program together.,Post-optimization
        ().display();
        try {
            ("Press any key to exit...");
            ();
        } catch (IOException e) {
            ();
        }
    } else {
        // Exporting Graphics as Files
        FileSinkImages pic = new FileSinkImages(, .HD1080);
        (.COMPUTED_FULLY_AT_NEW_IMAGE);
        ((), "");
    }
    return ();
}

Let me briefly explain what this part of the code means.

  • Initializing the workflow context is the part of the code just described above.
  • Processing input JSON strings: the main consideration here is that the workflow can have input parameters, and I will be input parameters are treated as json processing, so it is also good to do the object encapsulation in the future.
  • Execute workflow tasks: here it's a simple matter of starting all the TASK thread tasks.
  • UI display or file export: because I am not front-end, technology is limited, and did not use the front-end to generate HTML code output, but the use of the graphstream class to quickly generate the picture or pop-up window UI. and a simple record of the execution time of each event.
  • Return Result: Finally, I will put all the results of the workflow into result and return them to the caller. The whole workflow is complete.

The rest of the section is basically free of too much complex code. By referring to LlamaIndex's core workflow code, I customized this workflow wrapper and optimized it appropriately. Next, we move on to the real testing phase, focusing on checking its stability and completeness.

Although the currently implemented version still has a lot of room for optimization and improvement, at least it is running smoothly and has initial usability. Now, let's start the actual running tests and see how it works.

preliminary

Request api-key

The connection address is as follows:/?spm=a2c4g.11186623.0.0.140f3048QPbIUu#/model-market

Once you are in, please select any of the thousands of models according to your personal needs. Then, check and save your personal API key. If you don't have a key, you can follow the guidelines to create a new key by yourself, the specific steps are shown in the figure.

image

Once saved, we need to use this api-key.

Create a project

We can directly copy an official demo, keep all the dependencies unchanged, and other parts can be modified and adjusted according to our needs. The overall structure of the project is shown in the figure:

image

Okay, let's move on to the next step. First, since we need to integrate the Baidu Search and Bing Search plugins, we can add them directly to the in the dependencies. After the integration, the final dependency configuration is shown below:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="/POM/4.0.0" xmlns:xsi="http:///2001/XMLSchema-instance"
         xsi:schemaLocation="/POM/4.0.0 /xsd/maven-4.0.">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId></groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId></groupId>
    <artifactId>workflow-example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>workflow-example</name>
    <description>Demo project for Spring AI Alibaba</description>

    <properties>
        <>UTF-8</>
        <>UTF-8</>
        <>17</>
        <>17</>
        <>3.1.1</>
        <!-- Spring AI -->
        <>1.0.0-M3.2</>
    </properties>

    <dependencies>
        <dependency>
            <groupId></groupId>
            <artifactId>spring-ai-alibaba-starter</artifactId>
            <version>${}</version>
        </dependency>

        <dependency>
            <groupId></groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId></groupId>
            <artifactId>spring-ai-alibaba-starter-plugin-baidusearch</artifactId>
            <version>1.0.0-M3.2</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId></groupId>
            <artifactId>spring-ai-alibaba-starter-plugin-bingsearch</artifactId>
            <version>1.0.0-M3.2</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId></groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId></groupId>
                <artifactId>maven-deploy-plugin</artifactId>
                <version>${}</version>
                <configuration>
                    <skip>true</skip>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Next, we'll go ahead and fill out a profile message as shown below:

spring:
  application:
    name: workflow-example

  ai:
    dashscope:
      api-key: ownkey,as if:sk-
      chat:
        options:
          model: qwen-plus

Here, I'm using theqwen-plus model, but you are perfectly free to choose from the other available models depending on your actual needs. Based on the current options, you have the following choices:qwen-turbobailian-v1dolly-12b-v2qwen-plus as well asqwen-max

Event Encapsulation Optimization

Next, we'll start using our self-createdToolEvent Class. In a recent optimization, I made some improvements to the class, mainly adding an abstract logic designed to better handle the logic of workflow events. Not only did this improve the extensibility and maintainability of the code, but it also added aEvent output parameter nameThe introduction of this parameter name makes it easier and more efficient to get parameter information for other nodes. Below is an example of the updated code:

public abstract class ToolEvent {
//Some of the code is omitted here
public String getOutputName() {
    if ( == null) {
         = ().getSimpleName();
    }
    return ;
}

public abstract void handleEvent(Map<String, Object> globalContext);
}

Next, we need to encapsulate the node related to time processing. Considering that this method should not be exposed to the user to be called directly, but should be controlled and executed internally by us, I have encapsulated it to the time node before posting the event. Below is the implementation of the relevant code:

public class WorkflowContext {
  //Some of the code is omitted here
  public void sendEvent(ToolEvent value) {
      //Execute here
      (globalContext);
      ().stream().forEach(entry -> {
          String key = ();
          ("send event to {},event:{}", key, ());
          ().add(value);
      });
  }
}

The rest of the section has no obvious need for further optimization, and we can just start using the abstracted event class to create the required node information and integrate it into the system for specific functionality.

Workflow node creation

HistoryInfoEvent

In order to facilitate the demonstration, for the sake of simplification, we did not actually carry out the operation of extracting the user's historical portrait, but simply wrote a sentence of explanatory text. In actual application scenarios, it is entirely possible to extract relevant information from user data for further analysis and processing. The following is the relevant code example:

public class HistoryInfoEvent extends ToolEvent {

    private String HISTORY_INFO = "{\n" +
            " \"name\": \"Hardworking Rainy",\n" +
            " \"age\": \"28 years old\",\n" +
            " \"hobbies\": {\n" +
            " \"sports\": [\"Basketball\"],\n" +
            " \"movies\": [\"Transformers\", \"Iron Man\", \"Avengers\"],\n" +
            " \"news\": [\"AI real-time news\"]\n" +
            " },\n" +
            " \"occupation\": \"Software Engineer\",\n" +
            " \"location\": \"Beijing\"\n" +
            "}".

    @Override
    public void handleEvent(Map<String, Object> globalContext) {
        // Assuming information from the user portrait
        (1342);
        //Store the output value for others to use
        (getOutputName()+":output", HISTORY_INFO);
    }
}

AIChatEvent

Here, we need to add achatClient property object and make sure that it is injected into the relevant event at initialization time so that it can be used properly in subsequent operations. To better demonstrate how to use it, I have simply written some hint words for planning the output format. These hint words serve to qualify the style and structure of the output content. Typically, Spring AI is able to effectively restrict and format the output by passing in the appropriate class at call time and automatically passing the preset hint words to the methods in the class.

However, for the purposes of this demonstration, I have taken a simplified approach to the process in order to visualize the processes and functionality more visually.

public class AIChatEvent extends ToolEvent {

    private String systemprompt = """
                - Role: Personalized Information Retrieval Expert
                - Background: Users want to get customized recommendations based on their interests and other personal information, including movies and news.
                - Profile: You are a professional Personalized Information Retrieval Specialist who is good at efficiently retrieving and recommending relevant content based on users' personal information and preferences.
                - Skills: You have strong information filtering skills, in-depth knowledge of various information sources, and the ability to respond quickly to user needs.
                - Goals: Based on the user's interests and preferences, provide two keywords to help the user quickly find today's movie recommendations and today's real-time news in the search engine.
                - Constrains: The keywords need to be concise, relevant and can be used directly in search engine queries.
                - OutputFormat: return two array elements, each element contains a keyword.
                - Workflow: 1.
                  1. Analyze the user's interests and personal information. 2.
                  2. according to the results of the analysis, to determine the keywords related to movies, news.
                  3. Keywords will be returned to the user in the form of an array.
                - Examples.
                  - Example 1: Users like sci-fi movies and international news.
                    - ['Today's Science Fiction Movie Recommendations', 'Today's International News Headlines']
                  - Example 2: User likes history documentaries, financial news
                    - Example 2: Users like historical documentaries, financial news ['Today's historical documentaries', 'Today's financial news headlines'].
                  - Example 3: Users like action movies, sports news
                    - ['Today's Action Movie Recommendations', 'Today's Sports News Alerts']
                """;
    private ChatClient chatClient;

    public AIChatEvent(ChatClient chatClient) {
         = chatClient.
    }

    @Override
    public void handleEvent(Map<String, Object> globalContext) {

        String prompt = """
                Please return to me an array of two keywords based on the provided user profile, each of which needs to be concise, relevant, and directly usable for search engine queries.
                ---
                """ + (()+":output");
        String content = ().system(systemprompt).user(prompt).advisors(new MyLoggerAdvisor()).call().content();
        ("content->{}",content);
        (getOutputName()+":output", content);
    }
}

SearchEvent

To demonstrate the parallel processing capabilities of the workflow, I created two separate nodes in the Search Events section. These nodes are intended to demonstrate the execution of multiple tasks at the same time. Since the Bing plugin requires an API key that is not configured in the current environment, I have temporarily written it dead, making it impossible to get actual search results. Below is the relevant code example:

public class BingSearchEvnet extends ToolEvent {
// private BingSearchService bingSearchService = new BingSearchService();
    private String searchWord;
    public BingSearchEvnet(String searchWord) {
         = searchWord;
    }
    @Override
    public void handleEvent(Map<String, Object> globalContext) {
        //generic search
        (2000);
        (getOutputName()+":output", searchWord+",Not searched for");
    }
}


public class BaiDuSearchEvent extends ToolEvent {
    private BaiduSearchService baiduSearchService = new BaiduSearchService();
    private String searchWord;
    public BaiDuSearchEvent(String searchWord) {
         = searchWord;
    }
    @Override
    public void handleEvent(Map<String, Object> globalContext) {
         request = new (searchWord, 10);
         response = (request);
        if (response == null || ().isEmpty()){
            return;
        }
        StringBuilder stringBuilder = new StringBuilder();
        for ( result : ()) {
            ("---------");
            ("title:");
            (());
            ("text:");
            (());
            (":{}",());
            (":{}",());
        }
        (getOutputName()+":output", ());
    }
}

Note that the code provided above contains two classes. In order to reduce the length of the code, I have merged them together. It is important to note that the official plugin's Baidu search result object does not provide theget method. So, when processing the result, I parsed it out and stored it in a string variable instead of using the directtoJsonString method. This is because if you call this method directly for conversion, you will end up with an empty result.

Workflow planning

The remaining part is to plan and connect the nodes of the workflow. Specifically, this step is to ensure that the logical relationships and execution order between all the nodes are properly connected to form a complete workflow. Next, we can accomplish this with the following code:

public class RecommendWorkflow extends Workflow {
    private ChatClient chatClient;
    public RecommendWorkflow(ChatClient chatClient) {
         = chatClient;
    }
    
    @Step(acceptedEvents = {})
    public ToolEvent toHistoryInfoEvent(WorkflowContext context) {
        return new HistoryInfoEvent();
    }

    @Step(acceptedEvents = {})
    public ToolEvent toAIChatEvent(WorkflowContext context) {
        return new AIChatEvent(chatClient);
    }

    /**
     Demonstrate parallel effects
     */
    @Step(acceptedEvents = {})
    public ToolEvent toBaiduSearchEvent(WorkflowContext context) {
        Object array = ().get(() + ":output");
        //classifier for repeated actionsstringarrays
        JSONArray jsonArray = ((String) array);
        return new BaiDuSearchEvent((0));
    }
    /**
     Demonstrate parallel effects
     */
    @Step(acceptedEvents = {})
    public ToolEvent toBingSearchEvent(WorkflowContext context) {
        Object array = ().get(() + ":output");
        //classifier for repeated actionsstringarrays
        JSONArray jsonArray = ((String) array);
        return new BingSearchEvnet((1));
    }

    @Step(acceptedEvents = {,})
    public ToolEvent toStopEvent(WorkflowContext context) {
        //Get Search Results
        JSONObject result = new JSONObject();
        ("Baidu Search Results:",().get(() + ":output"));
        ("bingSearch results:",().get(() + ":output"));
        return new StopEvent(());
    }
}

In this part of the code, we can clearly see that the processing logic after workflow optimization becomes very concise. Apart from the need to encapsulate the necessary parameters for the events, there is almost no other complex processing logic. Next, we need to further configure and inject the required chat macromodel to ensure that the system can smoothly interact with it and accomplish the corresponding tasks.

Model Configuration

In this configuration, I tweaked the workflow visualization and timeout. In particular, note that if you are in a production environment, avoid enabling theshowui option, which will allow the workflow process to be converted into an image to be saved.

In addition to this, I also configured the timeout for large models, as the default timeout is short and if communication with the model takes too long, it can easily trigger a timeout error and result in an error report.

Finally, I also added a configuration for log printing so that I can easily view the logs of calls to the big model during testing for better debugging and troubleshooting.

@Configuration
class ChatConfig {

    @Bean
    RecommendWorkflow recommendWorkflow( builder) {
        RecommendWorkflow recommendWorkflow = new RecommendWorkflow(());
        (20);
        (true);
        return recommendWorkflow;
    }

    @Bean
     restClientBuilder(RestClientBuilderConfigurer restClientBuilderConfigurer) {
        ClientHttpRequestFactorySettings defaultConfigurer =  
                .withReadTimeout((5))
                .withConnectTimeout((30));
         builder = ()
                .requestFactory((defaultConfigurer));
        return (builder);
    }

    @Bean
    MyLoggerAdvisor myLoggerAdvisor() {
        return new MyLoggerAdvisor();
    }
}

log class

The way the log is written is actually very simple.

@Slf4j
public class MyLoggerAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {

    public static final Function<AdvisedRequest, String> DEFAULT_REQUEST_TO_STRING = (request) -> {
        return ();
    };

    public static final Function<ChatResponse, String> DEFAULT_RESPONSE_TO_STRING = (response) -> {
        return (response);
    };

    private final Function<AdvisedRequest, String> requestToString;

    private final Function<ChatResponse, String> responseToString;

    public MyLoggerAdvisor() {
        this(DEFAULT_REQUEST_TO_STRING, DEFAULT_RESPONSE_TO_STRING);
    }

    public MyLoggerAdvisor(Function<AdvisedRequest, String> requestToString,
                           Function<ChatResponse, String> responseToString) {
         = requestToString;
         = responseToString;
    }

    @Override
    public String getName() {
        return ().getSimpleName();
    }

    @Override
    public int getOrder() {
        return 0;
    }

    private AdvisedRequest before(AdvisedRequest request) {
        ("request: {}", (request));
        return request;
    }

    private void observeAfter(AdvisedResponse advisedResponse) {
        ("response: {}", (()));
    }

    @Override
    public String toString() {
        return ();
    }

    @Override
    public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {

        advisedRequest = before(advisedRequest);

        AdvisedResponse advisedResponse = (advisedRequest);

        observeAfter(advisedResponse);

        return advisedResponse;
    }

    @Override
    public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {

        advisedRequest = before(advisedRequest);

        Flux<AdvisedResponse> advisedResponses = (advisedRequest);

        return new MessageAggregator().aggregateAdvisedResponse(advisedResponses, this::observeAfter);
    }
}

Here contains all the enhancer methods we need to achieve a larger amount of code, but in fact, their core logic is not complex, mainly in different places to print the contents of the current object, and record the relevant log information. The purpose is to ensure that the system can be run in the process of obtaining enough debugging information in a timely manner to facilitate the subsequent debugging and optimization.

access portal

Ultimately, to simplify the process of accessing our workflow, I wrote a controller directly to receive and process external requests. Below is an example of code that implements this controller:

@RestController
@RequestMapping("/hello")
public class HelloWordController {

    @Autowired
    private RecommendWorkflow recommendWorkflow;

    @RequestMapping("/word")
    public String hello() throws IOException {
        return ("");
    }
}

For the sake of demonstration and to simplify the complexity of the example, I have not introduced any input parameters here. In fact, you can pass some necessary identifying information (e.g., user ID, session ID, or other contextual data needed for the workflow) to the controller as an input parameter, depending on your actual needs.

Workflow Demo

Next, we will perform a demonstration of the workflow functionality. After launching the project, simply enter the following address into your browser:http://localhost:8080/hello/wordThe function can be accessed directly. The details will be shown in the figure below.

image

The final return result is shown in the figure:

image

Under normal circumstances, our project automatically generates and outputs an image, and you can see how that image is displayed in the system. As shown in the picture:

image

I have enabled the UI switch function for better display of the effect. Below is the image of the interface effect after the switch is enabled:

image

Embedded AI Assistant

As is the design pattern of many intelligent body platforms on the market today, a workflow is usually a module that can be invoked separately. Therefore, we can deploy the workflow as part of a microservice and make it easy for our AI Agent to invoke it by exposing the corresponding interface. This interface is usually presented as a function. It is important to note that the AI Chat Q&A project and the Workflow project are two separate modules, nevertheless, it is perfectly possible for you to merge these two modules into a whole, depending on the project requirements. In order to maintain consistency with other intelligent body platforms, we have chosen to present them as two separate projects in this demonstration.

In order to help you understand the whole architecture more clearly, I simply draw a framework diagram, you can refer to it to avoid confusion in the subsequent explanation.

image

Okay, next, we need to make some adjustments to our existing system, specifically, replacing the OpenAI dependencies we were using with our Thousand Questions model.

Large model access

Here, it is important to note that the Thousand Questions model is not currently integrated directly into the official Spring AI framework, so it cannot be used directly through the Spring AI dependency introduction like other common models.

image

Therefore, if we wish to use the Thousand Questions model as a dependency in our project, we must follow the Spring AI Alibaba integration by following the instructions in their official demo for thespring-ai-alibaba-starterdependency is introduced into our project. Below is an example of how the dependency is introduced and configured for your reference:

<dependency>
    <groupId></groupId>
    <artifactId>spring-ai-alibaba-starter</artifactId>
    <version>1.0.0-M3.2</version>
</dependency>

Immediately following, the next step is to make sure to comment out or just remove the OpenAI dependencies that you added earlier in the project, otherwise it may lead to dependency conflicts and error reporting issues at runtime. Since my Spring AI Q&A project is based on thepropertiesconfiguration file to manage the configuration information, so we need to add thefile to add the relevant configuration information needed to integrate with the Thousand Questions model.

-key= sk-63eb29c7f4dd4de489fa64382d94a797
= qwen-plus

This allowed us to get the system up and running successfully. As you can see in the picture, the basic Q&A process is working properly and all functions are working as expected.

image

Now that the project is up and running, we can move on to writing a function call. At this stage, we will communicate directly via HTTP calls and leave the service discovery and registration mechanisms out of it for now. The purpose of this is to simplify the demo.

FunctionCall

It is important to note that after making this change, we found that many compatibility issues arose, and processes that were previously working well began to show errors. As a result, I fully optimized the entire system. After some tweaking and fixing, the final plugin call configuration that remained was the current version.

@Bean
public FunctionCallbackWrapper weatherFunctionInfo(AreaInfoPOMapper areaInfoPOMapper) {
  return (new BaiDuWeatherService(areaInfoPOMapper))
      .withName("CurrentWeather") // (1) function name
      .withDescription("获取指定地点的天气情况") // (2) function description
      .build();
}
@Bean
@Description("旅游规划")
public FunctionCallbackWrapper travelPlanningFunctionInfo() {
    return (new TravelPlanningService())
            .withName("TravelPlanning") // (1) function name
            .withDescription("根据用户的旅游目的地推荐景点、酒店以及给出实时机票、高铁票信息") // (2) function description
            .build();
}
@Bean
@Description("待办管理")
public FunctionCallbackWrapper toDoListFunctionWithContext(ToDoListInfoPOMapper toDoListInfoPOMapper, JdbcTemplate jdbcTemplate) {
    return (new ToDoListInfoService(toDoListInfoPOMapper,jdbcTemplate))
            .withName("toDoListFunctionWithContext") // (1) function name
            .withDescription("添加待办,crud:c 代表增加;r:代表查询,u:代表更新,d:代表删除") // (2) function description
            .build();
}
@Bean
@Description("用户查询今日推荐内容")
public FunctionCallbackWrapper myWorkFlowServiceCall() {
    return (new MyWorkFlowService())
            .withName("myWorkFlowServiceCall") // (1) function name
            .withDescription("用户查询今日推荐内容,参数:username为用户名") // (2) function description
            .build();
}

接下来,我们将讨论工作流中插件的调用部分。这里涉及到一个 HTTP 调用功能。下面是该部分的代码示例:

@Slf4j
@Description("今日推荐")
public class MyWorkFlowService implements Function<, > {
    @JsonClassDescription("username:用户名字")
    public record WorkFlowRequest(String username) {}
    public record WorkFlowResponse(String result) {}

    public WorkFlowResponse apply(WorkFlowRequest request) {
        MyWorkFlowRun myWorkFlowRun = new MyWorkFlowRun();
        String result = ();
        return new WorkFlowResponse(result);
    }
}
@Slf4j
public class MyWorkFlowRun {
    RestTemplate restTemplate = new RestTemplate();

    /**
     * 这里也可以优化成一个公用的插件,比如传入一个工作流id,然后工作流项目那边根据id运行,这样就可以复用
     * @param username 入参
     * @return 返回的结果
     */
    public String getResult(String username) {
        ("打印输入参数-username:{}", username);
        //我们不使用入参作为搜索词,而是直接写死,这里只是演示下
        String result  = ("http://localhost:8080/hello/word", );
        return result;
    }
}


在这里,我只是进行了一个简化的操作,即简单调用了本地的工作流接口,并打印了输入参数。由于当前的实现中并不需要这些参数,因此我只是将其输出做了展示。然而,如果在实际应用中你需要使用这些参数,完全可以将其传递给工作流接口进行相应的处理。

接下来,一切准备就绪,我们只需将这个插件集成到我们的问答模型中。具体的代码实现如下:

@PostMapping("/ai-function")
ChatDataPO functionGenerationByText(@RequestParam("userInput")  String userInput) {
    //Omit duplicate code here
    String content = 
            .prompt(systemPrompt)
            .user(userInput)
            .advisors(messageChatMemoryAdvisor,myLoggerAdvisor)
            //用来区分不同会话的参数conversation_id
            .advisors(advisor -> ("chat_memory_conversation_id", conversation_id)
                    .param("chat_memory_response_size", 100))
            .functions("CurrentWeather","TravelPlanning","toDoListFunctionWithContext","myWorkFlowServiceCall")
    //Omit duplicate code here

Here, we simply add the name we just used directly to thefunctions In fact, this step does not require a lot of complex configuration, and once it has been added, the rest is handled automatically by the big model. In fact, there is no need to configure this step in a complicated way, and once the additions have been made, the next steps are handled automatically by the larger model.

Assistant effect

Here is a brief overview of the front-end UI part of the implementation. I mainly used ChatSDK and integrated it into the project. The whole process is relatively simple, the configuration items are also relatively basic, developers only need to refer to the official documentation provided by the operation of the steps in accordance with the guidelines can be successfully completed integration. I won't go into details here.

image

Next, we will start two projects: one is a Spring AI project and the other is a Spring AI Alibaba project (responsible for the workflow part). After launching these two projects, we can visualize how they perform in real-world operation.

summarize

In this series of tutorials, we delve into Spring AI and its practical application in the domestic version of Spring AI Alibaba, focusing on how to build a feature-rich, intelligent and efficient AI assistant. By explaining in detail the whole process from the basic process design of the workflow to the actual operation of the implementation, we gradually unveil the mystery of the AI assistant development, so that Java developers can easily get started and apply the latest AI technology.

First, we review the whole process of building a personal AI assistant Agent, covering several useful functional modules such as travel tips, weather checking and personal to-do list. In this section, we not only introduce the design and implementation of the relevant functions, but also explore how to seamlessly integrate these functional modules into a comprehensive AI Assistant to ensure a smooth and intelligent user experience. In addition, we provide an in-depth analysis of workflow implementation details, focusing on event encapsulation optimization, creation and organization of workflow nodes, and how to efficiently plan and manage complex workflows.

At the end of the tutorial, we vividly demonstrate the actual effect of AI Assistant through an actual project startup and running test session. Through these practical tests, we not only verified the stability and scalability of the system, but also laid a solid foundation for subsequent optimization and feature expansion. Ultimately, our goal is to let developers not only understand the core technology and application framework of Spring AI, but also master the essence of AI assistant development through practical operation.


I'm Rain, a Java server-side coder, studying the mysteries of AI technology. I love technical communication and sharing, and I am passionate about open source community. I am also a Tencent Cloud Creative Star, Ali Cloud Expert Blogger, Huawei Cloud Enjoyment Expert, and Nuggets Excellent Author.

💡 I won't be shy about sharing my personal explorations and experiences on the technology path in the hope that I can bring some inspiration and help to your learning and growth.

🌟 Welcome to the effortless drizzle! 🌟