Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Initialize Spring AI Alibaba project with DotPrompt support #304

Open
wants to merge 1 commit into
base: workflow
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
tab_width = 4
max_line_length = 120
insert_final_newline = true
trim_trailing_whitespace = true

[*.java]
indent_style = tab
ij_java_continuation_indent_size = 8
ij_java_keep_control_statement_in_one_line = false
ij_java_for_brace_force = always
ij_java_if_brace_force = always
ij_java_keep_first_column_comment = false
ij_java_keep_line_breaks = false
ij_java_keep_simple_blocks_in_one_line = true
ij_java_keep_simple_classes_in_one_line = true
ij_java_keep_simple_lambdas_in_one_line = true
ij_java_keep_simple_methods_in_one_line = true
ij_java_keep_blank_lines_in_code = 1
ij_java_keep_blank_lines_in_declarations = 1
ij_java_blank_lines_after_class_header = 1
ij_java_class_count_to_use_import_on_demand = 999
ij_java_names_count_to_use_import_on_demand = 999
ij_java_imports_layout = javax.**, |, java.**, |, *, |, $*
ij_java_insert_inner_class_imports = true
ij_java_space_before_array_initializer_left_brace = true
ij_java_method_parameters_new_line_after_left_paren = true
ij_java_wrap_comments = false
ij_java_wrap_long_lines = false
ij_java_enum_constants_wrap = split_into_lines
ij_java_method_call_chain_wrap = on_every_item
ij_java_method_parameters_wrap = on_every_item
ij_java_extends_list_wrap = normal
ij_java_extends_keyword_wrap = normal
ij_java_binary_operation_wrap = normal
ij_java_binary_operation_sign_on_next_line = true
ij_java_generate_final_locals = false
ij_java_generate_final_parameters = false

[*.json]
tab_width = 2
indent_size = 2

[*.{yml,yaml}]
tab_width = 2
indent_size = 2

[*.xml]
ij_xml_attribute_wrap = off
ij_xml_text_wrap = off
ij_xml_keep_blank_lines = 1
9 changes: 8 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
<spring-ai.version>1.0.0-M3</spring-ai.version>
<dashscope-sdk-java.version>2.15.1</dashscope-sdk-java.version>

<handlebars.version>4.3.1</handlebars.version>

<!-- plugin versions -->
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
<maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version>
Expand Down Expand Up @@ -116,6 +118,11 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.github.jknack</groupId>
<artifactId>handlebars</artifactId>
<version>${handlebars.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down Expand Up @@ -517,4 +524,4 @@
</repository>
</repositories>

</project>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.alibaba.cloud.ai.autoconfigure.prompt;

import com.alibaba.cloud.ai.dotprompt.*;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

@AutoConfiguration
@EnableConfigurationProperties(DotPromptProperties.class)
@ConditionalOnProperty(prefix = DotPromptProperties.PREFIX, name = "enabled", matchIfMissing = true)
public class DotPromptAutoConfiguration {

@Bean
public DotPromptLoader dotPromptLoader(DotPromptProperties properties) {
return new ClassPathDotPromptLoader(properties);
}

@Bean
public DotPromptRenderer dotPromptRenderer(DotPromptProperties properties) {
return new DotPromptRenderer(properties);
}

@Bean
public DotPromptTemplate dotPromptTemplate(DotPromptLoader loader, DotPromptRenderer renderer) {
return new DotPromptTemplate(loader, renderer);
}

@Bean
public DotPromptService dotPromptService(DotPromptLoader loader) {
return new DefaultDotPromptService(loader);
}

}
10 changes: 10 additions & 0 deletions spring-ai-alibaba-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@
<artifactId>spring-ai-core</artifactId>
</dependency>

<dependency>
<groupId>com.github.jknack</groupId>
<artifactId>handlebars</artifactId>
</dependency>

<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-retry</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.alibaba.cloud.ai.dotprompt;

import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Implementation of DotPromptLoader that loads prompts from the classpath.
*/
public class ClassPathDotPromptLoader implements DotPromptLoader {

private final PathMatchingResourcePatternResolver resourceResolver;

private final Map<String, DotPrompt> promptCache;

private final DotPromptProperties properties;

private final Yaml yaml;

private static final Pattern FRONT_MATTER_PATTERN = Pattern.compile("^---\\s*$(.+?)^---\\s*$(.+)$",
Pattern.MULTILINE | Pattern.DOTALL);

public ClassPathDotPromptLoader(DotPromptProperties properties) {
Assert.notNull(properties, "properties must not be null");
this.resourceResolver = new PathMatchingResourcePatternResolver();
this.promptCache = new HashMap<>();
this.properties = properties;
this.yaml = new Yaml();
}

@Override
public List<String> getPromptNames() throws IOException {
List<String> promptNames = new ArrayList<>();
String promptLocation = properties.getBasePath();

Assert.hasText(promptLocation, "Prompt location must not be empty");

// Ensure the location ends with /
if (!promptLocation.endsWith("/")) {
promptLocation += "/";
}

// Search for all .prompt files in the configured location
String locationPattern = "classpath*:" + promptLocation + "**/*" + properties.getFileExtension();
Resource[] resources = resourceResolver.getResources(locationPattern);

for (Resource resource : resources) {
String filename = resource.getFilename();
if (filename != null) {
// Remove the file extension
String promptName = StringUtils.stripFilenameExtension(filename);
promptNames.add(promptName);
}
}

return promptNames;
}

@Override
public DotPrompt load(String promptName) throws IOException {
Assert.hasText(promptName, "promptName must not be empty");

// Check cache first
DotPrompt cachedPrompt = promptCache.get(promptName);
if (cachedPrompt != null) {
return cachedPrompt;
}

String promptLocation = properties.getBasePath();
if (!promptLocation.endsWith("/")) {
promptLocation += "/";
}

String resourcePath = promptLocation + promptName + properties.getFileExtension();
Resource resource = resourceResolver.getResource("classpath:" + resourcePath);

if (!resource.exists()) {
throw new IOException("Prompt file not found: " + resourcePath);
}

try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {
StringBuilder content = new StringBuilder();
char[] buffer = new char[1024];
int read;
while ((read = reader.read(buffer)) != -1) {
content.append(buffer, 0, read);
}

Matcher matcher = FRONT_MATTER_PATTERN.matcher(content.toString());
if (!matcher.find()) {
throw new IOException("Invalid prompt file format. Expected front matter and template sections.");
}

String frontMatter = matcher.group(1).trim();
String template = matcher.group(2).trim();

Map<String, Object> metadata = yaml.load(frontMatter);

DotPrompt prompt = new DotPrompt();
prompt.setModel((String) metadata.get("model"));
prompt.setConfig((Map<String, Object>) metadata.get("config"));
prompt.setInput(parseInputSchema((Map<String, Object>) metadata.get("input")));
prompt.setTemplate(template);

// Cache the prompt
promptCache.put(promptName, prompt);

return prompt;
}
}

private DotPrompt.InputSchema parseInputSchema(Map<String, Object> input) {
if (input == null) {
return null;
}

DotPrompt.InputSchema schema = new DotPrompt.InputSchema();
schema.setSchema((Map<String, String>) input.get("schema"));
schema.setDefaultValues((Map<String, Object>) input.get("default"));
return schema;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.alibaba.cloud.ai.dotprompt;

import org.springframework.util.Assert;

import java.io.IOException;
import java.util.List;

/**
* Default implementation of DotPromptService.
*/
public class DefaultDotPromptService implements DotPromptService {

private final DotPromptLoader promptLoader;

public DefaultDotPromptService(DotPromptLoader promptLoader) {
Assert.notNull(promptLoader, "promptLoader must not be null");
this.promptLoader = promptLoader;
}

@Override
public List<String> getPromptNames() throws IOException {
return promptLoader.getPromptNames();
}

@Override
public DotPrompt getPrompt(String promptName) throws IOException {
Assert.hasText(promptName, "promptName must not be empty");
return promptLoader.load(promptName);
}

}
Loading