commit a2578d7ff101e02a856765c995520b28fe5864f1 Author: wehub-resource-sync Date: Mon Jul 13 13:38:45 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..c90b8cd --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,34 @@ +{ + "name": "LangChain4j", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "ghcr.io/devcontainers/features/java:1": { + "version": "25", + "jdkDistro": "tem", + "installMaven": true, + "mavenVersion": "latest" + }, + "ghcr.io/devcontainers/features/azure-cli:1": {}, + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/github-cli:1": {} + }, + + "customizations": { + "vscode": { + "settings": { + "java.configuration.updateBuildConfiguration": "automatic", + "java.compile.nullAnalysis.mode": "automatic" + }, + "extensions": [ + "redhat.vscode-xml", + "redhat.vscode-yaml", + "visualstudioexptteam.vscodeintellicode", + "vscjava.vscode-java-pack", + "editorconfig.editorconfig" + ] + } + }, + + "postCreateCommand": "java -version && mvn -version", + "remoteUser": "vscode" +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9da7cf7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,942 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on + +[*.java] +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_deconstruction_list_components = true +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = true +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = true +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_records = true +ij_java_align_multiline_resources = true +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_align_types_in_multi_catch = true +ij_java_annotation_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = off +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = false +ij_java_binary_operation_wrap = off +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 0 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_field_with_annotations = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 0 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_add_space = false +ij_java_block_comment_at_first_column = true +ij_java_builder_methods = +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = off +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 999 +ij_java_class_names_in_javadoc = 1 +ij_java_deconstruction_list_wrap = normal +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_not_wrap_after_single_annotation_in_parameter = false +ij_java_do_while_brace_force = never +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_enum_constants_wrap = off +ij_java_enum_field_annotation_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +ij_java_field_annotation_wrap = split_into_lines +ij_java_field_name_prefix = +ij_java_field_name_suffix = +ij_java_filter_class_prefix = +ij_java_filter_class_suffix = +ij_java_filter_dd_prefix = +ij_java_filter_dd_suffix = +ij_java_finally_on_new_line = false +ij_java_for_brace_force = never +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = off +ij_java_generate_final_locals = true +ij_java_generate_final_parameters = true +ij_java_generate_use_type_annotation_before_type = true +ij_java_if_brace_force = never +ij_java_imports_layout = $*,|,javax.**,java.**,* +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = false +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 2 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 2 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_builder_methods_indents = false +ij_java_keep_control_statement_in_one_line = true +ij_java_keep_first_column_comment = true +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_add_space_on_reformat = false +ij_java_line_comment_at_first_column = true +ij_java_listener_class_prefix = +ij_java_listener_class_suffix = +ij_java_local_variable_name_prefix = +ij_java_local_variable_name_suffix = +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = off +ij_java_method_parameters_new_line_after_left_paren = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = off +ij_java_modifier_list_wrap = false +ij_java_multi_catch_types_wrap = normal +ij_java_names_count_to_use_import_on_demand = 999 +ij_java_new_line_after_lparen_in_annotation = false +ij_java_new_line_after_lparen_in_deconstruction_pattern = true +ij_java_new_line_after_lparen_in_record_header = false +ij_java_new_line_when_body_is_presented = false +ij_java_packages_to_use_import_on_demand = java.awt.*, javax.swing.* +ij_java_parameter_annotation_wrap = off +ij_java_parameter_name_prefix = +ij_java_parameter_name_suffix = +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_record_components_wrap = normal +ij_java_repeat_annotations = +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_rparen_on_new_line_in_annotation = false +ij_java_rparen_on_new_line_in_deconstruction_pattern = true +ij_java_rparen_on_new_line_in_record_header = false +ij_java_servlet_class_prefix = +ij_java_servlet_class_suffix = +ij_java_servlet_dd_prefix = +ij_java_servlet_dd_suffix = +ij_java_session_dd_prefix = +ij_java_session_dd_suffix = EJB +ij_java_session_eb_prefix = +ij_java_session_eb_suffix = Bean +ij_java_session_hi_prefix = +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_li_suffix = +ij_java_session_ri_prefix = +ij_java_session_ri_suffix = +ij_java_session_si_prefix = +ij_java_session_si_suffix = Service +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = false +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_deconstruction_list = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_annotation_eq = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_inside_block_braces_when_body_is_present = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_deconstruction_list = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_record_header = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_static_field_name_prefix = +ij_java_static_field_name_suffix = +ij_java_subclass_name_prefix = +ij_java_subclass_name_suffix = Impl +ij_java_switch_expressions_wrap = normal +ij_java_ternary_operation_signs_on_next_line = false +ij_java_ternary_operation_wrap = off +ij_java_test_name_prefix = +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = off +ij_java_throws_list_wrap = off +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = off +ij_java_visibility = public +ij_java_while_brace_force = never +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false +ij_java_wrap_semicolon_after_call_chain = false + +[*.less] +indent_size = 2 +ij_less_align_closing_brace_with_properties = false +ij_less_blank_lines_around_nested_selector = 1 +ij_less_blank_lines_between_blocks = 1 +ij_less_block_comment_add_space = false +ij_less_brace_placement = 0 +ij_less_enforce_quotes_on_format = false +ij_less_hex_color_long_format = false +ij_less_hex_color_lower_case = false +ij_less_hex_color_short_format = false +ij_less_hex_color_upper_case = false +ij_less_keep_blank_lines_in_code = 2 +ij_less_keep_indents_on_empty_lines = false +ij_less_keep_single_line_blocks = false +ij_less_line_comment_add_space = false +ij_less_line_comment_at_first_column = false +ij_less_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_less_space_after_colon = true +ij_less_space_before_opening_brace = true +ij_less_use_double_quotes = true +ij_less_value_alignment = 0 + +[*.sass] +indent_size = 2 +ij_sass_align_closing_brace_with_properties = false +ij_sass_blank_lines_around_nested_selector = 1 +ij_sass_blank_lines_between_blocks = 1 +ij_sass_brace_placement = 0 +ij_sass_enforce_quotes_on_format = false +ij_sass_hex_color_long_format = false +ij_sass_hex_color_lower_case = false +ij_sass_hex_color_short_format = false +ij_sass_hex_color_upper_case = false +ij_sass_keep_blank_lines_in_code = 2 +ij_sass_keep_indents_on_empty_lines = false +ij_sass_keep_single_line_blocks = false +ij_sass_line_comment_add_space = false +ij_sass_line_comment_at_first_column = false +ij_sass_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_sass_space_after_colon = true +ij_sass_space_before_opening_brace = true +ij_sass_use_double_quotes = true +ij_sass_value_alignment = 0 + +[*.scss] +indent_size = 2 +ij_scss_align_closing_brace_with_properties = false +ij_scss_blank_lines_around_nested_selector = 1 +ij_scss_blank_lines_between_blocks = 1 +ij_scss_block_comment_add_space = false +ij_scss_brace_placement = 0 +ij_scss_enforce_quotes_on_format = false +ij_scss_hex_color_long_format = false +ij_scss_hex_color_lower_case = false +ij_scss_hex_color_short_format = false +ij_scss_hex_color_upper_case = false +ij_scss_keep_blank_lines_in_code = 2 +ij_scss_keep_indents_on_empty_lines = false +ij_scss_keep_single_line_blocks = false +ij_scss_line_comment_add_space = false +ij_scss_line_comment_at_first_column = false +ij_scss_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_scss_space_after_colon = true +ij_scss_space_before_opening_brace = true +ij_scss_use_double_quotes = true +ij_scss_value_alignment = 0 + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.ant,*.fxml,*.gpx,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.sdef,*.tld,*.wadl,*.wsdd,*.wsdl,*.xcscheme,*.xjb,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +ij_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_add_space = false +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal +ij_xml_use_custom_settings = false + +[{*.ats,*.cts,*.mts,*.ts}] +ij_continuation_indent_size = 4 +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = true +ij_typescript_align_multiline_parameters = true +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_block_comment_add_space = false +ij_typescript_block_comment_at_first_column = true +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = off +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = never +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = keep +ij_typescript_enum_constants_wrap = on_every_item +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = never +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = off +ij_typescript_force_quote_style = false +ij_typescript_force_semicolon_style = false +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_if_brace_force = never +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 2 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = false +ij_typescript_keep_simple_methods_in_one_line = false +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = off +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = off +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_object_types_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_property_prefix = +ij_typescript_reformat_c_style_comments = false +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = false +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = false +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = false +ij_typescript_ternary_operation_wrap = off +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = true +ij_typescript_use_explicit_js_extension = auto +ij_typescript_use_import_type = auto +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = normal +ij_typescript_while_brace_force = never +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false +ij_shell_use_unix_line_separator = true + +[{*.cjs,*.js}] +ij_continuation_indent_size = 4 +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = off +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_block_comment_add_space = false +ij_javascript_block_comment_at_first_column = true +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = off +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = never +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = never +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_if_brace_force = never +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = off +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_object_types_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_property_prefix = +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = off +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = auto +ij_javascript_use_import_type = auto +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = never +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + + +[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,jest.config}] +indent_size = 2 +ij_json_array_wrapping = split_into_lines +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_keep_trailing_comma = false +ij_json_object_wrapping = split_into_lines +ij_json_property_alignment = do_not_align +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = false +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.htm,*.html,*.sht,*.shtm,*.shtml}] +ij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = normal +ij_html_block_comment_add_space = false +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title, h1, h2, h3, h4, h5, h6, p +ij_html_do_not_indent_children_of_tags = html, body, thead, tbody, tfoot +ij_html_enforce_quotes = false +ij_html_inline_tags = a, abbr, acronym, b, basefont, bdo, big, br, cite, cite, code, dfn, em, font, i, img, input, kbd, label, q, s, samp, select, small, span, strike, strong, sub, sup, textarea, tt, u, var +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span, pre, textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = normal + +[{*.http,*.rest}] +indent_size = 0 +ij_continuation_indent_size = 4 +ij_http-request_call_parameters_wrap = normal +ij_http-request_method_parameters_wrap = split_into_lines +ij_http-request_space_before_comma = true +ij_http-request_spaces_around_assignment_operators = true + +[{*.kt,*.kts}] +ij_kotlin_align_in_columns_case_branch = false +ij_kotlin_align_multiline_binary_operation = false +ij_kotlin_align_multiline_extends_list = false +ij_kotlin_align_multiline_method_parentheses = false +ij_kotlin_align_multiline_parameters = true +ij_kotlin_align_multiline_parameters_in_calls = false +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_assignment_wrap = normal +ij_kotlin_blank_lines_after_class_header = 0 +ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 +ij_kotlin_block_comment_add_space = false +ij_kotlin_block_comment_at_first_column = true +ij_kotlin_call_parameters_new_line_after_left_paren = true +ij_kotlin_call_parameters_right_paren_on_new_line = true +ij_kotlin_call_parameters_wrap = on_every_item +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL +ij_kotlin_continuation_indent_for_chained_calls = false +ij_kotlin_continuation_indent_for_expression_bodies = false +ij_kotlin_continuation_indent_in_argument_lists = false +ij_kotlin_continuation_indent_in_elvis = false +ij_kotlin_continuation_indent_in_if_conditions = false +ij_kotlin_continuation_indent_in_parameter_lists = false +ij_kotlin_continuation_indent_in_supertype_lists = false +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = normal +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_if_rparen_on_new_line = true +ij_kotlin_import_nested_classes = false +ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^ +ij_kotlin_indent_before_arrow_on_new_line = true +ij_kotlin_insert_whitespaces_in_simple_one_line_method = true +ij_kotlin_keep_blank_lines_before_right_brace = 2 +ij_kotlin_keep_blank_lines_in_code = 2 +ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_first_column_comment = true +ij_kotlin_keep_indents_on_empty_lines = false +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_break_after_multiline_when_entry = true +ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_add_space_on_reformat = false +ij_kotlin_line_comment_at_first_column = true +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_method_call_chain_wrap = normal +ij_kotlin_method_parameters_new_line_after_left_paren = true +ij_kotlin_method_parameters_right_paren_on_new_line = true +ij_kotlin_method_parameters_wrap = on_every_item +ij_kotlin_name_count_to_use_star_import = 999 +ij_kotlin_name_count_to_use_star_import_for_members = 999 +ij_kotlin_packages_to_use_import_on_demand = unset +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_when_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_range = false +ij_kotlin_spaces_around_relational_operators = true +ij_kotlin_spaces_around_unary_operator = false +ij_kotlin_spaces_around_when_arrow = true +ij_kotlin_use_custom_formatting_for_modifiers = true +ij_kotlin_variable_annotation_wrap = off +ij_kotlin_while_on_new_line = false +ij_kotlin_wrap_elvis_expressions = 1 +ij_kotlin_wrap_expression_body_functions = 1 +ij_kotlin_wrap_first_method_in_call_chain = false +ktlint_standard = enabled + +[{*Test.kt,*IT.kt}] +ktlint_standard_ignore_back_ticked_identifier = true +ktlint_standard_function-naming = disabled + +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_format_tables = true +ij_markdown_insert_quote_arrows_on_wrap = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_keep_line_breaks_inside_text_blocks = true +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 +ij_markdown_wrap_text_if_long = true +ij_markdown_wrap_text_inside_blockquotes = true + +[{*.properties,spring.handlers,spring.schemas}] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] +ij_toml_keep_indents_on_empty_lines = false + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_line_comment_add_space = false +ij_yaml_line_comment_add_space_on_reformat = false +ij_yaml_line_comment_at_first_column = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0bb75f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.onnx filter=lfs diff=lfs merge=lfs -text diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..8f950f0 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: langchain4j +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..14fb8ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG] " +labels: bug +assignees: '' + +--- + + + +**Describe the bug** + + +**Log and Stack trace** + + +**To Reproduce** + + +**Expected behavior** + + +**Please complete the following information:** +- LangChain4j version: +- LLM(s) used: +- Java version: +- Spring Boot version (if applicable): + +**Additional context** + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..79681ee --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[FEATURE] " +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** + + +**Describe the solution you'd like** + + +**Describe alternatives you've considered** + + +**Additional context** + diff --git a/.github/langchain4j-github-bot.yml b/.github/langchain4j-github-bot.yml new file mode 100644 index 0000000..0e8e536 --- /dev/null +++ b/.github/langchain4j-github-bot.yml @@ -0,0 +1,42 @@ +--- +# The format of this file is documented here: +# https://github.com/langchain4j/langchain4j-github-bot +features: [TRIAGE_ISSUES_AND_PULL_REQUESTS] +triage: + rules: + - id: azure + labels: [Azure] + title: "azure" + notify: [jdubois, agoncal] + notifyInPullRequest: true + files: + - '*-azure-*/**' + - id: gemini + labels: [Gemini] + title: "gemini|vertex|google" + notify: [glaforge] + notifyInPullRequest: true + files: + - '*-vertex-*/**' + - '*-google-*/**' + - id: elasticsearch + labels: [Elasticsearch] + title: "elasticsearch" + notify: [dadoonet] + notifyInPullRequest: true + files: + - 'langchain4j-elasticsearch/**' + - id: mistral + labels: [Mistral] + title: "mistral" + notify: [czelabueno] + notifyInPullRequest: true + files: + - 'langchain4j-mistral-ai/**' + - id: mcp + labels: [MCP] + title: "mcp" + notify: [jmartisk] + notifyInPullRequest: true + files: + - 'langchain4j-mcp/**' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..008d685 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,45 @@ + + +## Issue + +Closes # + +## Change + + + +## General checklist + +- [ ] There are no breaking changes (API, behaviour) +- [ ] I have added unit and/or integration tests for my change +- [ ] The tests cover both positive and negative cases +- [ ] I have manually run all the unit and integration tests in the module I have added/changed, and they are all green +- [ ] I have manually run all the unit and integration tests in the [core](https://github.com/langchain4j/langchain4j/tree/main/langchain4j-core) and [main](https://github.com/langchain4j/langchain4j/tree/main/langchain4j) modules, and they are all green +- [ ] I have added/updated the [documentation](https://github.com/langchain4j/langchain4j/tree/main/docs/docs) +- [ ] I have added an example in the [examples repo](https://github.com/langchain4j/langchain4j-examples) (only for "big" features) +- [ ] I have added/updated [Spring Boot starter(s)](https://github.com/langchain4j/langchain4j-spring) (if applicable) + + +## Checklist for adding new maven module + +- [ ] I have added my new module in the root `pom.xml` and `langchain4j-bom/pom.xml` + + +## Checklist for adding new embedding store integration + +- [ ] I have added a `{NameOfIntegration}EmbeddingStoreIT` that extends from either `EmbeddingStoreIT` or `EmbeddingStoreWithFilteringIT` +- [ ] I have added a `{NameOfIntegration}EmbeddingStoreRemovalIT` that extends from `EmbeddingStoreWithRemovalIT` + +## Checklist for changing existing embedding store integration + +- [ ] I have manually verified that the `{NameOfIntegration}EmbeddingStore` works correctly with the data persisted using the latest released version of LangChain4j diff --git a/.github/workflows/docs-update-chatbot.yaml b/.github/workflows/docs-update-chatbot.yaml new file mode 100644 index 0000000..f2d0a57 --- /dev/null +++ b/.github/workflows/docs-update-chatbot.yaml @@ -0,0 +1,26 @@ +name: "Documentation: update chatbot" + +on: + repository_dispatch: + types: [ trigger-docs-update-chatbot ] + workflow_dispatch: + +permissions: + contents: read + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + distribution: temurin + java-version: 21 + - name: Build and run + run: | + mvn -pl internal/langchain4j-docu-chatbot-updater clean compile exec:java + env: + GOOGLE_AI_GEMINI_API_KEY: ${{ secrets.GOOGLE_AI_GEMINI_API_KEY }} + MILVUS_API_KEY: ${{ secrets.MILVUS_API_KEY }} + MILVUS_URI: ${{ secrets.MILVUS_URI }} diff --git a/.github/workflows/docs-update-version.yaml b/.github/workflows/docs-update-version.yaml new file mode 100644 index 0000000..2a7f9cc --- /dev/null +++ b/.github/workflows/docs-update-version.yaml @@ -0,0 +1,91 @@ +name: "Documentation: update versions" + +on: + repository_dispatch: + types: [ trigger-docs-update-version ] + workflow_dispatch: + inputs: + stableVersion: + description: "Stable release version (e.g., 1.8.0)" + required: true + betaVersion: + description: "Beta release version (e.g., 1.8.0-beta15)" + required: true + +env: + STABLE_VERSION: ${{ github.event.inputs.stableVersion || github.event.client_payload.stableVersion }} + BETA_VERSION: ${{ github.event.inputs.betaVersion || github.event.client_payload.betaVersion }} + +permissions: + contents: write + +jobs: + update-docs: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + token: ${{ secrets.GH_RELEASE_AUTOMATION }} + + - name: Extract current stable and beta versions from docs metadata + id: extract-current-versions + run: | + STABLE_CURRENT=$(grep -E "^stableVersion:" docs/docs/get-started.md | sed 's/stableVersion: *//') + BETA_CURRENT=$(grep -E "^betaVersion:" docs/docs/get-started.md | sed 's/betaVersion: *//') + echo "stableCurrent=$STABLE_CURRENT" >> $GITHUB_OUTPUT + echo "betaCurrent=$BETA_CURRENT" >> $GITHUB_OUTPUT + echo "Found stable version in docs: $STABLE_CURRENT" + echo "Found beta version in docs: $BETA_CURRENT" + + - name: Replace versions in docs recursively + run: | + echo "Replacing all occurrences of stable and beta versions recursively under docs/docs/..." + + # Replace beta versions + grep -rl "${{ steps.extract-current-versions.outputs.betaCurrent }}" docs/docs | \ + xargs sed -i "s/${{ steps.extract-current-versions.outputs.betaCurrent }}/$BETA_VERSION/g" || true + + # Replace stable versions + grep -rl "${{ steps.extract-current-versions.outputs.stableCurrent }}" docs/docs | \ + xargs sed -i "s/${{ steps.extract-current-versions.outputs.stableCurrent }}/$STABLE_VERSION/g" || true + + - name: Update metadata in get-started.md + run: | + sed -i "s/betaVersion: .*/betaVersion: $BETA_VERSION/" docs/docs/get-started.md + sed -i "s/stableVersion: .*/stableVersion: $STABLE_VERSION/" docs/docs/get-started.md + + - name: Check if any changes were made + id: git-check + run: | + if git diff --quiet; then + echo "changed=false" >> $GITHUB_OUTPUT + else + echo "changed=true" >> $GITHUB_OUTPUT + fi + + - name: Commit and push changes + if: steps.git-check.outputs.changed == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add docs/docs + git commit -m "docu: update versions to $STABLE_VERSION and $BETA_VERSION" + git push origin main + + - name: Trigger documentation build and deploy + if: steps.git-check.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api repos/langchain4j/langchain4j/dispatches \ + -f event_type=trigger-docs-build-and-deploy + + - name: Trigger documentation chatbot update + if: steps.git-check.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api repos/langchain4j/langchain4j/dispatches \ + -f event_type=trigger-docs-update-chatbot diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000..0e58cdd --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,57 @@ +name: "Documentation: build and deploy" + +on: + push: + branches: + - main + paths: + - 'docs/**' + repository_dispatch: + types: [ trigger-docs-build-and-deploy ] + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +jobs: + deploy: + runs-on: ubuntu-latest + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - name: Set up JDK 25 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: '25' + distribution: 'temurin' + cache: 'maven' + + - name: Generate Javadoc + run: mvn javadoc:aggregate -DskipTests + + - name: Set up Node.js + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 + with: + node-version: '20.x' + + - run: cd docs && npm ci + - run: cd docs && npm run build + - run: cp -R target/reports/apidocs docs/build/ + + - name: Setup Pages + uses: actions/configure-pages@1f0c5cde4bc74cd7e1254d0cb4de8d49e9068c7d # v4 + - name: Upload artifact + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 + with: + # Upload entire repository + path: './docs/build/.' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/.github/workflows/langchain4j-github-bot-lint.yml b/.github/workflows/langchain4j-github-bot-lint.yml new file mode 100644 index 0000000..74ffe99 --- /dev/null +++ b/.github/workflows/langchain4j-github-bot-lint.yml @@ -0,0 +1,25 @@ +name: langchain4j-github-bot.yml lint + +on: + push: + paths: + - '.github/langchain4j-github-bot.yml' + pull_request: + paths: + - '.github/langchain4j-github-bot.yml' + +permissions: + contents: read + +jobs: + lint: + name: "langchain4j-github-bot.yml validation" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - name: install yamllint + shell: bash + run: sudo apt-get install -y yamllint + - name: run yamllint for langchain4j-github-bot.yml + shell: bash + run: yamllint -c .github/yamllint.conf .github/langchain4j-github-bot.yml diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..4aa3c3e --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,379 @@ +name: Java CI + +on: + push: + branches: + - main + - 'release/**' + paths-ignore: + - '.gitignore' + - '*.md' + - 'LICENSE' + - '.github/*.md' + - '.github/*.yml' + - '.github/*.conf' + - '.github/ISSUE_TEMPLATE/*.md' + pull_request: + branches: + - main + - 'release/**' + paths-ignore: + - '.gitignore' + - '*.md' + - 'LICENSE' + - '.github/*.md' + - '.github/*.yml' + - '.github/*.conf' + - '.github/ISSUE_TEMPLATE/*.md' + workflow_dispatch: + +permissions: + contents: read + +jobs: + + compile_and_unit_test: + runs-on: ubuntu-latest + strategy: + matrix: + java_version: + - 17 + - 21 + - 25 + steps: + + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + fetch-depth: 0 + + - name: Set up JDK ${{ matrix.java_version }} + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: ${{ matrix.java_version }} + distribution: 'temurin' + cache: 'maven' + + - name: Compile and unit test with JDK ${{ matrix.java_version }} + run: | + set -o pipefail + mvn -B -U -DembeddingsSkipDownload -T8C test javadoc:aggregate 2>&1 | tee maven-output.log + + - name: Publish test summary + if: always() + env: + SUMMARY_TITLE: 'Test Results (JDK ${{ matrix.java_version }})' + run: | + python3 << 'PYEOF' + import xml.etree.ElementTree as ET + import glob, os, re + + files = glob.glob('**/target/*-reports/TEST-*.xml', recursive=True) + if not files: + raise SystemExit(0) # e.g. compilation failed before any test ran + + total = failures = errors = skipped = 0 + failed_tests = [] + + for path in files: + try: + root = ET.parse(path).getroot() + total += int(root.get('tests', 0)) + failures += int(root.get('failures', 0)) + errors += int(root.get('errors', 0)) + skipped += int(root.get('skipped', 0)) + for tc in root.findall('.//testcase'): + fail = tc.find('failure') + if fail is None: + fail = tc.find('error') + if fail is not None: + cls = tc.get('classname', '') + name = tc.get('name', '') + trace = fail.text or '' + m = re.search(r'\bat ' + re.escape(cls) + r'[.$][^\n(]*\(([^)]+\.java:\d+)\)', trace) + loc = f'`{m.group(1)}`' if m else '' + msg = (fail.get('message', '') or '').replace('\n', '
').replace('|', '\\|') + if len(msg) > 500: + msg = f"{msg[:500]}
show more{msg[500:]}
" + failed_tests.append(f"| `{cls}` | `{name}` | {loc} | {msg} |") + except Exception: + pass + + passed = total - failures - errors - skipped + title = os.environ.get('SUMMARY_TITLE', 'Test Results') + summary = os.environ.get('GITHUB_STEP_SUMMARY', '/dev/null') + with open(summary, 'a') as f: + f.write(f'## {title}\n\n') + if failures == 0 and errors == 0: + f.write(f':white_check_mark: **{passed} passed**, {skipped} skipped out of {total} tests\n') + else: + f.write(f':x: **{failures} failed, {errors} errors**, {passed} passed, {skipped} skipped out of {total} tests\n') + if failed_tests: + f.write('\n### Failures\n\n') + f.write('| Class | Test | Location | Error Message |\n') + f.write('|-------|------|----------|---------------|\n') + for line in failed_tests[:50]: + f.write(line + '\n') + if len(failed_tests) > 50: + f.write(f'\n*... and {len(failed_tests) - 50} more*\n') + PYEOF + + - name: Surface build errors + if: failure() + run: | + { + echo '## Build Errors (JDK ${{ matrix.java_version }})' + echo '' + echo '```' + # Only Maven's own errors (compilation, javadoc, plugins). Exclude forked + # test-JVM output that Surefire re-emits with an [ERROR] prefix on channel + # corruption (e.g. mock-server stub dumps) — that is test noise, not a build error. + grep '^\[ERROR\]' maven-output.log \ + | grep -vE '^\[ERROR\]( +"|[[:space:]]*(Stub|DEBUG:)|[[:space:]]*$)' \ + | head -50 + echo '```' + } >> $GITHUB_STEP_SUMMARY + + integration_test: + needs: compile_and_unit_test + runs-on: ubuntu-latest + continue-on-error: true + env: + GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }} + strategy: + fail-fast: false + matrix: + java_version: + - 21 + steps: + + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + fetch-depth: 0 + + - name: Create branch from commit by event name + env: + EVENT_NAME: ${{ github.event_name }} + BEFORE_SHA: ${{ github.event.before }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + if [[ "$EVENT_NAME" == 'push' ]]; then + git branch __branch_before "$BEFORE_SHA" + elif [[ "$EVENT_NAME" == 'pull_request' ]]; then + git branch __branch_before "$PR_BASE_SHA" + fi + + - name: Set up JDK ${{ matrix.java_version }} + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: ${{ matrix.java_version }} + distribution: 'temurin' + cache: 'maven' + + - name: Authenticate to Google Cloud + # Required for Google modules (e.g., langchain4j-vertex-ai) + # Skipped for PRs from forks where secrets are not available + if: ${{ env.GCP_CREDENTIALS_JSON != '' }} + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3 + with: + project_id: ${{ secrets.GCP_PROJECT_ID }} + credentials_json: ${{ secrets.GCP_CREDENTIALS_JSON }} + + - name: Setup JBang + # Required for MCP module + uses: jbangdev/setup-jbang@2b1b465a7b75f4222b81426f23a01e013aa7b95c # v0.1.1 + continue-on-error: true + + - name: Integration test with JDK ${{ matrix.java_version }} + run: | + set -o pipefail + + ## Compile and install ALL modules to ensure that modules selected by GIB in the next step + ## reference the latest code (e.g., langchain4j-core and langchain4j) + mvn -B -U -T8C -DskipTests -DskipITs -DembeddingsSkipCache install 2>&1 | tee maven-output.log + + mvn -B -U verify \ + -Dgib.disable=false -Dgib.referenceBranch=__branch_before \ + -Djunit.jupiter.extensions.autodetection.enabled=true \ + -DskipAzureAiSearchITs -DskipJlamaITs -DskipLocalAiITs -DskipMilvusITs -DskipOllamaITs -DskipOracleITs -DskipVespaITs \ + -DembeddingsSkipCache \ + -Dtinylog.writer.level=info 2>&1 | tee maven-output.log + env: + LC4J_GLOBAL_TEST_RETRY_ENABLED: true + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + ANTHROPIC_CACHING_BASE_URL: ${{ secrets.ANTHROPIC_CACHING_BASE_URL }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AZURE_OPENAI_AUDIO_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_AUDIO_DEPLOYMENT_NAME }} + AZURE_OPENAI_AUDIO_ENDPOINT: ${{ secrets.AZURE_OPENAI_AUDIO_ENDPOINT }} + AZURE_OPENAI_AUDIO_KEY: ${{ secrets.AZURE_OPENAI_AUDIO_KEY }} + AZURE_OPENAI_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_DEPLOYMENT_NAME }} + AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} + AZURE_OPENAI_KEY: ${{ secrets.AZURE_OPENAI_KEY }} + AZURE_SEARCH_ENDPOINT: ${{ secrets.AZURE_SEARCH_ENDPOINT }} + AZURE_SEARCH_KEY: ${{ secrets.AZURE_SEARCH_KEY }} + COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} + DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} + ELASTICSEARCH_CLOUD_API_KEY: ${{ secrets.ELASTICSEARCH_CLOUD_API_KEY }} + ELASTICSEARCH_CLOUD_URL: ${{ secrets.ELASTICSEARCH_CLOUD_URL }} + GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }} + GCP_LOCATION: ${{ secrets.GCP_LOCATION }} + GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} + GCP_PROJECT_NUM: ${{ secrets.GCP_PROJECT_NUM }} + GCP_VERTEXAI_ENDPOINT: ${{ secrets.GCP_VERTEXAI_ENDPOINT }} + GOOGLE_AI_GEMINI_API_KEY: ${{ secrets.GOOGLE_AI_GEMINI_API_KEY }} + HF_API_KEY: ${{ secrets.HF_API_KEY }} + JINA_API_KEY: ${{ secrets.JINA_API_KEY }} + MICROSOFT_FOUNDRY_API_KEY: ${{ secrets.MICROSOFT_FOUNDRY_API_KEY }} + MICROSOFT_FOUNDRY_ENDPOINT: ${{ secrets.MICROSOFT_FOUNDRY_ENDPOINT }} + MILVUS_API_KEY: ${{ secrets.MILVUS_API_KEY }} + MILVUS_URI: ${{ secrets.MILVUS_URI }} + MISTRAL_AI_API_KEY: ${{ secrets.MISTRAL_AI_API_KEY }} + NOMIC_API_KEY: ${{ secrets.NOMIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} + PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }} + RAPID_API_KEY: ${{ secrets.RAPID_API_KEY }} + TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} + VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} + WEAVIATE_API_KEY: ${{ secrets.WEAVIATE_API_KEY }} + WEAVIATE_HOST: ${{ secrets.WEAVIATE_HOST }} + + - name: Publish test summary + if: always() + env: + SUMMARY_TITLE: 'Test Results (Integration Test, JDK ${{ matrix.java_version }})' + run: | + python3 << 'PYEOF' + import xml.etree.ElementTree as ET + import glob, os, re + + files = glob.glob('**/target/*-reports/TEST-*.xml', recursive=True) + if not files: + raise SystemExit(0) # e.g. compilation failed before any test ran + + total = failures = errors = skipped = 0 + failed_tests = [] + + for path in files: + try: + root = ET.parse(path).getroot() + total += int(root.get('tests', 0)) + failures += int(root.get('failures', 0)) + errors += int(root.get('errors', 0)) + skipped += int(root.get('skipped', 0)) + for tc in root.findall('.//testcase'): + fail = tc.find('failure') + if fail is None: + fail = tc.find('error') + if fail is not None: + cls = tc.get('classname', '') + name = tc.get('name', '') + trace = fail.text or '' + m = re.search(r'\bat ' + re.escape(cls) + r'[.$][^\n(]*\(([^)]+\.java:\d+)\)', trace) + loc = f'`{m.group(1)}`' if m else '' + msg = (fail.get('message', '') or '').replace('\n', '
').replace('|', '\\|') + if len(msg) > 500: + msg = f"{msg[:500]}
show more{msg[500:]}
" + failed_tests.append(f"| `{cls}` | `{name}` | {loc} | {msg} |") + except Exception: + pass + + passed = total - failures - errors - skipped + title = os.environ.get('SUMMARY_TITLE', 'Test Results') + summary = os.environ.get('GITHUB_STEP_SUMMARY', '/dev/null') + with open(summary, 'a') as f: + f.write(f'## {title}\n\n') + if failures == 0 and errors == 0: + f.write(f':white_check_mark: **{passed} passed**, {skipped} skipped out of {total} tests\n') + else: + f.write(f':x: **{failures} failed, {errors} errors**, {passed} passed, {skipped} skipped out of {total} tests\n') + if failed_tests: + f.write('\n### Failures\n\n') + f.write('| Class | Test | Location | Error Message |\n') + f.write('|-------|------|----------|---------------|\n') + for line in failed_tests[:50]: + f.write(line + '\n') + if len(failed_tests) > 50: + f.write(f'\n*... and {len(failed_tests) - 50} more*\n') + PYEOF + + - name: Surface build errors + if: failure() + run: | + { + echo '## Build Errors (Integration Test, JDK ${{ matrix.java_version }})' + echo '' + echo '```' + # Only Maven's own errors (compilation, javadoc, plugins). Exclude forked + # test-JVM output that Surefire re-emits with an [ERROR] prefix on channel + # corruption (e.g. mock-server stub dumps) — that is test noise, not a build error. + grep '^\[ERROR\]' maven-output.log \ + | grep -vE '^\[ERROR\]( +"|[[:space:]]*(Stub|DEBUG:)|[[:space:]]*$)' \ + | head -50 + echo '```' + } >> $GITHUB_STEP_SUMMARY + + - name: Clean Docker + run: docker system prune -af || true + + # For checking some compliance things (require a recent JDK due to plugins so in a separate step) + compliance: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - name: Set up JDK 17 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: '17' + distribution: 'temurin' + cache: 'maven' + # Check we only rely on permissive licenses in the main parts of the library: + - name: License Compliance + run: | + set -o pipefail + mvn -P compliance org.honton.chas:license-maven-plugin:compliance 2>&1 | tee maven-output.log + + - name: Surface build errors + if: failure() + run: | + { + echo '## Build Errors (License Compliance)' + echo '' + echo '```' + # Exclude forked-JVM output re-emitted with an [ERROR] prefix (test noise). + grep '^\[ERROR\]' maven-output.log \ + | grep -vE '^\[ERROR\]( +"|[[:space:]]*(Stub|DEBUG:)|[[:space:]]*$)' \ + | head -50 + echo '```' + } >> $GITHUB_STEP_SUMMARY + + spotless: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + fetch-depth: 0 + - name: Set up JDK 17 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: '17' + distribution: 'temurin' + cache: 'maven' + - name: Spotless Check + run: | + set -o pipefail + mvn -Pspotless validate 2>&1 | tee maven-output.log + + - name: Surface build errors + if: failure() + run: | + { + echo '## Build Errors (Spotless)' + echo '' + echo '```' + # Exclude forked-JVM output re-emitted with an [ERROR] prefix (test noise). + grep '^\[ERROR\]' maven-output.log \ + | grep -vE '^\[ERROR\]( +"|[[:space:]]*(Stub|DEBUG:)|[[:space:]]*$)' \ + | head -50 + echo '```' + } >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/nightly_jdk17.yaml b/.github/workflows/nightly_jdk17.yaml new file mode 100644 index 0000000..7864bb6 --- /dev/null +++ b/.github/workflows/nightly_jdk17.yaml @@ -0,0 +1,226 @@ +name: Nightly Build JDK 17 + +on: + schedule: + - cron: '0 22 * * *' # daily at 22:00 UTC + workflow_dispatch: + +permissions: + contents: read + +jobs: + discover-modules: + if: github.repository == 'langchain4j/langchain4j' + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - id: set-matrix + shell: bash + run: | + echo "Discovering Maven modules..." + modules=$(find . -type f -name pom.xml -exec dirname {} \; | sort | uniq | sed 's|^\./||' | grep -v '^$' | grep -v '^.$') + echo "Detected modules:" + echo "$modules" + + # number of parallel groups + N=7 + + # round-robin grouping + matrix=$(echo "$modules" | awk -v N="$N" ' + { + g = (NR - 1) % N + arr[g] = (arr[g] ? arr[g] "," "\"" $0 "\"" : "\"" $0 "\"") + } + END { + printf("{\"modules\": ["); + for (i = 0; i < N; i++) { + if (i > 0) printf(","); + printf("[%s]", arr[i]) + } + print "]}" + } + ') + + echo "Generated matrix: $matrix" + echo "matrix=$matrix" >> $GITHUB_OUTPUT + + build: + needs: discover-modules + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.discover-modules.outputs.matrix) }} + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - name: Set up JDK 17 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: 17 + distribution: 'temurin' + cache: 'maven' + + - name: Authenticate to Google Cloud + # Required for Google modules (e.g., langchain4j-vertex-ai) + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3 + with: + project_id: ${{ secrets.GCP_PROJECT_ID }} + credentials_json: ${{ secrets.GCP_CREDENTIALS_JSON }} + + - name: Setup JBang + # Required for MCP module + uses: jbangdev/setup-jbang@2b1b465a7b75f4222b81426f23a01e013aa7b95c # v0.1.1 + continue-on-error: true + + - name: Build with JDK 17 + run: | + modules="${{ join(matrix.modules, ',') }}" + exclude_modules="langchain4j-jlama,langchain4j-gpu-llama3" + for ex in ${exclude_modules//,/ }; do + modules=$(echo "$modules" | sed "s/\b${ex}\b//g" | sed 's/,,/,/g' | sed 's/^,//' | sed 's/,$//') + done + echo "Building modules: $modules" + + ## compile and install ALL modules to avoid running integration tests on dependent modules in the step below + mvn -B -U -T8C -DskipTests -DskipITs -DembeddingsSkipCache install + + mvn -B -U verify \ + -pl "${modules}" \ + -DskipAzureAiSearchITs -DskipJlamaITs -DskipLocalAiITs -DskipMilvusITs -DskipOllamaITs -DskipOracleITs -DskipVespaITs \ + --fail-at-end \ + -Djunit.jupiter.extensions.autodetection.enabled=true \ + -DembeddingsSkipCache \ + -Dtinylog.writer.level=info + + env: + LC4J_GLOBAL_TEST_RETRY_ENABLED: true + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + ANTHROPIC_CACHING_BASE_URL: ${{ secrets.ANTHROPIC_CACHING_BASE_URL }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AZURE_OPENAI_AUDIO_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_AUDIO_DEPLOYMENT_NAME }} + AZURE_OPENAI_AUDIO_ENDPOINT: ${{ secrets.AZURE_OPENAI_AUDIO_ENDPOINT }} + AZURE_OPENAI_AUDIO_KEY: ${{ secrets.AZURE_OPENAI_AUDIO_KEY }} + AZURE_OPENAI_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_DEPLOYMENT_NAME }} + AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} + AZURE_OPENAI_KEY: ${{ secrets.AZURE_OPENAI_KEY }} + AZURE_SEARCH_ENDPOINT: ${{ secrets.AZURE_SEARCH_ENDPOINT }} + AZURE_SEARCH_KEY: ${{ secrets.AZURE_SEARCH_KEY }} + COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} + DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} + ELASTICSEARCH_CLOUD_API_KEY: ${{ secrets.ELASTICSEARCH_CLOUD_API_KEY }} + ELASTICSEARCH_CLOUD_URL: ${{ secrets.ELASTICSEARCH_CLOUD_URL }} + GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }} + GCP_LOCATION: ${{ secrets.GCP_LOCATION }} + GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} + GCP_PROJECT_NUM: ${{ secrets.GCP_PROJECT_NUM }} + GCP_VERTEXAI_ENDPOINT: ${{ secrets.GCP_VERTEXAI_ENDPOINT }} + GOOGLE_AI_GEMINI_API_KEY: ${{ secrets.GOOGLE_AI_GEMINI_API_KEY }} + HF_API_KEY: ${{ secrets.HF_API_KEY }} + JINA_API_KEY: ${{ secrets.JINA_API_KEY }} + MICROSOFT_FOUNDRY_API_KEY: ${{ secrets.MICROSOFT_FOUNDRY_API_KEY }} + MICROSOFT_FOUNDRY_ENDPOINT: ${{ secrets.MICROSOFT_FOUNDRY_ENDPOINT }} + MILVUS_API_KEY: ${{ secrets.MILVUS_API_KEY }} + MILVUS_URI: ${{ secrets.MILVUS_URI }} + MISTRAL_AI_API_KEY: ${{ secrets.MISTRAL_AI_API_KEY }} + NOMIC_API_KEY: ${{ secrets.NOMIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} + OVHAI_AI_API_KEY: ${{ secrets.OVHAI_AI_API_KEY }} + PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }} + RAPID_API_KEY: ${{ secrets.RAPID_API_KEY }} + TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} + VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} + WEAVIATE_API_KEY: ${{ secrets.WEAVIATE_API_KEY }} + WEAVIATE_HOST: ${{ secrets.WEAVIATE_HOST }} + CI_DELAY_SECONDS_AZURE_AI_SEARCH: 4 + CI_DELAY_SECONDS_AZURE_OPENAI: 3 + CI_DELAY_SECONDS_BEDROCK: 3 + CI_DELAY_SECONDS_GITHUB_MODELS: 7 + CI_DELAY_SECONDS_GOOGLE_AI_GEMINI: 20 + CI_DELAY_SECONDS_GOOGLE_AI_GEMINI_BATCH: 20 + CI_DELAY_SECONDS_VERTEX_AI: 5 + CI_DELAY_SECONDS_VERTEX_AI_ANTHROPIC: 12 + CI_DELAY_SECONDS_VERTEX_AI_GEMINI: 12 + CI_DELAY_SECONDS_VOYAGE_AI: 22 + + - name: Clean Docker + run: docker system prune -af || true + + - name: Publish Test Summary + if: always() + run: | + python3 << 'PYEOF' + import xml.etree.ElementTree as ET + import glob, os, re + + files = glob.glob('**/target/*-reports/TEST-*.xml', recursive=True) + total = failures = errors = skipped = 0 + failed_tests = [] + + for path in files: + try: + root = ET.parse(path).getroot() + total += int(root.get('tests', 0)) + failures += int(root.get('failures', 0)) + errors += int(root.get('errors', 0)) + skipped += int(root.get('skipped', 0)) + for tc in root.findall('.//testcase'): + fail = tc.find('failure') + if fail is None: + fail = tc.find('error') + if fail is not None: + cls = tc.get('classname', '') + name = tc.get('name', '') + trace = fail.text or '' + m = re.search(r'\bat ' + re.escape(cls) + r'[.$][^\n(]*\(([^)]+\.java:\d+)\)', trace) + loc = f'`{m.group(1)}`' if m else '' + msg = (fail.get('message', '') or '').replace('\n', '
').replace('|', '\\|') + if len(msg) > 500: + msg = f"{msg[:500]}
show more{msg[500:]}
" + failed_tests.append(f"| `{cls}` | `{name}` | {loc} | {msg} |") + except Exception: + pass + + passed = total - failures - errors - skipped + summary = os.environ.get('GITHUB_STEP_SUMMARY', '/dev/null') + with open(summary, 'a') as f: + f.write('## Test Results\n\n') + if failures == 0 and errors == 0: + f.write(f':white_check_mark: **{passed} passed**, {skipped} skipped out of {total} tests\n') + else: + f.write(f':x: **{failures} failed, {errors} errors**, {passed} passed, {skipped} skipped out of {total} tests\n') + if failed_tests: + f.write('\n### Failures\n\n') + f.write('| Class | Test | Location | Error Message |\n') + f.write('|-------|------|----------|---------------|\n') + for line in failed_tests[:50]: + f.write(line + '\n') + if len(failed_tests) > 50: + f.write(f'\n*... and {len(failed_tests) - 50} more*\n') + PYEOF + + - name: Upload Test Reports + if: always() # always run even if the previous step failed or was cancelled + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: Test-Reports-${{ strategy.job-index }} + path: '**/target/*-reports/*' + + - name: Upload JaCoCo Reports + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: jacoco-${{ strategy.job-index }} + path: '**/target/site/jacoco/**' + + - name: Upload JaCoCo Execution Data + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: jacoco-exec-${{ strategy.job-index }} + path: '**/target/jacoco.exec' diff --git a/.github/workflows/nightly_jdk21.yaml b/.github/workflows/nightly_jdk21.yaml new file mode 100644 index 0000000..690c55b --- /dev/null +++ b/.github/workflows/nightly_jdk21.yaml @@ -0,0 +1,226 @@ +name: Nightly Build JDK 21 + +on: + schedule: + - cron: '0 1 * * *' # daily at 01:00 UTC + workflow_dispatch: + +permissions: + contents: read + +jobs: + discover-modules: + if: github.repository == 'langchain4j/langchain4j' + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - id: set-matrix + shell: bash + run: | + echo "Discovering Maven modules..." + modules=$(find . -type f -name pom.xml -exec dirname {} \; | sort | uniq | sed 's|^\./||' | grep -v '^$' | grep -v '^.$') + echo "Detected modules:" + echo "$modules" + + # number of parallel groups + N=7 + + # round-robin grouping + matrix=$(echo "$modules" | awk -v N="$N" ' + { + g = (NR - 1) % N + arr[g] = (arr[g] ? arr[g] "," "\"" $0 "\"" : "\"" $0 "\"") + } + END { + printf("{\"modules\": ["); + for (i = 0; i < N; i++) { + if (i > 0) printf(","); + printf("[%s]", arr[i]) + } + print "]}" + } + ') + + echo "Generated matrix: $matrix" + echo "matrix=$matrix" >> $GITHUB_OUTPUT + + build: + needs: discover-modules + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.discover-modules.outputs.matrix) }} + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - name: Set up JDK 21 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: 21 + distribution: 'temurin' + cache: 'maven' + + - name: Authenticate to Google Cloud + # Required for Google modules (e.g., langchain4j-vertex-ai) + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3 + with: + project_id: ${{ secrets.GCP_PROJECT_ID }} + credentials_json: ${{ secrets.GCP_CREDENTIALS_JSON }} + + - name: Setup JBang + # Required for MCP module + uses: jbangdev/setup-jbang@2b1b465a7b75f4222b81426f23a01e013aa7b95c # v0.1.1 + continue-on-error: true + + - name: Build with JDK 21 + run: | + modules="${{ join(matrix.modules, ',') }}" + exclude_modules="langchain4j-gpu-llama3" + for ex in ${exclude_modules//,/ }; do + modules=$(echo "$modules" | sed "s/\b${ex}\b//g" | sed 's/,,/,/g' | sed 's/^,//' | sed 's/,$//') + done + echo "Building modules: $modules" + + ## compile and install ALL modules to avoid running integration tests on dependent modules in the step below + mvn -B -U -T8C -DskipTests -DskipITs -DembeddingsSkipCache install + + mvn -B -U verify \ + -pl "${modules}" \ + -DskipAzureAiSearchITs -DskipJlamaITs -DskipLocalAiITs -DskipMilvusITs -DskipOllamaITs -DskipOracleITs -DskipVespaITs \ + --fail-at-end \ + -Djunit.jupiter.extensions.autodetection.enabled=true \ + -DembeddingsSkipCache \ + -Dtinylog.writer.level=info + + env: + LC4J_GLOBAL_TEST_RETRY_ENABLED: true + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + ANTHROPIC_CACHING_BASE_URL: ${{ secrets.ANTHROPIC_CACHING_BASE_URL }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AZURE_OPENAI_AUDIO_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_AUDIO_DEPLOYMENT_NAME }} + AZURE_OPENAI_AUDIO_ENDPOINT: ${{ secrets.AZURE_OPENAI_AUDIO_ENDPOINT }} + AZURE_OPENAI_AUDIO_KEY: ${{ secrets.AZURE_OPENAI_AUDIO_KEY }} + AZURE_OPENAI_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_DEPLOYMENT_NAME }} + AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} + AZURE_OPENAI_KEY: ${{ secrets.AZURE_OPENAI_KEY }} + AZURE_SEARCH_ENDPOINT: ${{ secrets.AZURE_SEARCH_ENDPOINT }} + AZURE_SEARCH_KEY: ${{ secrets.AZURE_SEARCH_KEY }} + COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} + DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} + ELASTICSEARCH_CLOUD_API_KEY: ${{ secrets.ELASTICSEARCH_CLOUD_API_KEY }} + ELASTICSEARCH_CLOUD_URL: ${{ secrets.ELASTICSEARCH_CLOUD_URL }} + GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }} + GCP_LOCATION: ${{ secrets.GCP_LOCATION }} + GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} + GCP_PROJECT_NUM: ${{ secrets.GCP_PROJECT_NUM }} + GCP_VERTEXAI_ENDPOINT: ${{ secrets.GCP_VERTEXAI_ENDPOINT }} + GOOGLE_AI_GEMINI_API_KEY: ${{ secrets.GOOGLE_AI_GEMINI_API_KEY }} + HF_API_KEY: ${{ secrets.HF_API_KEY }} + JINA_API_KEY: ${{ secrets.JINA_API_KEY }} + MICROSOFT_FOUNDRY_API_KEY: ${{ secrets.MICROSOFT_FOUNDRY_API_KEY }} + MICROSOFT_FOUNDRY_ENDPOINT: ${{ secrets.MICROSOFT_FOUNDRY_ENDPOINT }} + MILVUS_API_KEY: ${{ secrets.MILVUS_API_KEY }} + MILVUS_URI: ${{ secrets.MILVUS_URI }} + MISTRAL_AI_API_KEY: ${{ secrets.MISTRAL_AI_API_KEY }} + NOMIC_API_KEY: ${{ secrets.NOMIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} + OVHAI_AI_API_KEY: ${{ secrets.OVHAI_AI_API_KEY }} + PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }} + RAPID_API_KEY: ${{ secrets.RAPID_API_KEY }} + TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} + VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} + WEAVIATE_API_KEY: ${{ secrets.WEAVIATE_API_KEY }} + WEAVIATE_HOST: ${{ secrets.WEAVIATE_HOST }} + CI_DELAY_SECONDS_AZURE_AI_SEARCH: 4 + CI_DELAY_SECONDS_AZURE_OPENAI: 3 + CI_DELAY_SECONDS_BEDROCK: 3 + CI_DELAY_SECONDS_GITHUB_MODELS: 7 + CI_DELAY_SECONDS_GOOGLE_AI_GEMINI: 20 + CI_DELAY_SECONDS_GOOGLE_AI_GEMINI_BATCH: 20 + CI_DELAY_SECONDS_VERTEX_AI: 5 + CI_DELAY_SECONDS_VERTEX_AI_ANTHROPIC: 12 + CI_DELAY_SECONDS_VERTEX_AI_GEMINI: 12 + CI_DELAY_SECONDS_VOYAGE_AI: 22 + + - name: Clean Docker + run: docker system prune -af || true + + - name: Publish Test Summary + if: always() + run: | + python3 << 'PYEOF' + import xml.etree.ElementTree as ET + import glob, os, re + + files = glob.glob('**/target/*-reports/TEST-*.xml', recursive=True) + total = failures = errors = skipped = 0 + failed_tests = [] + + for path in files: + try: + root = ET.parse(path).getroot() + total += int(root.get('tests', 0)) + failures += int(root.get('failures', 0)) + errors += int(root.get('errors', 0)) + skipped += int(root.get('skipped', 0)) + for tc in root.findall('.//testcase'): + fail = tc.find('failure') + if fail is None: + fail = tc.find('error') + if fail is not None: + cls = tc.get('classname', '') + name = tc.get('name', '') + trace = fail.text or '' + m = re.search(r'\bat ' + re.escape(cls) + r'[.$][^\n(]*\(([^)]+\.java:\d+)\)', trace) + loc = f'`{m.group(1)}`' if m else '' + msg = (fail.get('message', '') or '').replace('\n', '
').replace('|', '\\|') + if len(msg) > 500: + msg = f"{msg[:500]}
show more{msg[500:]}
" + failed_tests.append(f"| `{cls}` | `{name}` | {loc} | {msg} |") + except Exception: + pass + + passed = total - failures - errors - skipped + summary = os.environ.get('GITHUB_STEP_SUMMARY', '/dev/null') + with open(summary, 'a') as f: + f.write('## Test Results\n\n') + if failures == 0 and errors == 0: + f.write(f':white_check_mark: **{passed} passed**, {skipped} skipped out of {total} tests\n') + else: + f.write(f':x: **{failures} failed, {errors} errors**, {passed} passed, {skipped} skipped out of {total} tests\n') + if failed_tests: + f.write('\n### Failures\n\n') + f.write('| Class | Test | Location | Error Message |\n') + f.write('|-------|------|----------|---------------|\n') + for line in failed_tests[:50]: + f.write(line + '\n') + if len(failed_tests) > 50: + f.write(f'\n*... and {len(failed_tests) - 50} more*\n') + PYEOF + + - name: Upload Test Reports + if: always() # always run even if the previous step failed or was cancelled + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: Test-Reports-${{ strategy.job-index }} + path: '**/target/*-reports/*' + + - name: Upload JaCoCo Reports + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: jacoco-${{ strategy.job-index }} + path: '**/target/site/jacoco/**' + + - name: Upload JaCoCo Execution Data + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: jacoco-exec-${{ strategy.job-index }} + path: '**/target/jacoco.exec' diff --git a/.github/workflows/nightly_jdk25.yaml b/.github/workflows/nightly_jdk25.yaml new file mode 100644 index 0000000..318c831 --- /dev/null +++ b/.github/workflows/nightly_jdk25.yaml @@ -0,0 +1,222 @@ +name: Nightly Build JDK 25 + +on: + schedule: + - cron: '0 4 * * *' # daily at 04:00 UTC + workflow_dispatch: + +permissions: + contents: read + +jobs: + discover-modules: + if: github.repository == 'langchain4j/langchain4j' + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - id: set-matrix + shell: bash + run: | + echo "Discovering Maven modules..." + modules=$(find . -type f -name pom.xml -exec dirname {} \; | sort | uniq | sed 's|^\./||' | grep -v '^$' | grep -v '^.$') + echo "Detected modules:" + echo "$modules" + + # number of parallel groups + N=7 + + # round-robin grouping + matrix=$(echo "$modules" | awk -v N="$N" ' + { + g = (NR - 1) % N + arr[g] = (arr[g] ? arr[g] "," "\"" $0 "\"" : "\"" $0 "\"") + } + END { + printf("{\"modules\": ["); + for (i = 0; i < N; i++) { + if (i > 0) printf(","); + printf("[%s]", arr[i]) + } + print "]}" + } + ') + + echo "Generated matrix: $matrix" + echo "matrix=$matrix" >> $GITHUB_OUTPUT + + build: + needs: discover-modules + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.discover-modules.outputs.matrix) }} + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - name: Set up JDK 25 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: 25 + distribution: 'temurin' + cache: 'maven' + + - name: Authenticate to Google Cloud + # Required for Google modules (e.g., langchain4j-vertex-ai) + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3 + with: + project_id: ${{ secrets.GCP_PROJECT_ID }} + credentials_json: ${{ secrets.GCP_CREDENTIALS_JSON }} + + - name: Setup JBang + # Required for MCP module + uses: jbangdev/setup-jbang@2b1b465a7b75f4222b81426f23a01e013aa7b95c # v0.1.1 + continue-on-error: true + + - name: Build with JDK 25 + run: | + modules="${{ join(matrix.modules, ',') }}" + echo "Building modules: $modules" + + ## compile and install ALL modules to avoid running integration tests on dependent modules in the step below + mvn -B -U -T8C -DskipTests -DskipITs -DembeddingsSkipCache install + + mvn -B -U verify \ + -pl "${modules}" \ + -DskipAzureAiSearchITs -DskipJlamaITs -DskipLocalAiITs -DskipMilvusITs -DskipOllamaITs -DskipOracleITs -DskipVespaITs \ + --fail-at-end \ + -Djunit.jupiter.extensions.autodetection.enabled=true \ + -DembeddingsSkipCache \ + -Dtinylog.writer.level=info + + env: + LC4J_GLOBAL_TEST_RETRY_ENABLED: true + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + ANTHROPIC_CACHING_BASE_URL: ${{ secrets.ANTHROPIC_CACHING_BASE_URL }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AZURE_OPENAI_AUDIO_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_AUDIO_DEPLOYMENT_NAME }} + AZURE_OPENAI_AUDIO_ENDPOINT: ${{ secrets.AZURE_OPENAI_AUDIO_ENDPOINT }} + AZURE_OPENAI_AUDIO_KEY: ${{ secrets.AZURE_OPENAI_AUDIO_KEY }} + AZURE_OPENAI_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_DEPLOYMENT_NAME }} + AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} + AZURE_OPENAI_KEY: ${{ secrets.AZURE_OPENAI_KEY }} + AZURE_SEARCH_ENDPOINT: ${{ secrets.AZURE_SEARCH_ENDPOINT }} + AZURE_SEARCH_KEY: ${{ secrets.AZURE_SEARCH_KEY }} + COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} + DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} + ELASTICSEARCH_CLOUD_API_KEY: ${{ secrets.ELASTICSEARCH_CLOUD_API_KEY }} + ELASTICSEARCH_CLOUD_URL: ${{ secrets.ELASTICSEARCH_CLOUD_URL }} + GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }} + GCP_LOCATION: ${{ secrets.GCP_LOCATION }} + GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} + GCP_PROJECT_NUM: ${{ secrets.GCP_PROJECT_NUM }} + GCP_VERTEXAI_ENDPOINT: ${{ secrets.GCP_VERTEXAI_ENDPOINT }} + GOOGLE_AI_GEMINI_API_KEY: ${{ secrets.GOOGLE_AI_GEMINI_API_KEY }} + HF_API_KEY: ${{ secrets.HF_API_KEY }} + JINA_API_KEY: ${{ secrets.JINA_API_KEY }} + MICROSOFT_FOUNDRY_API_KEY: ${{ secrets.MICROSOFT_FOUNDRY_API_KEY }} + MICROSOFT_FOUNDRY_ENDPOINT: ${{ secrets.MICROSOFT_FOUNDRY_ENDPOINT }} + MILVUS_API_KEY: ${{ secrets.MILVUS_API_KEY }} + MILVUS_URI: ${{ secrets.MILVUS_URI }} + MISTRAL_AI_API_KEY: ${{ secrets.MISTRAL_AI_API_KEY }} + NOMIC_API_KEY: ${{ secrets.NOMIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} + OVHAI_AI_API_KEY: ${{ secrets.OVHAI_AI_API_KEY }} + PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }} + RAPID_API_KEY: ${{ secrets.RAPID_API_KEY }} + TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} + VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} + WEAVIATE_API_KEY: ${{ secrets.WEAVIATE_API_KEY }} + WEAVIATE_HOST: ${{ secrets.WEAVIATE_HOST }} + CI_DELAY_SECONDS_AZURE_AI_SEARCH: 4 + CI_DELAY_SECONDS_AZURE_OPENAI: 3 + CI_DELAY_SECONDS_BEDROCK: 3 + CI_DELAY_SECONDS_GITHUB_MODELS: 7 + CI_DELAY_SECONDS_GOOGLE_AI_GEMINI: 20 + CI_DELAY_SECONDS_GOOGLE_AI_GEMINI_BATCH: 20 + CI_DELAY_SECONDS_VERTEX_AI: 5 + CI_DELAY_SECONDS_VERTEX_AI_ANTHROPIC: 12 + CI_DELAY_SECONDS_VERTEX_AI_GEMINI: 12 + CI_DELAY_SECONDS_VOYAGE_AI: 22 + + - name: Clean Docker + run: docker system prune -af || true + + - name: Publish Test Summary + if: always() + run: | + python3 << 'PYEOF' + import xml.etree.ElementTree as ET + import glob, os, re + + files = glob.glob('**/target/*-reports/TEST-*.xml', recursive=True) + total = failures = errors = skipped = 0 + failed_tests = [] + + for path in files: + try: + root = ET.parse(path).getroot() + total += int(root.get('tests', 0)) + failures += int(root.get('failures', 0)) + errors += int(root.get('errors', 0)) + skipped += int(root.get('skipped', 0)) + for tc in root.findall('.//testcase'): + fail = tc.find('failure') + if fail is None: + fail = tc.find('error') + if fail is not None: + cls = tc.get('classname', '') + name = tc.get('name', '') + trace = fail.text or '' + m = re.search(r'\bat ' + re.escape(cls) + r'[.$][^\n(]*\(([^)]+\.java:\d+)\)', trace) + loc = f'`{m.group(1)}`' if m else '' + msg = (fail.get('message', '') or '').replace('\n', '
').replace('|', '\\|') + if len(msg) > 500: + msg = f"{msg[:500]}
show more{msg[500:]}
" + failed_tests.append(f"| `{cls}` | `{name}` | {loc} | {msg} |") + except Exception: + pass + + passed = total - failures - errors - skipped + summary = os.environ.get('GITHUB_STEP_SUMMARY', '/dev/null') + with open(summary, 'a') as f: + f.write('## Test Results\n\n') + if failures == 0 and errors == 0: + f.write(f':white_check_mark: **{passed} passed**, {skipped} skipped out of {total} tests\n') + else: + f.write(f':x: **{failures} failed, {errors} errors**, {passed} passed, {skipped} skipped out of {total} tests\n') + if failed_tests: + f.write('\n### Failures\n\n') + f.write('| Class | Test | Location | Error Message |\n') + f.write('|-------|------|----------|---------------|\n') + for line in failed_tests[:50]: + f.write(line + '\n') + if len(failed_tests) > 50: + f.write(f'\n*... and {len(failed_tests) - 50} more*\n') + PYEOF + + - name: Upload Test Reports + if: always() # always run even if the previous step failed or was cancelled + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: Test-Reports-${{ strategy.job-index }} + path: '**/target/*-reports/*' + + - name: Upload JaCoCo Reports + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: jacoco-${{ strategy.job-index }} + path: '**/target/site/jacoco/**' + + - name: Upload JaCoCo Execution Data + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: jacoco-exec-${{ strategy.job-index }} + path: '**/target/jacoco.exec' diff --git a/.github/workflows/nightly_oracle.yaml b/.github/workflows/nightly_oracle.yaml new file mode 100644 index 0000000..e6b3553 --- /dev/null +++ b/.github/workflows/nightly_oracle.yaml @@ -0,0 +1,121 @@ +name: Nightly Build Oracle + +# Oracle module is tested separately because it requires a lot of disk space +# and does not fit in one job together with other modules + +on: + schedule: + - cron: '0 6 * * *' # daily at 06:00 UTC + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + if: github.repository == 'langchain4j/langchain4j' + runs-on: ubuntu-latest + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - name: Set up JDK 17 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: 17 + distribution: 'temurin' + cache: 'maven' + + - name: Build with JDK 17 + run: | + ## compile and install ALL modules to avoid running integration tests on dependent modules in the step below + mvn -B -U -T8C -DskipTests -DskipITs -DembeddingsSkipCache install + + mvn -B -U verify \ + -pl langchain4j-oracle \ + --fail-at-end \ + -Djunit.jupiter.extensions.autodetection.enabled=true \ + -DembeddingsSkipCache \ + -Dtinylog.writer.level=info + + env: + LC4J_GLOBAL_TEST_RETRY_ENABLED: true + + - name: Clean Docker + run: docker system prune -af || true + + - name: Publish Test Summary + if: always() + run: | + python3 << 'PYEOF' + import xml.etree.ElementTree as ET + import glob, os, re + + files = glob.glob('**/target/*-reports/TEST-*.xml', recursive=True) + total = failures = errors = skipped = 0 + failed_tests = [] + + for path in files: + try: + root = ET.parse(path).getroot() + total += int(root.get('tests', 0)) + failures += int(root.get('failures', 0)) + errors += int(root.get('errors', 0)) + skipped += int(root.get('skipped', 0)) + for tc in root.findall('.//testcase'): + fail = tc.find('failure') + if fail is None: + fail = tc.find('error') + if fail is not None: + cls = tc.get('classname', '') + name = tc.get('name', '') + trace = fail.text or '' + m = re.search(r'\bat ' + re.escape(cls) + r'[.$][^\n(]*\(([^)]+\.java:\d+)\)', trace) + loc = f'`{m.group(1)}`' if m else '' + msg = (fail.get('message', '') or '').replace('\n', '
').replace('|', '\\|') + if len(msg) > 500: + msg = f"{msg[:500]}
show more{msg[500:]}
" + failed_tests.append(f"| `{cls}` | `{name}` | {loc} | {msg} |") + except Exception: + pass + + passed = total - failures - errors - skipped + summary = os.environ.get('GITHUB_STEP_SUMMARY', '/dev/null') + with open(summary, 'a') as f: + f.write('## Test Results\n\n') + if failures == 0 and errors == 0: + f.write(f':white_check_mark: **{passed} passed**, {skipped} skipped out of {total} tests\n') + else: + f.write(f':x: **{failures} failed, {errors} errors**, {passed} passed, {skipped} skipped out of {total} tests\n') + if failed_tests: + f.write('\n### Failures\n\n') + f.write('| Class | Test | Location | Error Message |\n') + f.write('|-------|------|----------|---------------|\n') + for line in failed_tests[:50]: + f.write(line + '\n') + if len(failed_tests) > 50: + f.write(f'\n*... and {len(failed_tests) - 50} more*\n') + PYEOF + + - name: Upload Test Reports + if: always() # always run even if the previous step failed or was cancelled + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: Test-Reports-${{ strategy.job-index }} + path: '**/target/*-reports/*' + + - name: Upload JaCoCo Reports + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: jacoco-${{ strategy.job-index }} + path: '**/target/site/jacoco/**' + + - name: Upload JaCoCo Execution Data + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: jacoco-exec-${{ strategy.job-index }} + path: '**/target/jacoco.exec' diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..f342d64 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,190 @@ +name: release + +on: + workflow_dispatch: + inputs: + stableVersion: + description: "Stable release version (e.g., 1.8.0)" + required: true + betaVersion: + description: "Beta release version (e.g., 1.8.0-beta15)" + required: true + branch: + description: "Branch to release from (e.g., 'main' for minor releases, 'release/x.y.z' for patch releases)" + required: false + default: "main" + startFrom: + description: "Phase to start from (use to resume after a failed release)" + required: false + default: "core-and-parent" + type: choice + options: + - core-and-parent + - in-process-embeddings + - rest-of-modules + - commit-and-tag + +env: + STABLE_VERSION: ${{ github.event.inputs.stableVersion || github.event.client_payload.stableVersion }} + BETA_VERSION: ${{ github.event.inputs.betaVersion || github.event.client_payload.betaVersion }} + BRANCH: ${{ github.event.inputs.branch || github.event.client_payload.branch || 'main' }} + START_FROM: ${{ github.event.inputs.startFrom || github.event.client_payload.startFrom || 'core-and-parent' }} + +permissions: + contents: write + +concurrency: + group: release + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + ref: ${{ env.BRANCH }} + fetch-depth: 0 + # PAT is required to push the version commit and release tag to a protected branch + token: ${{ secrets.GH_RELEASE_AUTOMATION }} + + - name: Set up JDK 25 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: '25' + distribution: 'temurin' + cache: maven + server-id: central + server-username: MAVEN_CENTRAL_USERNAME + server-password: MAVEN_CENTRAL_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: GPG_PASSPHRASE + + - name: Show inputs + run: | + echo "Stable version: $STABLE_VERSION" + echo "Beta version: $BETA_VERSION" + echo "Branch to release from: $BRANCH" + echo "Start from phase: $START_FROM" + + - name: Compute start phase number + id: phase + run: | + case "$START_FROM" in + core-and-parent) echo "num=1" >> "$GITHUB_OUTPUT" ;; + in-process-embeddings) echo "num=2" >> "$GITHUB_OUTPUT" ;; + rest-of-modules) echo "num=3" >> "$GITHUB_OUTPUT" ;; + commit-and-tag) echo "num=4" >> "$GITHUB_OUTPUT" ;; + *) echo "Unknown startFrom: $START_FROM" >&2; exit 1 ;; + esac + + - name: Extract current stable SNAPSHOT version from POM property + id: extract-stable-snapshot-version + run: | + REVISION=$(mvn help:evaluate -Dexpression=langchain4j.stable.version -q -DforceStdout) + echo "stableVersionSnapshot=$REVISION" >> $GITHUB_OUTPUT + + - name: Update stable versions + run: mvn versions:set -DnewVersion="$STABLE_VERSION" -DoldVersion=${{ steps.extract-stable-snapshot-version.outputs.stableVersionSnapshot }} -DgroupId=* -DartifactId=* -DgenerateBackupPoms=false + + - name: Update stable version property + run: mvn versions:set-property -Dproperty=langchain4j.stable.version -DnewVersion="$STABLE_VERSION" -DgenerateBackupPoms=false + + - name: Extract current beta SNAPSHOT version from POM property + id: extract-beta-snapshot-version + run: | + REVISION=$(mvn help:evaluate -Dexpression=langchain4j.beta.version -q -DforceStdout) + echo "betaVersionSnapshot=$REVISION" >> $GITHUB_OUTPUT + + - name: Update beta versions + run: mvn versions:set -DnewVersion="$BETA_VERSION" -DoldVersion=${{ steps.extract-beta-snapshot-version.outputs.betaVersionSnapshot }} -DgroupId=* -DartifactId=* -DgenerateBackupPoms=false + + - name: Update beta version property + run: mvn versions:set-property -Dproperty=langchain4j.beta.version -DnewVersion="$BETA_VERSION" -DgenerateBackupPoms=false + + - name: Release core and parent modules + if: ${{ fromJSON(steps.phase.outputs.num) <= 1 }} + run: mvn -B -U -pl internal/langchain4j-internal-test-retry,langchain4j-core,langchain4j-parent -Psign clean deploy + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + + - name: Release in-process embeddings modules + if: ${{ fromJSON(steps.phase.outputs.num) <= 2 }} + run: | + EMBEDDING_MODULES=$(find embeddings -mindepth 1 -maxdepth 1 -type d -exec test -f "{}/pom.xml" \; -print | paste -sd , -) + + mvn -B -U --fail-at-end \ + -DskipTests -DskipITs \ + -pl "$EMBEDDING_MODULES" \ + -Psign clean deploy + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + + - name: Release the rest of the modules + if: ${{ fromJSON(steps.phase.outputs.num) <= 3 }} + run: | + EMBEDDING_MODULES=$(find embeddings -mindepth 1 -maxdepth 1 -type d -exec test -f "{}/pom.xml" \; -print | paste -sd , -) + + EXCLUDES="!internal/langchain4j-internal-test-retry,!langchain4j-core,!langchain4j-parent" + + # Append embeddings exclusions (converted to !path syntax) + if [ -n "$EMBEDDING_MODULES" ]; then + EMBEDDING_MODULES_EXCLUDES=$(echo "$EMBEDDING_MODULES" | sed 's/[^,]*/!\0/g') + EXCLUDES="$EXCLUDES,$EMBEDDING_MODULES_EXCLUDES" + fi + + # Exclude integration-tests and all its submodules + INTEGRATION_TEST_MODULES=$(find integration-tests -mindepth 1 -maxdepth 2 -type d -exec test -f "{}/pom.xml" \; -print | paste -sd , -) + EXCLUDES="$EXCLUDES,!integration-tests" + if [ -n "$INTEGRATION_TEST_MODULES" ]; then + INTEGRATION_TEST_EXCLUDES=$(echo "$INTEGRATION_TEST_MODULES" | sed 's/[^,]*/!\0/g') + EXCLUDES="$EXCLUDES,$INTEGRATION_TEST_EXCLUDES" + fi + + # Exclude internal modules + EXCLUDES="$EXCLUDES,!internal/langchain4j-docu-chatbot-updater" + + echo "Excluding modules: $EXCLUDES" + + mvn -B -U --fail-at-end \ + -DskipTests -DskipITs \ + -pl "$EXCLUDES" \ + -Psign clean deploy + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + + - name: Commit version changes + if: ${{ fromJSON(steps.phase.outputs.num) <= 4 }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add pom.xml '**/pom.xml' + git commit -m "Release versions $STABLE_VERSION and $BETA_VERSION" + + - name: Push + if: ${{ fromJSON(steps.phase.outputs.num) <= 4 }} + run: | + git push origin "$BRANCH" + + - name: Tag release + if: ${{ fromJSON(steps.phase.outputs.num) <= 4 }} + run: | + git tag "$STABLE_VERSION" + git push origin "$STABLE_VERSION" + + - name: Trigger release in the langchain4j/langchain4j-spring repo + env: + GH_TOKEN: ${{ secrets.GH_RELEASE_AUTOMATION }} + run: | + gh api repos/langchain4j/langchain4j-spring/dispatches \ + -f event_type=trigger-release \ + -f "client_payload[stableVersion]=$STABLE_VERSION" \ + -f "client_payload[betaVersion]=$BETA_VERSION" \ + -f "client_payload[branch]=$BRANCH" diff --git a/.github/workflows/snapshot_release.yaml b/.github/workflows/snapshot_release.yaml new file mode 100644 index 0000000..6a01914 --- /dev/null +++ b/.github/workflows/snapshot_release.yaml @@ -0,0 +1,49 @@ +name: snapshot_release + +on: + repository_dispatch: + types: [ trigger-snapshot-release ] + workflow_dispatch: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: snapshot-release + cancel-in-progress: false + +jobs: + snapshot_release: + if: github.repository == 'langchain4j/langchain4j' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - name: Set up JDK 25 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + # Modules specify the oldest java version that they support: + # even if we build with version 25, produced jars should be compatible with + # JVMs running the version specified in each module's maven.compiler.target property + java-version: '25' + distribution: 'temurin' + server-id: central + server-username: MAVEN_CENTRAL_USERNAME + server-password: MAVEN_CENTRAL_PASSWORD + + - name: snapshot_release + env: + MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + run: | + mvn -B -U --fail-at-end -DskipTests -DskipITs clean deploy + + - name: Trigger SNAPSHOT release for langchain4j-spring repo + if: ${{ github.event_name == 'repository_dispatch' }} + env: + GH_TOKEN: ${{ secrets.GH_RELEASE_AUTOMATION }} + run: | + gh api repos/langchain4j/langchain4j-spring/dispatches \ + -f event_type=trigger-snapshot-release diff --git a/.github/workflows/split-package-detection.yaml b/.github/workflows/split-package-detection.yaml new file mode 100644 index 0000000..fe38f1c --- /dev/null +++ b/.github/workflows/split-package-detection.yaml @@ -0,0 +1,44 @@ +name: Split Package Detection + +on: + push: + branches: [ main, master, 'release/**' ] + pull_request: + branches: [ main, master, 'release/**' ] + # Optional: Run manually from the Actions tab + workflow_dispatch: + +permissions: + contents: read + +jobs: + check-split-packages: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - name: Set up JDK + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + distribution: 'temurin' + java-version: '25' + + - name: Build with Maven + run: mvn package -Dmaven.javadoc.skip=true -DskipTests -DskipITs -DembeddingsSkipDownload + + - name: Make script executable + run: chmod +x ./check-split-packages.sh + + - name: Run split package detection + run: ./check-split-packages.sh + + - name: Upload results as artifact if failure + if: failure() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: split-package-report + path: | + check-split-packages.sh + # You could save the output to a file and include that too diff --git a/.github/workflows/update-versions-next-dev-iteration.yaml b/.github/workflows/update-versions-next-dev-iteration.yaml new file mode 100644 index 0000000..f128ed1 --- /dev/null +++ b/.github/workflows/update-versions-next-dev-iteration.yaml @@ -0,0 +1,104 @@ +name: Update versions for next dev iteration + +on: + repository_dispatch: + types: [ trigger-update-versions-next-dev-iteration ] + workflow_dispatch: + inputs: + nextStableVersion: + description: "Next stable SNAPSHOT version (e.g., 1.9.0-SNAPSHOT)" + required: true + nextBetaVersion: + description: "Next beta SNAPSHOT version (e.g., 1.9.0-beta16-SNAPSHOT)" + required: true + +env: + NEXT_STABLE_VERSION: ${{ github.event.inputs.nextStableVersion || github.event.client_payload.nextStableVersion }} + NEXT_BETA_VERSION: ${{ github.event.inputs.nextBetaVersion || github.event.client_payload.nextBetaVersion }} + +permissions: + contents: write + +concurrency: + group: update-versions + cancel-in-progress: false + +jobs: + update_versions: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + fetch-depth: 0 + token: ${{ secrets.GH_RELEASE_AUTOMATION }} + + - name: Set up JDK 25 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: '25' + distribution: 'temurin' + cache: maven + + - name: Show inputs + run: | + echo "Next stable version: $NEXT_STABLE_VERSION" + echo "Next beta version: $NEXT_BETA_VERSION" + + - name: Extract current stable version property + id: extract-current-stable + run: | + REVISION=$(mvn help:evaluate -Dexpression=langchain4j.stable.version -q -DforceStdout) + echo "currentStableVersion=$REVISION" >> $GITHUB_OUTPUT + + - name: Extract current beta version property + id: extract-current-beta + run: | + REVISION=$(mvn help:evaluate -Dexpression=langchain4j.beta.version -q -DforceStdout) + echo "currentBetaVersion=$REVISION" >> $GITHUB_OUTPUT + + - name: Update stable versions + run: | + mvn versions:set \ + -DnewVersion="$NEXT_STABLE_VERSION" \ + -DoldVersion=${{ steps.extract-current-stable.outputs.currentStableVersion }} \ + -DgroupId=* -DartifactId=* -DgenerateBackupPoms=false + + - name: Update stable version property + run: | + mvn versions:set-property \ + -Dproperty=langchain4j.stable.version \ + -DnewVersion="$NEXT_STABLE_VERSION" \ + -DgenerateBackupPoms=false + + - name: Update beta versions + run: | + mvn versions:set \ + -DnewVersion="$NEXT_BETA_VERSION" \ + -DoldVersion=${{ steps.extract-current-beta.outputs.currentBetaVersion }} \ + -DgroupId=* -DartifactId=* -DgenerateBackupPoms=false + + - name: Update beta version property + run: | + mvn versions:set-property \ + -Dproperty=langchain4j.beta.version \ + -DnewVersion="$NEXT_BETA_VERSION" \ + -DgenerateBackupPoms=false + + - name: Commit and push changes + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add pom.xml '**/pom.xml' + git commit -m "Update versions to $NEXT_STABLE_VERSION and $NEXT_BETA_VERSION" + git push origin main + + - name: Trigger the same job in the langchain4j-spring repo + env: + GH_TOKEN: ${{ secrets.GH_RELEASE_AUTOMATION }} + run: | + gh api repos/langchain4j/langchain4j-spring/dispatches \ + -f event_type=trigger-update-versions-next-dev-iteration \ + -f "client_payload[nextStableVersion]=$NEXT_STABLE_VERSION" \ + -f "client_payload[nextBetaVersion]=$NEXT_BETA_VERSION" diff --git a/.github/yamllint.conf b/.github/yamllint.conf new file mode 100644 index 0000000..1b6be04 --- /dev/null +++ b/.github/yamllint.conf @@ -0,0 +1,5 @@ +extends: default + +rules: + line-length: disable + commas: disable diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88821ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +target/ +**/.flattened-pom.xml + +### IntelliJ IDEA ### +.idea/* +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +### .env files contain local environment variables ### +.env +langchain4j-core/target_test-classes/ + +## AI Coding Assistants ### +CLAUDE.md +.claude + +### Quarkus stuff ### +.quarkus + +jdk/ diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 0000000..52bed3c --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1,3 @@ +--add-opens=java.base/java.lang=ALL-UNNAMED +--add-opens=java.base/java.io=ALL-UNNAMED +-Djava.awt.headless=true diff --git a/.mvn/maven.config b/.mvn/maven.config new file mode 100644 index 0000000..bdfa80a --- /dev/null +++ b/.mvn/maven.config @@ -0,0 +1,3 @@ +-Dkotlin.compiler.incremental=true +-Dmaven.wagon.http.retryHandler.count=5 +-Dmaven.wagon.http.retryHandler.requestSentEnabled=true diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..5291372 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.15/apache-maven-3.9.15-bin.zip diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..e9e3a01 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 4, + "printWidth": 120 +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..50fc1ef --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +info@langchain4j.dev. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e32cf7b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,65 @@ +Thank you for investing your time and effort in contributing to our project, we appreciate it a lot! 🤗 + +# General guidelines + +- For new integrations, please consider adding it in [community repo](https://github.com/langchain4j/langchain4j-community) first. +- If you want to contribute a bug fix or a new feature that isn't listed in the [issues](https://github.com/langchain4j/langchain4j/issues) yet, please open a new issue for it. We will triage it shortly. +- Follow [Google's Best Practices for Java Libraries](https://jlbp.dev/) +- Keep the code compatible with Java 17. +- When integrating third-party services, use the official SDK whenever possible. If no official SDK is available, implement the client using `langchain4j-http-client` and Jackson. +- Avoid adding new dependencies as much as possible (new dependencies with test scope are OK). If absolutely necessary, try to use the same libraries which are already used in the project. Make sure you run `mvn dependency:analyze` to identify unnecessary dependencies. +- Write unit and/or integration tests for your code. This is critical: no tests, no review! +- The tests should cover both positive and negative cases. +- Make sure you run all unit tests on all modules with `mvn clean test`. Some integration tests need the API token (key) to be set up as an environment variable in order to communicate with the configured model provider (look for "EnabledIfEnvironmentVariable" annotation to find out the name of this token). +- Avoid making breaking changes. Always keep backward compatibility in mind. For example, instead of removing fields/methods/etc, mark them `@Deprecated` and make sure they still work as before. +- Follow existing naming conventions. +- Add Javadoc where necessary. There's no need to duplicate Javadoc from the implemented interfaces. +- Follow existing code style present in the project. Run `make lint` and `make format` before commit. +- Large features should be discussed with maintainers before implementation. + +# Opening an issue + +- Please fill in all sections of the issue template. + +# Opening a PR + +- Please open the PR as ready for review, not as a draft. +- Before opening the PR, please make sure it is complete: + - Add unit and/or integration tests for your change (see the testing guidelines above). This is critical: no tests, no review! + - Add [documentation](https://github.com/langchain4j/langchain4j/tree/main/docs/docs) (if required). + - Run `./mvnw spotless:check` and `./mvnw spotless:apply` to ensure compliance with the source code formatting of the project. +- Fill in all the sections of the PR template. +- Please make it easier to review your PR: + - Keep changes as small as possible. + - Do not combine refactoring with changes in a single PR. + - Avoid reformatting existing code. + +Please note that we do not have the capacity to review PRs immediately. We ask for your patience. We are doing our best to review your PR as quickly as possible. + +# Guidelines on adding a new model integration + +- Please open PRs with new model integrations in the [langchain4j-community](https://github.com/langchain4j/langchain4j-community) repository +- [Integration with OpenAI](https://github.com/langchain4j/langchain4j/tree/main/langchain4j-open-ai) is a good example. +- Create integration test classes that extend from [`AbstractChatModelIT`](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-core/src/test/java/dev/langchain4j/model/chat/common/AbstractChatModelIT.java), [`AbstractStreamingChatModelIT`](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-core/src/test/java/dev/langchain4j/model/chat/common/AbstractStreamingChatModelIT.java), [`AbstractChatModelListenerIT`](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-core/src/test/java/dev/langchain4j/model/chat/common/AbstractChatModelListenerIT.java), [`AbstractStreamingChatModelListenerIT`](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-core/src/test/java/dev/langchain4j/model/chat/common/AbstractStreamingChatModelListenerIT.java) and [`AbstractStreamingAiServiceIT`](https://github.com/langchain4j/langchain4j/blob/main/langchain4j/src/test/java/dev/langchain4j/service/common/AbstractStreamingAiServiceIT.java). There are many examples in existing modules. +- If the model provider supports embeddings, implement `EmbeddingModel` using the request/response API (`embed(EmbeddingRequest)`, `supportedParameters()`, `supportedContentTypes()`, `listeners()`), and create an integration test class that extends from [`AbstractEmbeddingModelIT`](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-core/src/test/java/dev/langchain4j/model/embedding/common/AbstractEmbeddingModelIT.java). Override the `supports*()` methods to declare the model's capabilities (per-call parameters such as `input_type`/`dimensions`, image/multimodal inputs); the base test then verifies `embed(EmbeddingRequest)`, the convenience methods, listeners, and the fail-fast behavior for unsupported parameters/modalities. See the [OpenAI module](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-open-ai/src/test/java/dev/langchain4j/model/openai/common/OpenAiEmbeddingModelIT.java) for an example. +- If model provider supports tools, create an integration test class that extends from [`AbstractAiServiceWithToolsIT`](https://github.com/langchain4j/langchain4j/blob/main/langchain4j/src/test/java/dev/langchain4j/service/common/AbstractAiServiceWithToolsIT.java). There are many examples in existing modules. +- If model provider supports structured outputs, create an integration test class that extends from [`AbstractAiServiceWithJsonSchemaIT`](https://github.com/langchain4j/langchain4j/blob/main/langchain4j/src/test/java/dev/langchain4j/service/common/AbstractAiServiceWithJsonSchemaIT.java). There are many examples in existing modules. +- Document the new integration [here](https://github.com/langchain4j/langchain4j/blob/main/README.md), [here](https://github.com/langchain4j/langchain4j/tree/main/docs/docs/integrations/language-models) and [here](https://github.com/langchain4j/langchain4j/blob/main/docs/docs/integrations/language-models/index.md). +- Add an example to the [examples repository](https://github.com/langchain4j/langchain4j-examples), similar to [this](https://github.com/langchain4j/langchain4j-examples/tree/main/anthropic-examples). +- Add a new module to the appropriate section of the [BOM](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-bom/pom.xml). +- It would be great if you could add a [Spring Boot starter](https://github.com/langchain4j/langchain4j-spring). + +# Guidelines on adding a new embedding store integration + +- Please open PRs with new embedding store integrations in the [langchain4j-community](https://github.com/langchain4j/langchain4j-community) repository +- [Integration with Chroma](https://github.com/langchain4j/langchain4j/tree/main/langchain4j-chroma) is a good example. +- Add a `{IntegrationName}EmbeddingStoreIT`. It should extend from `EmbeddingStoreWithFilteringIT` (when store supports metadata filtering) or `EmbeddingStoreIT` and pass all tests. +- Add a `{IntegrationName}EmbeddingStoreRemovalIT`. It should extend from `EmbeddingStoreWithRemovalIT` and pass all tests. +- Document the new integration [here](https://github.com/langchain4j/langchain4j/blob/main/README.md), [here](https://github.com/langchain4j/langchain4j/tree/main/docs/docs/integrations/embedding-stores) and [here](https://github.com/langchain4j/langchain4j/blob/main/docs/docs/integrations/embedding-stores/index.md). +- Add an example to the [examples repository](https://github.com/langchain4j/langchain4j-examples), similar to [this](https://github.com/langchain4j/langchain4j-examples/tree/main/chroma-example). +- Add a new module to the appropriate section of the [BOM](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-bom/pom.xml). +- It would be great if you could add a [Spring Boot starter](https://github.com/langchain4j/langchain4j-spring). (after + +# Guidelines on changing an existing embedding store integration + +- Ensure that your changes are backwards compatible. `Embedding`s and `TextSegment`s persisted with the latest released version of LangChain4j should still work. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + 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. diff --git a/LOGO_LICENSE.md b/LOGO_LICENSE.md new file mode 100644 index 0000000..cc83ee4 --- /dev/null +++ b/LOGO_LICENSE.md @@ -0,0 +1,26 @@ +# LangChain4j Logo License + +The LangChain4j logo (a cup of coffee with a parrot next to it) is a trademark of the LangChain4j project. + +We want the community to freely use the logo in ways that support adoption, +learning, and discussion of LangChain4j. + +At the same time, we need to protect the project’s identity +and avoid confusion about what is or isn’t officially part of LangChain4j. + +## Permitted Uses + +You may use the logo without additional permission in the following contexts: + +- Books, tutorials, talks, videos, courses, and other educational content about LangChain4j. +- Articles, blog posts, and presentations discussing or teaching LangChain4j. +- Community events, meetups, and hackathons. +- Informational graphics, blog headers, or social media posts referencing LangChain4j. + +No attribution is required, though mentioning the LangChain4j project is appreciated. + +## Requesting Exceptions + +If you would like to use the logo in a way not covered above, +please open an issue in the LangChain4j GitHub repository +or contact the maintainers through community channels to request permission. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e66f6fa --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +.PHONY: build lint format # always run + +build: + mvn --version + mvn -U -T12C clean package + +# Analyze code for errors, potential issues, and coding standard violations. +# Reports problems but does not modify the code. +lint: + mvn -T12C -Pspotless spotless:check + +# Automatically format the code to conform to a style guide. +# Modifies the code to ensure consistent formatting. +format: + mvn -T12C -Pspotless spotless:apply diff --git a/README.md b/README.md new file mode 100644 index 0000000..d38c72d --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# LangChain4j: idiomatic, open-source Java library for building LLM-powered applications on the JVM + +[![Build Status](https://img.shields.io/github/actions/workflow/status/langchain4j/langchain4j/main.yaml?branch=main&style=for-the-badge&label=CI%20BUILD&logo=github)](https://github.com/langchain4j/langchain4j/actions/workflows/main.yaml) +[![Nightly Build](https://img.shields.io/github/actions/workflow/status/langchain4j/langchain4j/nightly_jdk17.yaml?branch=main&style=for-the-badge&label=NIGHTLY%20BUILD&logo=github)](https://github.com/langchain4j/langchain4j/actions/workflows/nightly_jdk17.yaml) +[![CODACY](https://img.shields.io/badge/Codacy-Dashboard-blue?style=for-the-badge&logo=codacy)](https://app.codacy.com/gh/langchain4j/langchain4j/dashboard) + +[![Discord](https://img.shields.io/discord/1156626270772269217?logo=discord&style=for-the-badge)](https://discord.gg/JzTFvyjG6R) +[![BlueSky](https://img.shields.io/badge/@langchain4j-follow-blue?logo=bluesky&style=for-the-badge)](https://bsky.app/profile/langchain4j.dev) +[![X](https://img.shields.io/badge/@langchain4j-follow-blue?logo=x&style=for-the-badge)](https://x.com/langchain4j) +[![Maven Version](https://img.shields.io/maven-central/v/dev.langchain4j/langchain4j?logo=apachemaven&style=for-the-badge)](https://search.maven.org/#search|gav|1|g:"dev.langchain4j"%20AND%20a:"langchain4j") + + +## Introduction + +Welcome! + +The goal of LangChain4j is to simplify integrating LLMs into Java applications. + +Here's how: +1. **Unified APIs:** + LLM providers (like OpenAI or Google Vertex AI) and embedding (vector) stores (such as Pinecone or Milvus) + use proprietary APIs. LangChain4j offers a unified API to avoid the need for learning and implementing specific APIs for each of them. + To experiment with different LLMs or embedding stores, you can easily switch between them without the need to rewrite your code. + LangChain4j currently supports [20+ popular LLM providers](https://docs.langchain4j.dev/integrations/language-models/) + and [30+ embedding stores](https://docs.langchain4j.dev/integrations/embedding-stores/). +2. **Comprehensive Toolbox:** + Since early 2023, the community has been building numerous LLM-powered applications, + identifying common abstractions, patterns, and techniques. LangChain4j has refined these into practical code. + Our toolbox includes tools ranging from low-level prompt templating, chat memory management, and function calling + to high-level patterns like Agents and RAG. + For each abstraction, we provide an interface along with multiple ready-to-use implementations based on common techniques. + Whether you're building a chatbot or developing a RAG with a complete pipeline from data ingestion to retrieval, + LangChain4j offers a wide variety of options. +3. **Numerous Examples:** + These [examples](https://github.com/langchain4j/langchain4j-examples) showcase how to begin creating various LLM-powered applications, + providing inspiration and enabling you to start building quickly. + +LangChain4j began development in early 2023 amid the ChatGPT hype. +We noticed a lack of Java counterparts to the numerous Python and JavaScript LLM libraries and frameworks, +and we had to fix that! + +**Despite the name, LangChain4j is not a Java port of LangChain (Python) — it is built for Java, not ported to it.** +It is an idiomatic Java library designed from the ground up around Java conventions: +type safety, POJOs, annotations, interfaces, dependency injection, fluent APIs, and first-class integrations with Quarkus, Spring Boot, Helidon, and Micronaut. +Its API, internals, and release cycle are independent of the Python LangChain project. + +We actively monitor community developments, aiming to quickly incorporate new techniques and integrations, +ensuring you stay up-to-date. +The library is under active development. While some features are still being worked on, +the core functionality is in place, allowing you to start building LLM-powered apps now! + + +## Documentation +Documentation can be found [here](https://docs.langchain4j.dev). + +The documentation chatbot (experimental) can be found [here](https://chat.langchain4j.dev/). + + +## Getting Started +Getting started guide can be found [here](https://docs.langchain4j.dev/get-started). + + +## Code Examples +Please see examples of how LangChain4j can be used in [langchain4j-examples](https://github.com/langchain4j/langchain4j-examples) repo: +- [Examples in plain Java](https://github.com/langchain4j/langchain4j-examples/tree/main/other-examples/src/main/java) +- [Examples with Quarkus](https://github.com/quarkiverse/quarkus-langchain4j/tree/main/samples) (uses [quarkus-langchain4j](https://github.com/quarkiverse/quarkus-langchain4j) dependency) +- [Example with Spring Boot](https://github.com/langchain4j/langchain4j-examples/tree/main/spring-boot-example/src/main/java/dev/langchain4j/example) +- [Examples with Helidon](https://github.com/helidon-io/helidon-examples/tree/helidon-4.x/examples/integrations/langchain4j) (uses [io.helidon.integrations.langchain4j](https://mvnrepository.com/artifact/io.helidon.integrations.langchain4j) dependency) +- [Examples with Micronaut](https://github.com/micronaut-projects/micronaut-langchain4j/tree/0.3.x/doc-examples/example-openai-java) (uses [micronaut-langchain4j](https://micronaut-projects.github.io/micronaut-langchain4j/latest/guide/) dependency) + +## Useful Materials +Useful materials can be found [here](https://docs.langchain4j.dev/useful-materials). + + +## Get Help +Please use [Discord](https://discord.gg/JzTFvyjG6R) or [GitHub discussions](https://github.com/langchain4j/langchain4j/discussions) +to get help. + + +## Request Features +Please let us know what features you need by [opening an issue](https://github.com/langchain4j/langchain4j/issues/new/choose). + + +## Contribute +Contribution guidelines can be found [here](https://github.com/langchain4j/langchain4j/blob/main/CONTRIBUTING.md). diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..63c0341 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`langchain4j/langchain4j` +- 原始仓库:https://github.com/langchain4j/langchain4j +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6d5b1c4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,39 @@ +# Security Policy + +## Supported Versions + +Only the latest stable version of LangChain4j is supported with security updates. + +| Version | Supported | +| --------- | ------------------ | +| 1.0.x | :white_check_mark: | +| < 1.0.0 | :x: | + +## Reporting a Vulnerability + +Please do not report security issues to the public issue tracker. +If you think you have found a security vulnerability, please send security issues to [info@langchain4j.dev](mailto:info@langchain4j.dev). +We will do our best to get back to you as soon as possible. + +## Public key used to sign LangChain4j releases + +- **Fingerprint:** `2553 29E5 0411 A7BC 7238 74C0 CF11 8D1B B973 026E` +- **Key ID:** `CF118D1BB973026E` +- **User ID:** LangChain4j +- **Keyserver:** https://keyserver.ubuntu.com/pks/lookup?search=CF118D1BB973026E&fingerprint=on&op=index + +``` +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEaBSzjxYJKwYBBAHaRw8BAQdAM/2/Y9vimDqlD5rq6W9wv/3S3XJBxp3L7LmI +rasKHOu0IkxhbmdDaGFpbjRqIDxpbmZvQGxhbmdjaGFpbjRqLmRldj6IkwQTFgoA +OxYhBCVTKeUEEae8cjh0wM8RjRu5cwJuBQJoFLOPAhsDBQsJCAcCAiICBhUKCQgL +AgQWAgMBAh4HAheAAAoJEM8RjRu5cwJu9ZAA/386qRNBrOmuyivyKKLnw9TLI39m +03akxSOy1V8i/JuyAP4gbeyGjnTrXB2TD1/ZMz2LbCF+dAXGshi6mZZ9BkEfDbg4 +BGgUs48SCisGAQQBl1UBBQEBB0Ao2acTNRUOJ3o+zQs4gX5HSi3CuaHJQvxGuume +W6wIfAMBCAeIeAQYFgoAIBYhBCVTKeUEEae8cjh0wM8RjRu5cwJuBQJoFLOPAhsM +AAoJEM8RjRu5cwJugCAA+gO5uhtyLQTo+zEclAqQY7+dsJhpO3jf0mzj4n1OTx6d +AQCd+f/ty5wml6ACroRZHYr2LQPjt/uqz5CW/3gTyl9mDg== +=py+h +-----END PGP PUBLIC KEY BLOCK----- +``` \ No newline at end of file diff --git a/check-split-packages.sh b/check-split-packages.sh new file mode 100755 index 0000000..9e2ad46 --- /dev/null +++ b/check-split-packages.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# Usage: ./check-split-packages.sh /path/to/your/root/dir +ROOT_DIR="${1:-.}" # default to current dir if no argument passed + +# Use process substitution instead of pipes to avoid subshell issues +declare -A package_map +declare -A seen +declare -A last_conflict_jar # Track the last conflicting jar for each package + +echo "🔍 Scanning all JARs under: $ROOT_DIR (excluding test JARs)" + +# Get all JAR files first, excluding test JARs +jar_files=() +while IFS= read -r jar; do + # Skip JAR files ending with "-tests.jar" and anything in the integration-tests directory + if [[ "$jar" != *"-tests.jar" ]] && [[ "$jar" != "./integration-tests/"* ]]; then + jar_files+=("$jar") + fi +done < <(find "$ROOT_DIR" -type f -name "*.jar") + +echo "Found ${#jar_files[@]} non-test JAR files to analyze" + +# Process each JAR file +for jar in "${jar_files[@]}"; do + jarname=$(realpath "$jar") + jarbasename=$(basename "$jarname") # Get just the filename without path + tmpdir=$(mktemp -d) + unzip -qq "$jar" -d "$tmpdir" + + # Get all class files + class_files=() + while IFS= read -r classfile; do + class_files+=("$classfile") + done < <(find "$tmpdir" -type f -name "*.class") + + # Process each class file + for classfile in "${class_files[@]}"; do + pkg=$(dirname "${classfile#$tmpdir/}" | tr '/' '.') + [[ "$pkg" == "." ]] && continue # skip default package + + if [ -n "${package_map[$pkg]}" ]; then + if [ "${package_map[$pkg]}" != "$jarbasename" ]; then + if [[ -z "${seen[$pkg]}" ]]; then + # First time seeing this conflict + echo "🚨 Split package detected: $pkg" + echo " ↳ in: ${package_map[$pkg]}" + echo " ↳ and: $jarbasename" + seen[$pkg]=1 + last_conflict_jar[$pkg]="$jarbasename" + elif [[ "${last_conflict_jar[$pkg]}" != "$jarbasename" ]]; then + # New jar with same conflict + echo " ↳ also in: $jarbasename" + last_conflict_jar[$pkg]="$jarbasename" + fi + # If it's the same jar as last time, we don't print anything + fi + else + package_map[$pkg]=$jarbasename; + fi + done + + rm -rf "$tmpdir" +done + +if [[ ${#seen[@]} -eq 0 ]]; then + echo "✅ No split packages found!" +else + echo "❌ Split packages detected — please fix before modularizing." + exit 1 +fi diff --git a/code-execution-engines/langchain4j-code-execution-engine-azure-acads/pom.xml b/code-execution-engines/langchain4j-code-execution-engine-azure-acads/pom.xml new file mode 100644 index 0000000..c7c693d --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-azure-acads/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + + dev.langchain4j + langchain4j-parent + 1.18.0-beta28-SNAPSHOT + ../../langchain4j-parent/pom.xml + + + langchain4j-code-execution-engine-azure-acads + 1.18.0-beta28-SNAPSHOT + LangChain4j :: Integration :: Azure ACADS + Implementation of a code execution engine using Azure ACA Dynamic Sessions + + + + + io.netty + netty-bom + 4.1.132.Final + pom + import + + + net.java.dev.jna + jna + 5.17.0 + + + net.java.dev.jna + jna-platform + 5.17.0 + + + com.microsoft.azure + msal4j + 1.23.1 + + + + + + + + dev.langchain4j + langchain4j-core + 1.18.0-SNAPSHOT + + + + dev.langchain4j + langchain4j-http-client + 1.18.0-SNAPSHOT + + + + dev.langchain4j + langchain4j-http-client-jdk + 1.18.0-SNAPSHOT + runtime + + + + com.azure + azure-core + + + + com.azure + azure-identity + + + + com.fasterxml.jackson.core + jackson-databind + + + + + diff --git a/code-execution-engines/langchain4j-code-execution-engine-azure-acads/src/main/java/dev/langchain4j/code/azure/acads/SessionsREPLTool.java b/code-execution-engines/langchain4j-code-execution-engine-azure-acads/src/main/java/dev/langchain4j/code/azure/acads/SessionsREPLTool.java new file mode 100644 index 0000000..1972d98 --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-azure-acads/src/main/java/dev/langchain4j/code/azure/acads/SessionsREPLTool.java @@ -0,0 +1,587 @@ +package dev.langchain4j.code.azure.acads; + +import static dev.langchain4j.http.client.HttpClientBuilderLoader.loadHttpClientBuilder; + +import com.azure.core.credential.AccessToken; +import com.azure.core.credential.TokenRequestContext; +import com.azure.identity.DefaultAzureCredential; +import com.azure.identity.DefaultAzureCredentialBuilder; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.langchain4j.agent.tool.Tool; +import dev.langchain4j.code.CodeExecutionEngine; +import dev.langchain4j.http.client.HttpClient; +import dev.langchain4j.http.client.HttpMethod; +import dev.langchain4j.http.client.HttpRequest; +import dev.langchain4j.http.client.SuccessfulHttpResponse; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A tool for executing code in Azure ACA dynamic sessions. + * + * Overview: + * SessionsREPLTool provides a mechanism to execute code snippets within an Azure + * Container Apps (ACA) dynamic session. It facilitates interaction with a remote code execution environment, + * allowing for tasks such as performing calculations, running scripts, and managing files. + * This class implements {@link CodeExecutionEngine}, making it compatible with the standard + * code execution interfaces in langchain4j. + * + * Key Components: + * USER_AGENT: A static string defining the User-Agent header for HTTP requests. + * API_VERSION: A static string specifying the API version for interacting with the ACA dynamic sessions endpoint. + * SANITIZE_PATTERN_START and SANITIZE_PATTERN_END: Static patterns used to sanitize input code by removing leading/trailing whitespace or keywords. + * sanitizeInput: A boolean flag indicating whether to sanitize the input code before execution. + * poolManagementEndpoint: The URL of the pool management endpoint for the ACA dynamic sessions. + * sessionId: The unique identifier for the ACA dynamic session. + * nativeHttpClient: A standard Java HttpClient instance for multipart form data operations. + * langchainHttpClient: A langchain4j HttpClient abstraction for standard HTTP operations. + * credential: A DefaultAzureCredential instance for authenticating with Azure services. + * accessTokenRef: An AtomicReference to store and manage the access token. + * FileUploader, FileDownloader, FileLister: Interfaces defining file management operations. + * DefaultFileUploader, DefaultFileDownloader, DefaultFileLister: Implementations of the file management interfaces. + * RemoteFileMetadata: A class representing metadata for remote files. + * + * Functionality: + * The SessionsREPLTool class provides the following core functionalities: + * Code Execution: Executes code snippets within the ACA dynamic session and returns the results. + * - Implements {@link CodeExecutionEngine#execute(String)} for standardized execution API + * - Provides a {@link Tool} annotated {@link #use(String)} method for integration with AI services + * File Management: Allows uploading, downloading, and listing files within the session's environment. + * Authentication: Handles authentication with Azure services using the DefaultAzureCredential. + * Input Sanitization: Provides an option to sanitize input code for security and compatibility. + * + * Usage: + * To use the SessionsREPLTool, you need to: + * Create an instance of the class, providing the pool management endpoint. + * Call the use() or execute() method to execute code, passing the code snippet as a string. + * Use the file management interfaces and implementations to interact with files in the session. + * Ensure proper error handling and resource management. + * For testing, use the protected constructor that accepts pre-configured dependencies. + */ +public class SessionsREPLTool implements CodeExecutionEngine { + + private static final String USER_AGENT = "langchain4j-azure-dynamic-sessions"; + private static final String API_VERSION = "2024-02-02-preview"; + private static final Logger log = LoggerFactory.getLogger(SessionsREPLTool.class); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private static final TypeReference> MAP_TYPE_REF = new TypeReference>() {}; + private static final TypeReference>> LIST_MAP_TYPE_REF = + new TypeReference>>() {}; + + private static final Pattern SANITIZE_PATTERN_START = Pattern.compile("^(\\s|`)*(?i:python)?\\s*"); + private static final Pattern SANITIZE_PATTERN_END = Pattern.compile("(\\s|`)*$"); + + private final boolean sanitizeInput; + private final String poolManagementEndpoint; + private final String sessionId; + private final java.net.http.HttpClient nativeHttpClient; + private final HttpClient langchainHttpClient; + private final DefaultAzureCredential credential; + private final AtomicReference accessTokenRef = new AtomicReference<>(); + + /** + * Constructs a new SessionsREPLTool with the specified endpoint. + * + * @param poolManagementEndpoint the pool management endpoint URL + */ + public SessionsREPLTool(String poolManagementEndpoint) { + this(poolManagementEndpoint, UUID.randomUUID().toString(), true); + } + + /** + * Interface for uploading files to ACA. + */ + public interface FileUploader { + /** + * Uploads a local file to Azure Container Apps. + * + * @param localFilePath the path to the local file to upload + * @return metadata about the uploaded file + */ + RemoteFileMetadata uploadFileToAca(Path localFilePath); + } + + /** + * Interface for downloading files from ACA. + */ + public interface FileDownloader { + /** + * Downloads a file from Azure Container Apps. + * + * @param remoteFilePath the path of the file to download + * @return the content of the downloaded file + */ + String downloadFile(String remoteFilePath); + } + + /** + * Interface for listing files in ACA. + */ + public interface FileLister { + /** + * Lists all files in the Azure Container Apps session. + * + * @return a string representation of the file listing + */ + String listFiles(); + } + + /** + * Implementation of CodeExecutionEngine.execute method + * @param code The code to execute. + * @return The result of the execution as a String in JSON format. + */ + @Override + public String execute(String code) { + return use(code); + } + + /** + * Constructs a new SessionsREPLTool with specified parameters. + * + * @param poolManagementEndpoint the pool management endpoint URL + * @param sessionId the session ID + * @param sanitizeInput whether to sanitize the input code + */ + public SessionsREPLTool(String poolManagementEndpoint, String sessionId, boolean sanitizeInput) { + this.poolManagementEndpoint = poolManagementEndpoint; + this.sessionId = sessionId; + this.sanitizeInput = sanitizeInput; + this.nativeHttpClient = java.net.http.HttpClient.newBuilder().build(); + this.langchainHttpClient = loadHttpClientBuilder().build(); + this.credential = new DefaultAzureCredentialBuilder().build(); + } + + /** + * Protected constructor for testing purposes that allows injecting dependencies. + * + * @param poolManagementEndpoint the pool management endpoint URL + * @param sessionId the session ID + * @param sanitizeInput whether to sanitize the input code + * @param httpClient a pre-configured HTTP client + * @param credential a pre-configured credential + */ + protected SessionsREPLTool( + String poolManagementEndpoint, + String sessionId, + boolean sanitizeInput, + HttpClient httpClient, + DefaultAzureCredential credential) { + this.poolManagementEndpoint = poolManagementEndpoint; + this.sessionId = sessionId; + this.sanitizeInput = sanitizeInput; + this.nativeHttpClient = java.net.http.HttpClient.newBuilder().build(); + this.langchainHttpClient = httpClient; + this.credential = credential; + } + + // Method is now a no-op as langchain4j HTTP client handles resource cleanup internally + // public void shutdown() { + // No manual cleanup needed for the HTTP client + // } + + /** + * Executes the input code and returns the result. + * + * @param input the code or query to execute + * @return the execution result as a JSON string + */ + @Tool(name = "sessions_REPL") + public String use(String input) { + Map response = executeCode(input); + + Object result = response.get("result"); + if (result instanceof Map) { + Map resultMap = (Map) result; + if ("image".equals(resultMap.get("type")) && resultMap.containsKey("base64_data")) { + resultMap.remove("base64_data"); + } + } + + Map contentMap = new HashMap<>(); + contentMap.put("result", result); + contentMap.put("stdout", response.get("stdout")); + contentMap.put("stderr", response.get("stderr")); + + try { + return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(contentMap); + } catch (Exception e) { + throw new RuntimeException("Failed to serialize response.", e); + } + } + + /** + * Returns the value of the tool. + * + * @return an array containing the tool name + */ + public String[] value() { + return new String[] {"REPL Tool"}; + } + + private String getAccessToken() { + AccessToken token = accessTokenRef.get(); + if (token == null || token.isExpired()) { + try { + TokenRequestContext context = + new TokenRequestContext().addScopes("https://dynamicsessions.io/.default"); + token = credential.getToken(context).block(); + if (token != null) { + accessTokenRef.set(token); + } else { + throw new RuntimeException("Failed to acquire access token."); + } + } catch (Exception e) { + throw new RuntimeException("Failed to acquire access token.", e); + } + } + return token.getToken(); + } + + private String buildUrl(String path) { + String endpoint = poolManagementEndpoint; + if (!endpoint.endsWith("/")) { + endpoint += "/"; + } + String encodedSessionId = URLEncoder.encode(sessionId, StandardCharsets.UTF_8); + String query = "identifier=" + encodedSessionId + "&api-version=" + API_VERSION; + return endpoint + path + "?" + query; + } + + private String sanitizeInput(String input) { + input = SANITIZE_PATTERN_START.matcher(input).replaceAll(""); + input = SANITIZE_PATTERN_END.matcher(input).replaceAll(""); + return input; + } + + /** + * Executes a query to the code interpreter. + * + * @param sessionCode the code or query to execute + * @return a map containing the execution results + */ + public Map executeCode(String sessionCode) { + if (sanitizeInput) { + sessionCode = sanitizeInput(sessionCode); + } + + String accessToken = getAccessToken(); + String apiUrl = buildUrl("code/execute"); + + Map body = new HashMap<>(); + Map properties = new HashMap<>(); + properties.put("codeInputType", "inline"); + properties.put("executionType", "synchronous"); + properties.put("code", sessionCode); + body.put("properties", properties); + + try { + String requestBody = OBJECT_MAPPER.writeValueAsString(body); + + HttpRequest request = HttpRequest.builder() + .method(HttpMethod.POST) + .url(apiUrl) + .addHeader("Authorization", "Bearer " + accessToken) + .addHeader("Content-Type", "application/json") + .addHeader("User-Agent", USER_AGENT) + .body(requestBody) + .build(); + + SuccessfulHttpResponse response = langchainHttpClient.execute(request); + + if (response.statusCode() >= 200 && response.statusCode() < 300) { + Map responseJson = OBJECT_MAPPER.readValue(response.body(), MAP_TYPE_REF); + return getNestedMapProperty(responseJson, "properties"); + } else { + throw new RuntimeException( + "Request failed with status code " + response.statusCode() + ": " + response.body()); + } + + } catch (Exception e) { + throw new RuntimeException("Failed to execute code.", e); + } + } + + /** + * Default implementation of the FileUploader interface that uploads files to Azure Container Apps. + */ + public class DefaultFileUploader implements FileUploader { + + /** + * Constructs a new DefaultFileUploader. + * Uses the parent SessionsREPLTool's configuration for authentication and endpoint access. + */ + public DefaultFileUploader() { + // Default constructor - uses parent SessionsREPLTool's configuration + } + + @Override + public RemoteFileMetadata uploadFileToAca(Path localFilePath) { + String remoteFilePath = localFilePath.getFileName().toString(); + log.debug("Found file at local path: " + remoteFilePath); + String accessToken = getAccessToken(); + String apiUrl = buildUrl("files/upload"); + log.debug("Uploading: API URL:" + apiUrl); + + File file = localFilePath.toFile(); // Convert Path to File + + try { + // Prepare multipart form data + + String boundary = "----" + System.currentTimeMillis(); + String contentDisposition = + "Content-Disposition: form-data; name=\"file\"; filename=\"" + file.getName() + "\""; + + byte[] fileBytes = Files.readAllBytes(localFilePath); + String separator = "--" + boundary + "\r\n"; + String contentType = "Content-Type: application/json\r\n\r\n"; + String end = "\r\n--" + boundary + "--\r\n"; + + byte[] formDataBytes = + (separator + contentDisposition + "\r\n" + contentType).getBytes(StandardCharsets.UTF_8); + byte[] endBytes = end.getBytes(StandardCharsets.UTF_8); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(formDataBytes); + outputStream.write(fileBytes); + outputStream.write(endBytes); + byte[] requestBody = outputStream.toByteArray(); + + java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder() + .uri(URI.create(apiUrl)) + .header("Authorization", "Bearer " + accessToken) + .header("Content-Type", "multipart/form-data; boundary=" + boundary) + .POST(java.net.http.HttpRequest.BodyPublishers.ofByteArray(requestBody)) + .build(); + + java.net.http.HttpResponse response = + nativeHttpClient.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new IOException("Unexpected code " + response); + } + + Map responseJson = OBJECT_MAPPER.readValue(response.body(), MAP_TYPE_REF); + List> valueList = + OBJECT_MAPPER.convertValue(responseJson.get("value"), LIST_MAP_TYPE_REF); + Map fileMetadataMap = valueList.get(0); + return RemoteFileMetadata.fromDict(fileMetadataMap); + + } catch (Exception e) { + throw new RuntimeException("Failed to upload file: " + e.getMessage() + " API URL: " + apiUrl, e); + } + } + } + + /** + * Default implementation of the FileDownloader interface that downloads files from Azure Container Apps. + */ + public class DefaultFileDownloader implements FileDownloader { + + /** + * Constructs a new DefaultFileDownloader. + * Uses the parent SessionsREPLTool's configuration for authentication and endpoint access. + */ + public DefaultFileDownloader() { + // Default constructor - uses parent SessionsREPLTool's configuration + } + + @Override + public String downloadFile(String remoteFilePath) { + String accessToken = getAccessToken(); + String apiUrl = buildUrl("files/content/" + remoteFilePath); + log.debug("Downloading: API URL:" + apiUrl); + + HttpRequest request = HttpRequest.builder() + .method(HttpMethod.GET) + .url(apiUrl) + .addHeader("Authorization", "Bearer " + accessToken) + .build(); + + try { + SuccessfulHttpResponse response = langchainHttpClient.execute(request); + + if (response.statusCode() == 404) { + return "File not found: " + remoteFilePath; + } + + // Convert response body to Base64 from the canonical raw bytes. + // Using response.body() would round-trip through a decoded String and corrupt + // binary files (images, xlsx, etc.) whose bytes are not valid UTF-8. + return Base64.getEncoder().encodeToString(response.bodyBytes()); + + } catch (Exception e) { + if (e.getMessage().contains("404")) { + return "File not found: " + remoteFilePath; + } + throw new RuntimeException("Failed to download file: " + e.getMessage() + " API URL: " + apiUrl, e); + } + } + } + + /** + * Default implementation of the FileLister interface that lists files in Azure Container Apps. + */ + public class DefaultFileLister implements FileLister { + + /** + * Constructs a new DefaultFileLister. + * Uses the parent SessionsREPLTool's configuration for authentication and endpoint access. + */ + public DefaultFileLister() { + // Default constructor - uses parent SessionsREPLTool's configuration + } + + @Override + public String listFiles() { + String accessToken = getAccessToken(); + String apiUrl = buildUrl("files"); + + HttpRequest request = HttpRequest.builder() + .method(HttpMethod.GET) + .url(apiUrl) + .addHeader("Authorization", "Bearer " + accessToken) + .build(); + + try { + SuccessfulHttpResponse response = langchainHttpClient.execute(request); + + // Parse the response body as JSON using Jackson + JsonNode json = OBJECT_MAPPER.readTree(response.body()); + + // Create a StringBuilder to store the filenames + StringBuilder filenames = new StringBuilder(); + + // Get the "value" array from the JSON object + JsonNode valueArray = json.get("value"); + + // Check if the "value" array is empty + if (valueArray.isEmpty()) { + return "No files were found at " + apiUrl; + } + + // Loop through each object in the "value" array + for (int i = 0; i < valueArray.size(); i++) { + // Get the current object + JsonNode currentObject = valueArray.get(i); + + // Get the "properties" object from the current object + JsonNode properties = currentObject.get("properties"); + + // Get the filename from the "properties" object + String filename = properties.get("filename").asText(); + + // Append the filename to the StringBuilder + filenames.append(filename); + + // If this is not the last filename, append a comma and a space + if (i < valueArray.size() - 1) { + filenames.append(", "); + } + } + + // Return the string representation of the StringBuilder + return filenames.toString(); + + } catch (Exception e) { + throw new RuntimeException("Failed to list files: " + e.getMessage() + " API URL: " + apiUrl, e); + } + } + } + + private static Map getNestedMapProperty(Map map, String key) { + Object value = map.get(key); + if (value instanceof Map) { + // This is safe because we know the structure from Azure API + @SuppressWarnings("unchecked") + Map result = (Map) value; + return result; + } + return null; + } + + /** + * Represents metadata of a remote file. + */ + public static class RemoteFileMetadata { + private final String filename; + private final long sizeInBytes; + + /** + * Constructs a RemoteFileMetadata instance. + * + * @param filename the name of the file + * @param sizeInBytes the size of the file in bytes + */ + public RemoteFileMetadata(String filename, long sizeInBytes) { + this.filename = filename; + this.sizeInBytes = sizeInBytes; + } + + /** + * Gets the filename. + * + * @return the filename + */ + public String getFilename() { + return filename; + } + + /** + * Gets the size of the file in bytes. + * + * @return the size in bytes + */ + public long getSizeInBytes() { + return sizeInBytes; + } + + /** + * Gets the full path of the file. + * + * @return the full file path + */ + public String getFullPath() { + return "/mnt/data/" + filename; + } + + /** + * Creates a RemoteFileMetadata instance from a map. + * + * @param data the map containing file properties + * @return a RemoteFileMetadata instance + */ + public static RemoteFileMetadata fromDict(Map data) { + Object propertiesObj = data.get("properties"); + if (!(propertiesObj instanceof Map)) { + throw new IllegalArgumentException("Data does not contain a valid properties map"); + } + + // This cast is safe because we checked the type above + @SuppressWarnings("unchecked") + Map properties = (Map) propertiesObj; + + String filename = (String) properties.get("filename"); + Number size = (Number) properties.get("size"); + return new RemoteFileMetadata(filename, size.longValue()); + } + } +} diff --git a/code-execution-engines/langchain4j-code-execution-engine-azure-acads/src/test/java/dev/langchain4j/code/azure/acads/SessionsREPLToolTest.java b/code-execution-engines/langchain4j-code-execution-engine-azure-acads/src/test/java/dev/langchain4j/code/azure/acads/SessionsREPLToolTest.java new file mode 100644 index 0000000..563487a --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-azure-acads/src/test/java/dev/langchain4j/code/azure/acads/SessionsREPLToolTest.java @@ -0,0 +1,207 @@ +package dev.langchain4j.code.azure.acads; + +/** + * Test Suite for SessionsREPLTool + * + * Testing Strategy: + * ----------------- + * This test suite focuses on unit testing the SessionsREPLTool class which implements + * the CodeExecutionEngine interface. The tests utilize a mock-based approach to isolate + * the class under test from external dependencies like Azure services. + * + * Key test areas covered: + * 1. Interface contract verification - Ensuring SessionsREPLTool correctly implements CodeExecutionEngine + * 2. Input sanitization - Verifying code snippets are properly sanitized before execution + * 3. HTTP request formation - Confirming requests to the Azure Container Apps service are properly formed + * 4. Response handling - Testing that responses are correctly parsed and transformed + * + * Implementation approach: + * - Uses a custom TestSessionsREPLTool subclass that allows dependency injection via reflection + * - Mocks HTTP client responses to simulate Azure Container Apps service responses + * - Uses ArgumentCaptor to inspect request details for verification + * + * This approach allows comprehensive testing of the SessionsREPLTool's functionality + * without requiring actual Azure credentials or incurring costs from real service calls. + */ +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import com.azure.core.credential.AccessToken; +import com.azure.core.credential.TokenRequestContext; +import com.azure.identity.DefaultAzureCredential; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.langchain4j.code.CodeExecutionEngine; +import dev.langchain4j.http.client.HttpClient; +import dev.langchain4j.http.client.HttpRequest; +import dev.langchain4j.http.client.SuccessfulHttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import reactor.core.publisher.Mono; + +class SessionsREPLToolTest { + + private SessionsREPLTool sessionsREPLTool; + private DefaultAzureCredential mockCredential; + private HttpClient mockHttpClient; + private static final String TEST_ENDPOINT = "https://test-endpoint.com"; + private static final String TEST_CODE = "print('Hello World')"; + private static final String TEST_TOKEN = "test-token"; + + @BeforeEach + void setUp() throws Exception { + // Create mocks + mockCredential = mock(DefaultAzureCredential.class); + mockHttpClient = mock(HttpClient.class); + + // Set up the mock credential + AccessToken accessToken = + new AccessToken(TEST_TOKEN, OffsetDateTime.now().plusHours(1)); + when(mockCredential.getToken(any(TokenRequestContext.class))).thenReturn(Mono.just(accessToken)); + + // Create a custom SessionsREPLTool with mocked dependencies + sessionsREPLTool = new TestSessionsREPLTool(TEST_ENDPOINT, mockCredential, mockHttpClient); + } + + @Test + void testExecuteMethodImplementsCodeExecutionEngineInterface() { + // Verify that SessionsREPLTool implements CodeExecutionEngine + assertThat(sessionsREPLTool).isInstanceOf(CodeExecutionEngine.class); + + // Set up mock response for execute call + setupMockHttpResponse(); + + // Execute code using the CodeExecutionEngine interface method + String result = sessionsREPLTool.execute(TEST_CODE); + + // Verify result contains expected JSON structure + assertThat(result).contains("\"result\""); + assertThat(result).contains("\"stdout\""); + assertThat(result).contains("\"stderr\""); + } + + @Test + void testUseMethodCallsExecuteCode() { + // Set up mock response + setupMockHttpResponse(); + + // Call the use method + String result = sessionsREPLTool.use(TEST_CODE); + + // Verify HTTP request was made + verify(mockHttpClient).execute(any(HttpRequest.class)); + + // Verify result contains expected JSON structure + assertThat(result).contains("\"result\""); + assertThat(result).contains("\"stdout\""); + assertThat(result).contains("\"stderr\""); + } + + @Test + void testExecuteCodeSanitizesInput() { + // Set up mock response + setupMockHttpResponse(); + + // Input with whitespace and python prefix + String inputWithPrefix = "```python\nprint('Hello World')\n```"; + + // Execute code + Map result = sessionsREPLTool.executeCode(inputWithPrefix); + + // Capture HTTP request to verify sanitized input + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + verify(mockHttpClient).execute(requestCaptor.capture()); + + // Verify that the request body contains the sanitized code + String requestBody = requestCaptor.getValue().body(); + assertThat(requestBody).contains("print('Hello World')"); + assertThat(requestBody).doesNotContain("```python"); + assertThat(requestBody).doesNotContain("```"); + + // Verify result structure + assertThat(result).containsKey("result"); + assertThat(result).containsKey("stdout"); + assertThat(result).containsKey("stderr"); + } + + @Test + void testDownloadFilePreservesBinaryBytes() { + // A blob with bytes that are not valid UTF-8 (a fake PNG-ish header). + byte[] binaryBytes = new byte[] {(byte) 0xFF, (byte) 0xFE, (byte) 0x00, (byte) 0x01, (byte) 0xC0}; + + // Return a real SuccessfulHttpResponse so body()/bodyBytes() behave as in production. + SuccessfulHttpResponse response = SuccessfulHttpResponse.builder() + .statusCode(200) + .body(binaryBytes) + .headers(Collections.emptyMap()) + .build(); + when(mockHttpClient.execute(any(HttpRequest.class))).thenReturn(response); + + SessionsREPLTool.DefaultFileDownloader downloader = sessionsREPLTool.new DefaultFileDownloader(); + + String result = downloader.downloadFile("some/file.png"); + + // Base64 must be computed from the canonical raw bytes, not a lossy String round-trip. + assertThat(result).isEqualTo(Base64.getEncoder().encodeToString(binaryBytes)); + } + + @Test + void testDownloadFileRoundTripsValidUtf8Text() { + byte[] textBytes = "hello, world".getBytes(StandardCharsets.UTF_8); + + SuccessfulHttpResponse response = SuccessfulHttpResponse.builder() + .statusCode(200) + .body(textBytes) + .headers(Collections.emptyMap()) + .build(); + when(mockHttpClient.execute(any(HttpRequest.class))).thenReturn(response); + + SessionsREPLTool.DefaultFileDownloader downloader = sessionsREPLTool.new DefaultFileDownloader(); + + String result = downloader.downloadFile("notes.txt"); + + assertThat(result).isEqualTo(Base64.getEncoder().encodeToString(textBytes)); + } + + /** + * Helper method to set up mock HTTP response + */ + private void setupMockHttpResponse() { + // Create mock response data + Map responseProperties = new HashMap<>(); + responseProperties.put("result", "Hello World"); + responseProperties.put("stdout", "Hello World"); + responseProperties.put("stderr", ""); + + Map responseMap = new HashMap<>(); + responseMap.put("properties", responseProperties); + + // Set up mock HTTP response + SuccessfulHttpResponse mockResponse = mock(SuccessfulHttpResponse.class); + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()) + .thenReturn(new ObjectMapper().valueToTree(responseMap).toString()); + when(mockResponse.headers()).thenReturn(Collections.emptyMap()); + + // Set up mock HTTP client + when(mockHttpClient.execute(any(HttpRequest.class))).thenReturn(mockResponse); + } + + /** + * Custom SessionsREPLTool for testing that uses mocked dependencies + */ + private static class TestSessionsREPLTool extends SessionsREPLTool { + public TestSessionsREPLTool( + String poolManagementEndpoint, DefaultAzureCredential credential, HttpClient mockHttpClient) { + // Use the protected constructor for testing that accepts pre-configured dependencies + super(poolManagementEndpoint, "test-session-id", true, mockHttpClient, credential); + } + } +} diff --git a/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/pom.xml b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/pom.xml new file mode 100644 index 0000000..0a1e035 --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + + dev.langchain4j + langchain4j-parent + 1.18.0-beta28-SNAPSHOT + ../../langchain4j-parent/pom.xml + + + langchain4j-code-execution-engine-graalvm-polyglot + LangChain4j :: Integration :: GraalVM Polyglot/Truffle + Implementation of JavaScript and Python code execution engines and tools using GraalVM Polyglot/Truffle + + + 24.1.1 + + + + + + dev.langchain4j + langchain4j-core + 1.18.0-SNAPSHOT + + + + org.graalvm.polyglot + polyglot + ${graalvm.version} + + + + org.graalvm.polyglot + js + ${graalvm.version} + pom + + + + org.graalvm.polyglot + python + ${graalvm.version} + pom + + + + dev.langchain4j + langchain4j + 1.18.0-SNAPSHOT + test + + + + dev.langchain4j + langchain4j-open-ai + 1.18.0-SNAPSHOT + test + + + + + + + + org.honton.chas + license-maven-plugin + + + true + + + + + + + diff --git a/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/main/java/dev/langchain4j/agent/tool/graalvm/GraalVmJavaScriptExecutionTool.java b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/main/java/dev/langchain4j/agent/tool/graalvm/GraalVmJavaScriptExecutionTool.java new file mode 100644 index 0000000..dcce76c --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/main/java/dev/langchain4j/agent/tool/graalvm/GraalVmJavaScriptExecutionTool.java @@ -0,0 +1,20 @@ +package dev.langchain4j.agent.tool.graalvm; + +import dev.langchain4j.agent.tool.P; +import dev.langchain4j.agent.tool.Tool; +import dev.langchain4j.code.CodeExecutionEngine; +import dev.langchain4j.code.graalvm.GraalVmJavaScriptExecutionEngine; + +/** + * A tool that executes provided JavaScript code using GraalVM Polyglot/Truffle. + * Attention! It might be dangerous to execute the code, see {@link GraalVmJavaScriptExecutionEngine} for more details. + */ +public class GraalVmJavaScriptExecutionTool { + + private final CodeExecutionEngine engine = new GraalVmJavaScriptExecutionEngine(); + + @Tool("MUST be used for accurate calculations: math, sorting, filtering, aggregating, string processing, etc") + public String executeJavaScriptCode(@P("JavaScript code to execute, result MUST be returned by the code") String code) { + return engine.execute(code); + } +} diff --git a/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/main/java/dev/langchain4j/agent/tool/graalvm/GraalVmPythonExecutionTool.java b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/main/java/dev/langchain4j/agent/tool/graalvm/GraalVmPythonExecutionTool.java new file mode 100644 index 0000000..d3ad7d1 --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/main/java/dev/langchain4j/agent/tool/graalvm/GraalVmPythonExecutionTool.java @@ -0,0 +1,20 @@ +package dev.langchain4j.agent.tool.graalvm; + +import dev.langchain4j.agent.tool.P; +import dev.langchain4j.agent.tool.Tool; +import dev.langchain4j.code.CodeExecutionEngine; +import dev.langchain4j.code.graalvm.GraalVmPythonExecutionEngine; + +/** + * A tool that executes provided Python code using GraalVM Polyglot/Truffle. + * Attention! It might be dangerous to execute the code, see {@link GraalVmPythonExecutionEngine} for more details. + */ +public class GraalVmPythonExecutionTool { + + private final CodeExecutionEngine engine = new GraalVmPythonExecutionEngine(); + + @Tool("MUST be used for accurate calculations: math, sorting, filtering, aggregating, string processing, etc") + public String executePythonCode(@P("Python code to execute, result MUST be returned by the code") String code) { + return engine.execute(code); + } +} diff --git a/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/main/java/dev/langchain4j/code/graalvm/GraalVmJavaScriptExecutionEngine.java b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/main/java/dev/langchain4j/code/graalvm/GraalVmJavaScriptExecutionEngine.java new file mode 100644 index 0000000..f5526ce --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/main/java/dev/langchain4j/code/graalvm/GraalVmJavaScriptExecutionEngine.java @@ -0,0 +1,34 @@ +package dev.langchain4j.code.graalvm; + +import dev.langchain4j.code.CodeExecutionEngine; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.HostAccess; +import org.graalvm.polyglot.SandboxPolicy; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; + +import static org.graalvm.polyglot.HostAccess.UNTRUSTED; +import static org.graalvm.polyglot.SandboxPolicy.CONSTRAINED; + +/** + * {@link CodeExecutionEngine} that uses GraalVM Polyglot/Truffle to execute provided JavaScript code. + * Attention! It might be dangerous to execute the code, see {@link SandboxPolicy#CONSTRAINED} + * and {@link HostAccess#UNTRUSTED} for more details. + */ +public class GraalVmJavaScriptExecutionEngine implements CodeExecutionEngine { + + @Override + public String execute(String code) { + OutputStream outputStream = new ByteArrayOutputStream(); + try (Context context = Context.newBuilder("js") + .sandbox(CONSTRAINED) + .allowHostAccess(UNTRUSTED) + .out(outputStream) + .err(outputStream) + .build()) { + Object result = context.eval("js", code).as(Object.class); + return String.valueOf(result); + } + } +} diff --git a/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/main/java/dev/langchain4j/code/graalvm/GraalVmPythonExecutionEngine.java b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/main/java/dev/langchain4j/code/graalvm/GraalVmPythonExecutionEngine.java new file mode 100644 index 0000000..2b6daa5 --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/main/java/dev/langchain4j/code/graalvm/GraalVmPythonExecutionEngine.java @@ -0,0 +1,34 @@ +package dev.langchain4j.code.graalvm; + +import dev.langchain4j.code.CodeExecutionEngine; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.HostAccess; +import org.graalvm.polyglot.SandboxPolicy; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; + +import static org.graalvm.polyglot.HostAccess.UNTRUSTED; +import static org.graalvm.polyglot.SandboxPolicy.TRUSTED; + +/** + * {@link CodeExecutionEngine} that uses GraalVM Polyglot/Truffle to execute provided Python code. + * Attention! It might be dangerous to execute the code, see {@link SandboxPolicy#TRUSTED} + * and {@link HostAccess#UNTRUSTED} for more details. + */ +public class GraalVmPythonExecutionEngine implements CodeExecutionEngine { + + @Override + public String execute(String code) { + OutputStream outputStream = new ByteArrayOutputStream(); + try (Context context = Context.newBuilder("python") + .sandbox(TRUSTED) + .allowHostAccess(UNTRUSTED) + .out(outputStream) + .err(outputStream) + .build()) { + Object result = context.eval("python", code).as(Object.class); + return String.valueOf(result); + } + } +} diff --git a/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/test/java/dev/langchain4j/agent/tool/graalvm/GraalVmJavaScriptExecutionToolIT.java b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/test/java/dev/langchain4j/agent/tool/graalvm/GraalVmJavaScriptExecutionToolIT.java new file mode 100644 index 0000000..cad3505 --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/test/java/dev/langchain4j/agent/tool/graalvm/GraalVmJavaScriptExecutionToolIT.java @@ -0,0 +1,47 @@ +package dev.langchain4j.agent.tool.graalvm; + +import static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +import dev.langchain4j.memory.chat.MessageWindowChatMemory; +import dev.langchain4j.model.openai.OpenAiChatModel; +import dev.langchain4j.service.AiServices; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; + +@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") +class GraalVmJavaScriptExecutionToolIT { + + OpenAiChatModel model = OpenAiChatModel.builder() + .baseUrl(System.getenv("OPENAI_BASE_URL")) + .apiKey(System.getenv("OPENAI_API_KEY")) + .organizationId(System.getenv("OPENAI_ORGANIZATION_ID")) + .modelName(GPT_4_O_MINI) + .logRequests(true) + .logResponses(true) + .build(); + + interface Assistant { + + String chat(String userMessage); + } + + @Test + void should_execute_tool() { + + GraalVmJavaScriptExecutionTool tool = spy(new GraalVmJavaScriptExecutionTool()); + + Assistant assistant = AiServices.builder(Assistant.class) + .chatModel(model) + .tools(tool) + .chatMemory(MessageWindowChatMemory.withMaxMessages(10)) + .build(); + + String answer = assistant.chat("What is the square root of 485906798473894056 in scientific notation?"); + + assertThat(answer).contains("6.97"); + + verify(tool).executeJavaScriptCode(contains("485906798473894056")); + } +} diff --git a/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/test/java/dev/langchain4j/agent/tool/graalvm/GraalVmPythonExecutionToolIT.java b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/test/java/dev/langchain4j/agent/tool/graalvm/GraalVmPythonExecutionToolIT.java new file mode 100644 index 0000000..15996cc --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/test/java/dev/langchain4j/agent/tool/graalvm/GraalVmPythonExecutionToolIT.java @@ -0,0 +1,47 @@ +package dev.langchain4j.agent.tool.graalvm; + +import static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +import dev.langchain4j.memory.chat.MessageWindowChatMemory; +import dev.langchain4j.model.openai.OpenAiChatModel; +import dev.langchain4j.service.AiServices; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; + +@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") +class GraalVmPythonExecutionToolIT { + + OpenAiChatModel model = OpenAiChatModel.builder() + .baseUrl(System.getenv("OPENAI_BASE_URL")) + .apiKey(System.getenv("OPENAI_API_KEY")) + .organizationId(System.getenv("OPENAI_ORGANIZATION_ID")) + .modelName(GPT_4_O_MINI) + .logRequests(true) + .logResponses(true) + .build(); + + interface Assistant { + + String chat(String userMessage); + } + + @Test + void should_execute_tool() { + + GraalVmPythonExecutionTool tool = spy(new GraalVmPythonExecutionTool()); + + Assistant assistant = AiServices.builder(Assistant.class) + .chatModel(model) + .tools(tool) + .chatMemory(MessageWindowChatMemory.withMaxMessages(100)) + .build(); + + String answer = assistant.chat("What is the square root of 485906798473894056 in scientific notation?"); + + assertThat(answer).contains("6.97"); + + verify(tool).executePythonCode(contains("485906798473894056")); + } +} diff --git a/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/test/java/dev/langchain4j/code/graalvm/GraalVmJavaScriptExecutionEngineTest.java b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/test/java/dev/langchain4j/code/graalvm/GraalVmJavaScriptExecutionEngineTest.java new file mode 100644 index 0000000..7b10744 --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/test/java/dev/langchain4j/code/graalvm/GraalVmJavaScriptExecutionEngineTest.java @@ -0,0 +1,28 @@ +package dev.langchain4j.code.graalvm; + +import dev.langchain4j.code.CodeExecutionEngine; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class GraalVmJavaScriptExecutionEngineTest { + + CodeExecutionEngine engine = new GraalVmJavaScriptExecutionEngine(); + + @Test + void should_execute_code() { + + String code = """ + function fibonacci(n) { + if (n <= 1) return n; + return fibonacci(n - 1) + fibonacci(n - 2); + } + + fibonacci(10) + """; + + String result = engine.execute(code); + + assertThat(result).isEqualTo("55"); + } +} \ No newline at end of file diff --git a/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/test/java/dev/langchain4j/code/graalvm/GraalVmPythonExecutionEngineTest.java b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/test/java/dev/langchain4j/code/graalvm/GraalVmPythonExecutionEngineTest.java new file mode 100644 index 0000000..562a3a9 --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/test/java/dev/langchain4j/code/graalvm/GraalVmPythonExecutionEngineTest.java @@ -0,0 +1,29 @@ +package dev.langchain4j.code.graalvm; + +import dev.langchain4j.code.CodeExecutionEngine; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class GraalVmPythonExecutionEngineTest { + + CodeExecutionEngine engine = new GraalVmPythonExecutionEngine(); + + @Test + void should_execute_code() { + + String code = """ + def fibonacci(n): + if n <= 1: + return n + else: + return fibonacci(n-1) + fibonacci(n-2) + + fibonacci(10) + """; + + String result = engine.execute(code); + + assertThat(result).isEqualTo("55"); + } +} \ No newline at end of file diff --git a/code-execution-engines/langchain4j-code-execution-engine-judge0/pom.xml b/code-execution-engines/langchain4j-code-execution-engine-judge0/pom.xml new file mode 100644 index 0000000..7bb825f --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-judge0/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + + dev.langchain4j + langchain4j-parent + 1.18.0-beta28-SNAPSHOT + ../../langchain4j-parent/pom.xml + + + langchain4j-code-execution-engine-judge0 + LangChain4j :: Integration :: Judge0 + Implementation of JavaScript code execution engine and tool using Judge0 + + + + + dev.langchain4j + langchain4j-core + 1.18.0-SNAPSHOT + + + + com.squareup.okhttp3 + okhttp + + + + com.fasterxml.jackson.core + jackson-core + + + + com.fasterxml.jackson.core + jackson-databind + + + + org.slf4j + slf4j-api + + + + + + + dev.langchain4j + langchain4j + 1.18.0-SNAPSHOT + test + + + + dev.langchain4j + langchain4j-open-ai + 1.18.0-SNAPSHOT + test + + + + diff --git a/code-execution-engines/langchain4j-code-execution-engine-judge0/src/main/java/dev/langchain4j/code/judge0/JavaScriptCodeFixer.java b/code-execution-engines/langchain4j-code-execution-engine-judge0/src/main/java/dev/langchain4j/code/judge0/JavaScriptCodeFixer.java new file mode 100644 index 0000000..ff3f15a --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-judge0/src/main/java/dev/langchain4j/code/judge0/JavaScriptCodeFixer.java @@ -0,0 +1,44 @@ +package dev.langchain4j.code.judge0; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class JavaScriptCodeFixer { + + private static final Logger log = LoggerFactory.getLogger(JavaScriptCodeFixer.class); + + static String fixIfNoLogToConsole(String code) { + if (code.contains("\n")) { + return fixIfNoLogToConsole(code, "\n"); + } + return fixSingleLineIfNoLogToConsole(code); + } + + private static String fixIfNoLogToConsole(String code, String separator) { + String[] parts = code.split(separator); + String lastPart = parts[parts.length - 1]; + if (lastPart.startsWith("console.log")) { + return code; + } + + parts[parts.length - 1] = "console.log(" + lastPart.replace(";", "") + ");"; + String fixedCode = String.join(separator, parts); + log.debug("The following code \"{}\" was fixed: \"{}\"", code, fixedCode); + return fixedCode; + } + + private static String fixSingleLineIfNoLogToConsole(String code) { + int lastSemicolon = code.lastIndexOf(';'); + String trailing = code.substring(lastSemicolon + 1); + String statement = trailing.trim(); + if (statement.isEmpty() || statement.startsWith("console.log")) { + return code; + } + + String prefix = code.substring(0, lastSemicolon + 1); + String leadingWhitespace = trailing.substring(0, trailing.indexOf(statement)); + String fixedCode = prefix + leadingWhitespace + "console.log(" + statement + ");"; + log.debug("The following code \"{}\" was fixed: \"{}\"", code, fixedCode); + return fixedCode; + } +} diff --git a/code-execution-engines/langchain4j-code-execution-engine-judge0/src/main/java/dev/langchain4j/code/judge0/Json.java b/code-execution-engines/langchain4j-code-execution-engine-judge0/src/main/java/dev/langchain4j/code/judge0/Json.java new file mode 100644 index 0000000..2d1949d --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-judge0/src/main/java/dev/langchain4j/code/judge0/Json.java @@ -0,0 +1,31 @@ +package dev.langchain4j.code.judge0; + + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT; + +class Json { + + static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + .enable(INDENT_OUTPUT) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + + static String toJson(Object o) { + try { + return OBJECT_MAPPER.writeValueAsString(o); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + static T fromJson(String json, Class type) { + try { + return OBJECT_MAPPER.readValue(json, type); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/code-execution-engines/langchain4j-code-execution-engine-judge0/src/main/java/dev/langchain4j/code/judge0/Judge0JavaScriptEngine.java b/code-execution-engines/langchain4j-code-execution-engine-judge0/src/main/java/dev/langchain4j/code/judge0/Judge0JavaScriptEngine.java new file mode 100644 index 0000000..ffa2989 --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-judge0/src/main/java/dev/langchain4j/code/judge0/Judge0JavaScriptEngine.java @@ -0,0 +1,314 @@ +package dev.langchain4j.code.judge0; + +import static dev.langchain4j.internal.Utils.isNullOrBlank; + +import dev.langchain4j.code.CodeExecutionEngine; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import okhttp3.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A JavaScript code execution engine that uses Judge0 API for code execution. + *

+ * This class connects to the Judge0 API via RapidAPI to execute JavaScript code. + */ +class Judge0JavaScriptEngine implements CodeExecutionEngine { + + private static final Logger log = LoggerFactory.getLogger(Judge0JavaScriptEngine.class); + + // HTTP Constants + private static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json"); + private static final String RAPID_API_HOST = "judge0-ce.p.rapidapi.com"; + private static final String API_URL = + "https://judge0-ce.p.rapidapi.com/submissions?base64_encoded=true&wait=true&fields=*"; + + // Judge0 status codes + private static final int STATUS_ACCEPTED = 3; + + // Error messages + private static final String ERROR_RATE_LIMIT = "Rate limit exceeded. Please try again later."; + private static final String ERROR_FORBIDDEN = "Access forbidden. Please check your API key."; + private static final String ERROR_NOT_FOUND = "Resource not found. Please check the endpoint URL."; + private static final String ERROR_SERVER = "Internal server error. Please try again later."; + private static final String ERROR_UNAVAILABLE = "Service unavailable. Please try again later."; + private static final String ERROR_NULL_RESPONSE = "Response body is null"; + private static final String ERROR_NO_STDOUT = "No result: nothing was printed out to the console"; + + // Retry settings + private static final int MAX_RETRIES = 3; + private static final long RETRY_DELAY_MS = 1000; + + private final String apiKey; + private final int languageId; + private final OkHttpClient client; + + /** + * Creates a new Judge0JavaScriptEngine with the specified API key, language ID, and timeout. + * + * @param apiKey The RapidAPI key for Judge0 + * @param languageId The language ID for the Judge0 API (e.g., 102 for JavaScript) + * @param timeout The timeout duration for HTTP requests + * @throws IllegalArgumentException if apiKey is null or blank + * @throws NullPointerException if timeout is null + */ + Judge0JavaScriptEngine(String apiKey, int languageId, Duration timeout) { + if (isNullOrBlank(apiKey)) { + throw new IllegalArgumentException("API key must not be null or blank"); + } + Objects.requireNonNull(timeout, "Timeout must not be null"); + + this.apiKey = apiKey; + this.languageId = languageId; + this.client = new OkHttpClient.Builder() + .connectTimeout(timeout) + .readTimeout(timeout) + .writeTimeout(timeout) + .callTimeout(timeout) + .build(); + } + + /** + * Executes the provided code using the Judge0 API. + * + * @param code The code to execute + * @return The result of code execution or an error message + */ + @Override + public String execute(String code) { + if (isNullOrBlank(code)) { + return "Error: Cannot execute empty or null code"; + } + + String base64EncodedCode = Base64.getEncoder().encodeToString(code.getBytes(StandardCharsets.UTF_8)); + Submission submission = new Submission(languageId, base64EncodedCode); + RequestBody requestBody = RequestBody.create(Json.toJson(submission), MEDIA_TYPE_JSON); + Request request = buildRequest(requestBody); + + return executeWithRetry(request); + } + + /** + * Builds a request to the Judge0 API. + * + * @param requestBody The request body + * @return The built request + */ + private Request buildRequest(RequestBody requestBody) { + return new Request.Builder() + .url(API_URL) + .addHeader("x-rapidapi-host", RAPID_API_HOST) + .addHeader("x-rapidapi-key", apiKey) + .post(requestBody) + .build(); + } + + /** + * Executes a request with retry logic. + * + * @param request The request to execute + * @return The result of the request or an error message + */ + private String executeWithRetry(Request request) { + Exception lastException = null; + + for (int attempt = 0; attempt < MAX_RETRIES; attempt++) { + if (attempt > 0) { + try { + log.info("Retrying request, attempt {} of {}", attempt + 1, MAX_RETRIES); + TimeUnit.MILLISECONDS.sleep(RETRY_DELAY_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return "Request interrupted: " + e.getMessage(); + } + } + + try { + return processRequest(request); + } catch (IOException e) { + lastException = e; + log.warn("Request failed (attempt {}/{}): {}", attempt + 1, MAX_RETRIES, e.getMessage()); + } + } + + log.error("All retry attempts failed", lastException); + return "Failed after " + MAX_RETRIES + " attempts: " + lastException.getMessage(); + } + + /** + * Processes a single request to the Judge0 API. + * + * @param request The request to process + * @return The result of the request or an error message + * @throws IOException if an I/O error occurs + */ + private String processRequest(Request request) throws IOException { + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + return handleErrorResponse(response); + } + + if (response.body() == null) { + log.warn(ERROR_NULL_RESPONSE); + return ERROR_NULL_RESPONSE; + } + + String responseBody = response.body().string(); + return processResponseBody(responseBody); + } + } + + /** + * Handles error responses from the Judge0 API. + * + * @param response The error response + * @return An appropriate error message + */ + private String handleErrorResponse(Response response) { + String errorMessage = + switch (response.code()) { + case 429 -> ERROR_RATE_LIMIT; + case 403 -> ERROR_FORBIDDEN; + case 404 -> ERROR_NOT_FOUND; + case 500 -> ERROR_SERVER; + case 503 -> ERROR_UNAVAILABLE; + default -> "Unexpected error code " + response.code() + ": " + response.message(); + }; + + log.warn(errorMessage); + return errorMessage; + } + + /** + * Processes the response body from a successful request. + * + * @param responseBody The response body as a string + * @return The processed result or an error message + */ + private String processResponseBody(String responseBody) { + SubmissionResult result = Json.fromJson(responseBody, SubmissionResult.class); + + if (result.status.id != STATUS_ACCEPTED) { + return formatErrorResult(result); + } + + String base64EncodedStdout = result.stdout; + if (base64EncodedStdout == null) { + return ERROR_NO_STDOUT; + } + + try { + return new String(Base64.getMimeDecoder().decode(base64EncodedStdout.trim()), StandardCharsets.UTF_8) + .trim(); + } catch (IllegalArgumentException e) { + log.warn("Failed to decode base64 output", e); + return "Error decoding result: " + e.getMessage(); + } + } + + /** + * Formats an error result from Judge0. + * + * @param result The submission result containing error information + * @return A formatted error message + */ + private String formatErrorResult(SubmissionResult result) { + StringBuilder error = new StringBuilder(result.status.description); + + if (!isNullOrBlank(result.compile_output)) { + error.append("\n"); + try { + error.append(new String(Base64.getMimeDecoder().decode(result.compile_output), StandardCharsets.UTF_8)); + } catch (IllegalArgumentException e) { + error.append("(Error decoding compile output: ") + .append(e.getMessage()) + .append(")"); + } + } + + return error.toString(); + } + + /** + * Represents a code submission to the Judge0 API. + */ + private static class Submission { + private final int language_id; + private final String source_code; + + Submission(int languageId, String sourceCode) { + this.language_id = languageId; + this.source_code = sourceCode; + } + + public int getLanguage_id() { + return language_id; + } + + public String getSource_code() { + return source_code; + } + } + + /** + * Represents the result of a code submission. + */ + private static class SubmissionResult { + private String stdout; + private Status status; + private String compile_output; + + public String getStdout() { + return stdout; + } + + public void setStdout(String stdout) { + this.stdout = stdout; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public String getCompile_output() { + return compile_output; + } + + public void setCompile_output(String compile_output) { + this.compile_output = compile_output; + } + } + + /** + * Represents the status of a code submission. + */ + private static class Status { + private int id; + private String description; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + } +} diff --git a/code-execution-engines/langchain4j-code-execution-engine-judge0/src/main/java/dev/langchain4j/code/judge0/Judge0JavaScriptExecutionTool.java b/code-execution-engines/langchain4j-code-execution-engine-judge0/src/main/java/dev/langchain4j/code/judge0/Judge0JavaScriptExecutionTool.java new file mode 100644 index 0000000..db7dd9d --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-judge0/src/main/java/dev/langchain4j/code/judge0/Judge0JavaScriptExecutionTool.java @@ -0,0 +1,180 @@ +package dev.langchain4j.code.judge0; + +import static dev.langchain4j.code.judge0.JavaScriptCodeFixer.fixIfNoLogToConsole; +import static dev.langchain4j.internal.Utils.isNullOrBlank; + +import dev.langchain4j.agent.tool.P; +import dev.langchain4j.agent.tool.Tool; +import java.time.Duration; +import java.util.Objects; + +/** + * A tool that executes JS code using the Judge0 service, hosted by Rapid API. + */ +public class Judge0JavaScriptExecutionTool { + + /** The language ID for JavaScript in Judge0's API */ + public static final int JAVASCRIPT = 102; + + /** Default timeout for Judge0 code execution (10 seconds) */ + private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(10); + + private final Judge0JavaScriptEngine engine; + private final boolean fixCodeIfNeeded; + + /** + * Constructs a new instance with the provided Rapid API key. + * The code fixing feature is enabled by default. + * Default timeout is 10 seconds. + * + * @param apiKey The Rapid API key. You can subscribe to the free plan (Basic) here: https://rapidapi.com/judge0-official/api/judge0-ce/pricing + * @throws IllegalArgumentException if the API key is null, empty, or blank + */ + public Judge0JavaScriptExecutionTool(String apiKey) { + this(apiKey, true, DEFAULT_TIMEOUT); + } + + /** + * Constructs a new instance with the provided Rapid API key and a timeout. + * The code fixing feature is enabled by default. + * + * @param apiKey The Rapid API key + * @param timeout Timeout for calling Judge0 + * @throws IllegalArgumentException if the API key is null, empty, or blank + * @throws NullPointerException if timeout is null + */ + public Judge0JavaScriptExecutionTool(String apiKey, Duration timeout) { + this(apiKey, true, timeout); + } + + /** + * Constructs a new instance with the provided Rapid API key and code fixing flag. + * Uses default timeout of 10 seconds. + * + * @param apiKey The Rapid API key + * @param fixCodeIfNeeded Whether to automatically fix code if needed + * @throws IllegalArgumentException if the API key is null, empty, or blank + */ + public Judge0JavaScriptExecutionTool(String apiKey, boolean fixCodeIfNeeded) { + this(apiKey, fixCodeIfNeeded, DEFAULT_TIMEOUT); + } + + /** + * Constructs a new instance with the provided Rapid API key, a flag to control whether to fix the code, and a timeout. + * + * @param apiKey Rapid API key. You can subscribe to the free plan (Basic) here: https://rapidapi.com/judge0-official/api/judge0-ce/pricing + * @param fixCodeIfNeeded Judge0 can return result of an execution if it was printed to the console. + * If provided JS code does not print result to the console, attempt will be made to fix it. + * @param timeout Timeout for calling Judge0. + * @throws IllegalArgumentException if the API key is null, empty, or blank + * @throws NullPointerException if timeout is null + */ + public Judge0JavaScriptExecutionTool(String apiKey, boolean fixCodeIfNeeded, Duration timeout) { + if (isNullOrBlank(apiKey)) { + throw new IllegalArgumentException("Please provide a valid Rapid API key"); + } + Objects.requireNonNull(timeout, "Timeout must not be null"); + + this.engine = new Judge0JavaScriptEngine(apiKey, JAVASCRIPT, timeout); + this.fixCodeIfNeeded = fixCodeIfNeeded; + } + + /** + * Executes the provided JavaScript code using the Judge0 service. + * + * If code fixing is enabled (default), the method will attempt to add console.log statements + * if the code doesn't already output results to the console. + * + * @param javaScriptCode JavaScript code to execute + * @return The result of the JavaScript code execution + * @throws IllegalArgumentException if javaScriptCode is null, empty or blank + * @throws RuntimeException if code execution fails + */ + @Tool("MUST be used for accurate calculations: math, sorting, filtering, aggregating, string processing, etc") + public String executeJavaScriptCode( + @P("JavaScript code to execute, result MUST be printed to console") String javaScriptCode) { + if (isNullOrBlank(javaScriptCode)) { + throw new IllegalArgumentException("JavaScript code must not be null or empty"); + } + + if (fixCodeIfNeeded) { + javaScriptCode = fixIfNoLogToConsole(javaScriptCode); + } + + try { + return engine.execute(javaScriptCode); + } catch (Exception e) { + throw new RuntimeException("Failed to execute JavaScript code: " + e.getMessage(), e); + } + } + + /** + * Returns whether code fixing is enabled for this tool. + * + * @return true if code fixing is enabled, false otherwise + */ + public boolean isCodeFixingEnabled() { + return fixCodeIfNeeded; + } + + /** + * Creates a builder for configuring a Judge0JavaScriptExecutionTool. + * + * @return a new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for creating a configured Judge0JavaScriptExecutionTool. + */ + public static class Builder { + private String apiKey; + private boolean fixCodeIfNeeded = true; + private Duration timeout = DEFAULT_TIMEOUT; + + /** + * Sets the Rapid API key for Judge0. + * + * @param apiKey the API key + * @return this builder + */ + public Builder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + /** + * Sets whether to fix code if needed. + * + * @param fixCodeIfNeeded whether to fix code + * @return this builder + */ + public Builder fixCodeIfNeeded(boolean fixCodeIfNeeded) { + this.fixCodeIfNeeded = fixCodeIfNeeded; + return this; + } + + /** + * Sets the timeout for code execution. + * + * @param timeout the timeout duration + * @return this builder + */ + public Builder timeout(Duration timeout) { + this.timeout = timeout; + return this; + } + + /** + * Builds a new Judge0JavaScriptExecutionTool with the configured properties. + * + * @return a new Judge0JavaScriptExecutionTool + * @throws IllegalArgumentException if apiKey is null, empty or blank + */ + public Judge0JavaScriptExecutionTool build() { + return new Judge0JavaScriptExecutionTool(apiKey, fixCodeIfNeeded, timeout); + } + } +} diff --git a/code-execution-engines/langchain4j-code-execution-engine-judge0/src/test/java/dev/langchain4j/code/judge0/JavaScriptCodeFixerTest.java b/code-execution-engines/langchain4j-code-execution-engine-judge0/src/test/java/dev/langchain4j/code/judge0/JavaScriptCodeFixerTest.java new file mode 100644 index 0000000..997fd78 --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-judge0/src/test/java/dev/langchain4j/code/judge0/JavaScriptCodeFixerTest.java @@ -0,0 +1,79 @@ +package dev.langchain4j.code.judge0; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class JavaScriptCodeFixerTest { + + @Test + void should_fix_multi_line_code_when_result_is_not_logged_to_console() { + String code = + "const startDate = new Date('Feb 21, 1988 17:00:00');\nconst endDate = new Date('Apr 12, 2014 04:00:00');\nconst diff = endDate - startDate;\nconst result = diff / (1000*60*60);\n;const startDate = new Date('Feb 21, 1988 17:00:00');\nconst endDate = new Date('Apr 12, 2014 04:00:00');\nconst diff = endDate - startDate;\nconst result = diff / (1000*60*60);\n"; + + String result = JavaScriptCodeFixer.fixIfNoLogToConsole(code + "result"); + + assertThat(result).isEqualTo(code + "console.log(result);"); + } + + @Test + void should_not_fix_multi_line_code_when_result_is_logged_to_console() { + String code = + "const startDate = new Date('Feb 21, 1988 17:00:00');\nconst endDate = new Date('Apr 12, 2014 04:00:00');\nconst diff = endDate - startDate;\nconst result = diff / (1000*60*60);\nconsole.log(result);"; + + String result = JavaScriptCodeFixer.fixIfNoLogToConsole(code); + + assertThat(result).isEqualTo(code); + } + + @Test + void should_fix_one_line_code_when_result_is_not_logged_to_console() { + String code = + "const start = new Date('1988-02-21T17:00:00'); const end = new Date('2014-04-12T04:00:00'); const hours = (end - start) / (1000 * 60 * 60); "; + + String result = JavaScriptCodeFixer.fixIfNoLogToConsole(code + "hours"); + + assertThat(result).isEqualTo(code + "console.log(hours);"); + } + + @Test + void should_not_fix_one_line_code_when_result_is_logged_to_console() { + String code = + "const start = new Date('1988-02-21T17:00:00'); const end = new Date('2014-04-12T04:00:00'); const hours = (end - start) / (1000 * 60 * 60); console.log(hours);"; + + String result = JavaScriptCodeFixer.fixIfNoLogToConsole(code); + + assertThat(result).isEqualTo(code); + } + + @Test + void should_fix_one_statement_when_result_is_not_logged_to_console() { + String code = "Math.sqrt(49506838032859)"; + + String result = JavaScriptCodeFixer.fixIfNoLogToConsole(code); + + assertThat(result).isEqualTo("console.log(Math.sqrt(49506838032859));"); + } + + @Test + void should_not_fix_one_statement_when_result_is_logged_to_console() { + String code = "console.log(Math.sqrt(49506838032859));"; + + String result = JavaScriptCodeFixer.fixIfNoLogToConsole(code); + + assertThat(result).isEqualTo(code); + } + + @Test + void should_wrap_full_expression_for_single_line_code_with_spaces() { + assertThat(JavaScriptCodeFixer.fixIfNoLogToConsole("2 + 3")).isEqualTo("console.log(2 + 3);"); + assertThat(JavaScriptCodeFixer.fixIfNoLogToConsole("Math.min(3, 7)")).isEqualTo("console.log(Math.min(3, 7));"); + } + + @Test + void should_wrap_full_last_expression_when_single_line_has_preceding_statements() { + String code = "const a = 2; const b = 3; a + b"; + assertThat(JavaScriptCodeFixer.fixIfNoLogToConsole(code)) + .isEqualTo("const a = 2; const b = 3; console.log(a + b);"); + } +} diff --git a/code-execution-engines/langchain4j-code-execution-engine-judge0/src/test/java/dev/langchain4j/code/judge0/Judge0JavaScriptEngineIT.java b/code-execution-engines/langchain4j-code-execution-engine-judge0/src/test/java/dev/langchain4j/code/judge0/Judge0JavaScriptEngineIT.java new file mode 100644 index 0000000..05c3c1d --- /dev/null +++ b/code-execution-engines/langchain4j-code-execution-engine-judge0/src/test/java/dev/langchain4j/code/judge0/Judge0JavaScriptEngineIT.java @@ -0,0 +1,55 @@ +package dev.langchain4j.code.judge0; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.langchain4j.code.CodeExecutionEngine; +import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; + +/** + * Integration tests for the Judge0JavaScriptEngine. + * + * Note: These tests require a valid RAPID_API_KEY environment variable to be set. + */ +@EnabledIfEnvironmentVariable(named = "RAPID_API_KEY", matches = ".+") +class Judge0JavaScriptEngineIT { + + private static final Duration TEST_TIMEOUT = Duration.ofSeconds(60); + private CodeExecutionEngine codeExecutionEngine; + + @BeforeEach + void setUp() { + String apiKey = System.getenv("RAPID_API_KEY"); + codeExecutionEngine = + new Judge0JavaScriptEngine(apiKey, Judge0JavaScriptExecutionTool.JAVASCRIPT, TEST_TIMEOUT); + } + + @Test + void should_execute_simple_code() { + // when + String result = codeExecutionEngine.execute("console.log('hello world');"); + + // then + assertThat(result).isEqualTo("hello world"); + } + + @Test + void should_execute_mathematical_calculations() { + // when + String result = + codeExecutionEngine.execute("function calculateCompoundInterest(principal, rate, time, compounds) {\n" + + " const amount = principal * Math.pow(1 + rate/compounds, compounds * time);\n" + + " const interest = amount - principal;\n" + + " return { principal, amount, interest };\n" + + "}\n\n" + + "const result = calculateCompoundInterest(1000, 0.05, 5, 12);\n" + + "console.log(JSON.stringify(result));"); + + assertThat(result) + .contains("\"principal\":1000") + .contains("\"amount\":1283.3586785035118") + .contains("\"interest\":283.3586785035118"); + } +} diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 0000000..60edf25 --- /dev/null +++ b/detekt.yml @@ -0,0 +1,8 @@ +style: + active: true + MethodName: + active: true + ignoreOverriddenFunctions: true + excludeAnnotatedMethodsOrClasses: + - "org.junit.jupiter.api.Test" + ignoreTestFiles: true diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..b2d6de3 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/docs/.nvmrc b/docs/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/docs/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..9ea6902 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,7 @@ +build-docs: + npm ci; \ + npm run build + +run-docs: + npm ci; \ + npm run start \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..e4655ef --- /dev/null +++ b/docs/README.md @@ -0,0 +1,42 @@ +## LangChain4j Documentation + +### Table of Contents + +- [Prerequisites](#prerequisites) +- [Installation](#installation) + +This is built using [Docusaurus](https://docusaurus.io/), a modern static docs/website generator. + +### Prerequisites + +- [Node.JS](https://nodejs.org/en/download) (20+) + +If Node.js is managed by Node Version Manager (nvm), run: +```shell +nvm use +``` + +### Installation + +This command installs all the Node dependencies. + +```bash +npm ci +``` + +### Build + +This command builds the deployable static artifacts (i.e., html, js, css, etc) under the `build` directory. This static content can then be deployed to `GitHub pages` or `Vercel`, etc. + +``` +npm run build +``` + +### Local Development + +This command starts a local development server and opens up a browser window. Most changes are reflected live without +having to restart the server. + +``` +npm run start +``` diff --git a/docs/babel.config.js b/docs/babel.config.js new file mode 100644 index 0000000..e00595d --- /dev/null +++ b/docs/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [require.resolve('@docusaurus/core/lib/babel/preset')], +}; diff --git a/docs/build.sh b/docs/build.sh new file mode 100755 index 0000000..a602c00 --- /dev/null +++ b/docs/build.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +npm ci + +npm run build \ No newline at end of file diff --git a/docs/docs/get-started.md b/docs/docs/get-started.md new file mode 100644 index 0000000..234c64c --- /dev/null +++ b/docs/docs/get-started.md @@ -0,0 +1,145 @@ +--- +sidebar_position: 5 +stableVersion: 1.17.2 +betaVersion: 1.17.2-beta27 +--- + +# Get Started + +:::note +If you are using Quarkus, see [Quarkus Integration](/tutorials/quarkus-integration/). + +If you are using Spring Boot, see [Spring Boot Integration](/tutorials/spring-boot-integration). + +If you are using Helidon, see [Helidon Integration](/tutorials/helidon-integration) +::: + +LangChain4j offers integrations with many [LLM providers](/integrations/language-models/), +[embedding/vector stores](/integrations/embedding-stores), etc. +Each integration has its own maven dependency. + +The minimum supported JDK version is 17. + +As an example, let's import the OpenAI dependency: + +- For Maven in `pom.xml`: +```xml + + dev.langchain4j + langchain4j-open-ai + 1.17.2 + +``` + +If you wish to use a high-level [AI Services](/tutorials/ai-services) API, you will also need to add +the following dependency: + +```xml + + dev.langchain4j + langchain4j + 1.17.2 + +``` + +- For Gradle in `build.gradle`: +```groovy +implementation 'dev.langchain4j:langchain4j-open-ai:1.17.2' +implementation 'dev.langchain4j:langchain4j:1.17.2' +``` + +

+Bill of Materials (BOM) + +```xml + + + + dev.langchain4j + langchain4j-bom + 1.17.2 + pom + import + + + +``` + +:::note +Please note that `langchain4j-bom` always contains the latest versions of all LangChain4j modules. +::: + +:::note +Please note that while the `langchain4j-bom` version is `1.17.2`, +many of the modules still have version `1.17.2-beta27`, +so there might be some breaking changes for these modules in the future. +::: +
+ +
+SNAPSHOT dependencies (newest features) + +If you'd like to test the newest features before their official release, +you can use the most recent `SNAPSHOT` dependency: +```xml + + + Central Portal Snapshots + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + + + + dev.langchain4j + langchain4j + 1.17.2-SNAPSHOT + + +``` +
+ +Then, import your OpenAI API key. +It's recommended to store your API keys in environment variables to reduce the risk of exposing them publicly. +```java +String apiKey = System.getenv("OPENAI_API_KEY"); +``` + +
+What if I don't have an API key? + +If you don't have your own OpenAI API key, don't worry. +You can temporarily use `demo` key, which we provide for free for demonstration purposes. +Be aware that when using the `demo` key, all requests to the OpenAI API need to go through our proxy, +which injects the real key before forwarding your request to the OpenAI API. +We do not collect or use your data in any way. +The `demo` key has a quota, is restricted to the `gpt-4o-mini` model, and should only be used for demonstration purposes. + +```java +OpenAiChatModel model = OpenAiChatModel.builder() + .baseUrl("http://langchain4j.dev/demo/openai/v1") + .apiKey("demo") + .modelName("gpt-4o-mini") + .build(); +``` +
+ +Once you've set up the key, let's create an instance of an `OpenAiChatModel`: +```java +OpenAiChatModel model = OpenAiChatModel.builder() + .apiKey(apiKey) + .modelName("gpt-4o-mini") + .build(); +``` +Now, it is time to chat! +```java +String answer = model.chat("Say 'Hello World'"); +System.out.println(answer); // Hello World +``` diff --git a/docs/docs/integrations/_category_.json b/docs/docs/integrations/_category_.json new file mode 100644 index 0000000..1929124 --- /dev/null +++ b/docs/docs/integrations/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Integrations", + "position": 7, + "link": { + "type": "generated-index", + "description": "Integrations" + } +} diff --git a/docs/docs/integrations/browser-execution-engines/_category_.json b/docs/docs/integrations/browser-execution-engines/_category_.json new file mode 100644 index 0000000..4980177 --- /dev/null +++ b/docs/docs/integrations/browser-execution-engines/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Browser Execution Engines", + "position": 20, + "link": { + "type": "generated-index", + "description": "Browser Execution Engines" + } +} diff --git a/docs/docs/integrations/browser-execution-engines/playwright.md b/docs/docs/integrations/browser-execution-engines/playwright.md new file mode 100644 index 0000000..37596a9 --- /dev/null +++ b/docs/docs/integrations/browser-execution-engines/playwright.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 1 +--- + +# Playwright +`BrowserExecutionEngine` represents a browser execution engine that can be used to perform actions on the browser in response to a user action. +`PlaywrightBrowserExecutionEngine` is an implementation of a `BrowserExecutionEngine` that uses Playwright Java API for performing browser actions. +`BrowserUseTool` executes browser actions using `BrowserExecutionEngine`, that can be useful for **Browser-Use** Agents. You can control your browser using natural language, something like: +* `open page 'https://docs.langchain4j.dev/', and summary the page text` + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-community-browser-execution-engine-playwright + ${latest version here} + + + dev.langchain4j + langchain4j-community-tool-browser-use + ${latest version here} + +``` + + +## APIs + +- `BrowserExecutionEngine` +- `PlaywrightBrowserExecutionEngine` +- `BrowserUseTool` + + +## Examples + +```java + Playwright playwright = Playwright.create(); + BrowserType.LaunchOptions options = new BrowserType.LaunchOptions() + .setHeadless(false) + .setChannel("chrome") + .setChromiumSandbox(true) + .setSlowMo(500); + Browser browser = playwright.chromium().launch(options); + + Assistant assistant = AiServices.builder(Assistant.class) + .chatModel(model) + .tools(BrowserUseTool.from(PlaywrightBrowserExecutionEngine.builder().browser(browser).build())) + .chatMemory(MessageWindowChatMemory.withMaxMessages(10)) + .build(); + + String question = "open page 'https://docs.langchain4j.dev/', and summary the page text"; + System.out.println(assistant.chat(question)); +``` + +- [PlaywrightBrowserExecutionEngineIT](https://github.com/langchain4j/langchain4j-community/blob/main/browser-execution-engines/langchain4j-community-browser-execution-engine-playwright/src/test/java/dev/langchain4j/community/browser/playwright/PlaywrightBrowserExecutionEngineIT.java) +- [BrowserUseToolIT](https://github.com/langchain4j/langchain4j-community/blob/main/tools/langchain4j-community-tool-browser-use/src/test/java/dev/langchain4j/community/tool/browseruse/BrowserUseToolIT.java) diff --git a/docs/docs/integrations/chat-memory-stores/_category_.json b/docs/docs/integrations/chat-memory-stores/_category_.json new file mode 100644 index 0000000..acada9c --- /dev/null +++ b/docs/docs/integrations/chat-memory-stores/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Chat Memory Stores", + "position": 4, + "link": { + "type": "generated-index", + "description": "Chat Memory Stores" + } +} diff --git a/docs/docs/integrations/chat-memory-stores/index.md b/docs/docs/integrations/chat-memory-stores/index.md new file mode 100644 index 0000000..4785704 --- /dev/null +++ b/docs/docs/integrations/chat-memory-stores/index.md @@ -0,0 +1,18 @@ +--- +title: Comparison table of all supported Chat Memory Stores +hide_title: false +sidebar_position: 0 +--- + +| Chat Memory Stores | Persistence | Default | +|---------------------------------------------------------------------------|-------------|---------| +| [In-memory](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-core/src/test/java/dev/langchain4j/store/memory/chat/InMemoryChatMemoryStoreTest.java) | | | +| [Azure CosmosDB NoSQL](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-azure-cosmos-nosql/src/test/java/dev/langchain4j/store/memory/azure/cosmos/nosql/AzureCosmosDBNoSqlMemoryStoreIT.java) | ✅ | | +| [Cassandra](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-cassandra/src/test/java/dev/langchain4j/store/memory/chat/cassandra/CassandraChatMemoryStoreDockerIT.java) | ✅ | | +| [Coherence](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-coherence/src/test/java/dev/langchain4j/store/memory/chat/coherence/CoherenceChatMemoryStoreIT.java) | ✅ | | +| [Hazelcast (IMap)](https://github.com/langchain4j/langchain4j-community/blob/main/chat-memory-stores/langchain4j-community-hazelcast/src/test/java/dev/langchain4j/community/store/memory/chat/hazelcast/HazelcastChatMemoryStoreIT.java) | ✅ | | +| [Hazelcast (CPMap)](https://github.com/langchain4j/langchain4j-community/blob/main/embedding-stores/langchain4j-community-hazelcast-enterprise/src/test/java/dev/langchain4j/community/store/memory/chat/hazelcast/HazelcastCPMapChatMemoryStoreIT.java) | ✅ | | +| [Neo4j](https://github.com/langchain4j/langchain4j-community/blob/main/embedding-stores/langchain4j-community-neo4j/src/test/java/dev/langchain4j/community/store/memory/chat/neo4j/Neo4jChatMemoryStoreIT.java) | ✅ | | +| [Oracle](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-oracle/src/test/java/dev/langchain4j/store/memory/chat/oracle/OracleChatMemoryStoreIT.java) | ✅ | | +| [Redis](https://github.com/langchain4j/langchain4j-community/blob/main/embedding-stores/langchain4j-community-redis/src/test/java/dev/langchain4j/community/store/memory/chat/redis/RedisChatMemoryStoreIT.java) | ✅ | | +| [Tablestore](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-tablestore/src/test/java/dev/langchain4j/store/memory/chat/tablestore/TablestoreChatMemoryStoreIT.java) | ✅ | | diff --git a/docs/docs/integrations/code-execution-engines/_category_.json b/docs/docs/integrations/code-execution-engines/_category_.json new file mode 100644 index 0000000..b8b8cd4 --- /dev/null +++ b/docs/docs/integrations/code-execution-engines/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Code Execution Engines", + "position": 15, + "link": { + "type": "generated-index", + "description": "Code Execution Engines" + } +} diff --git a/docs/docs/integrations/code-execution-engines/azure-acads.md b/docs/docs/integrations/code-execution-engines/azure-acads.md new file mode 100644 index 0000000..08bc8b3 --- /dev/null +++ b/docs/docs/integrations/code-execution-engines/azure-acads.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 1 +--- + +# Azure ACA Dynamic Sessions + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-code-execution-engine-azure-acads + 1.17.2-beta27 + +``` + +## APIs + +- `SessionsREPLTool` + +## Examples + +- [SessionsREPLToolTest](https://github.com/langchain4j/langchain4j/blob/main/code-execution-engines/langchain4j-code-execution-engine-azure-acads/src/test/java/dev/langchain4j/code/azure/acads/SessionsREPLToolTest.java) diff --git a/docs/docs/integrations/code-execution-engines/graalvm-polyglot.md b/docs/docs/integrations/code-execution-engines/graalvm-polyglot.md new file mode 100644 index 0000000..bfc7f38 --- /dev/null +++ b/docs/docs/integrations/code-execution-engines/graalvm-polyglot.md @@ -0,0 +1,38 @@ +--- +sidebar_position: 2 +--- + +# GraalVM Polyglot/Truffle + +:::danger +⚠️ Security Warning: High-Risk Code Execution + +This module enables execution of arbitrary Python/JavaScript code via GraalVM and is inherently dangerous. + +❗ Do NOT use in production environments! +::: + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-code-execution-engine-graalvm-polyglot + 1.17.2-beta27 + +``` + +## APIs + +- `GraalVmJavaScriptExecutionEngine` +- `GraalVmJavaScriptExecutionTool` +- `GraalVmPythonExecutionEngine` +- `GraalVmPythonExecutionTool` + + +## Examples + +- [GraalVmJavaScriptExecutionEngineTest](https://github.com/langchain4j/langchain4j/blob/main/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/test/java/dev/langchain4j/code/graalvm/GraalVmJavaScriptExecutionEngineTest.java) +- [GraalVmJavaScriptExecutionToolIT](https://github.com/langchain4j/langchain4j/blob/main/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/test/java/dev/langchain4j/agent/tool/graalvm/GraalVmJavaScriptExecutionToolIT.java) +- [GraalVmPythonExecutionEngineTest](https://github.com/langchain4j/langchain4j/blob/main/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/test/java/dev/langchain4j/code/graalvm/GraalVmPythonExecutionEngineTest.java) +- [GraalVmPythonExecutionToolIT](https://github.com/langchain4j/langchain4j/blob/main/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/test/java/dev/langchain4j/agent/tool/graalvm/GraalVmPythonExecutionToolIT.java) diff --git a/docs/docs/integrations/code-execution-engines/judge0.md b/docs/docs/integrations/code-execution-engines/judge0.md new file mode 100644 index 0000000..bfc56e0 --- /dev/null +++ b/docs/docs/integrations/code-execution-engines/judge0.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 3 +--- + +# Judge0 + +https://github.com/judge0/judge0 + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-code-execution-engine-judge0 + 1.17.2-beta27 + +``` + +## APIs + +- `Judge0JavaScriptEngine` +- `Judge0JavaScriptExecutionTool` + + +## Examples + +- [ServiceWithDynamicToolsExample](https://github.com/langchain4j/langchain4j-examples/blob/main/other-examples/src/main/java/ServiceWithDynamicToolsExample.java) diff --git a/docs/docs/integrations/code-execution-engines/local.md b/docs/docs/integrations/code-execution-engines/local.md new file mode 100644 index 0000000..bc61a58 --- /dev/null +++ b/docs/docs/integrations/code-execution-engines/local.md @@ -0,0 +1,62 @@ +--- +sidebar_position: 4 +--- + +# Local + +`CommandLineExecutionEngine` uses the local computer env to execute provided command line code. +There are `CommandLineTool` and `LocalScriptExecutionTool` two implemented tools, +that can be useful for **Desktop Automation** or **Computer-Use** Agents +(such as File Management, Application Control). You can control your computer using natural language, something like: + - `set my mac output volume 50` + - `list all running applications in my mac` + - `tell a story and then read it out loud` + - `tell a story about moon and save it into a text file` + - ... + +:::danger +⚠️ Security Warning: High-Risk Code Execution + +Attention! It might be *dangerous* to execute the code in a production online serving environment. +It needs to be executed through security sandbox environment if used in online serving. + +❗ Do NOT use in production environments! +::: + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-community-code-execution-engine-local + ${latest version here} + +``` + +## APIs + +- `CommandLineExecutionEngine` +- `CommandLineTool` +- `LocalScriptExecutionTool` + + +## Examples + +```java + LocalScriptExecutionTool tool = new LocalScriptExecutionTool(); + + Assistant assistant = AiServices.builder(Assistant.class) + .chatModel(model) + .tools(tool) + .chatMemory(MessageWindowChatMemory.withMaxMessages(10)) + .build(); + + String answer = assistant.chat("list all running applications in my mac"); + System.out.println(answer); +``` + +- [CommandLineExecutionEngineTest](https://github.com/langchain4j/langchain4j-community/blob/main/code-execution-engines/langchain4j-community-code-execution-engine-local/src/test/java/dev/langchain4j/community/code/local/CommandLineExecutionEngineTest.java) +- [CommandLineToolIT](https://github.com/langchain4j/langchain4j-community/blob/main/code-execution-engines/langchain4j-community-code-execution-engine-local/src/test/java/dev/langchain4j/community/code/local/CommandLineToolIT.java) +- [CommandLineToolTest](https://github.com/langchain4j/langchain4j-community/blob/main/code-execution-engines/langchain4j-community-code-execution-engine-local/src/test/java/dev/langchain4j/community/code/local/CommandLineToolTest.java) +- [LocalScriptExecutionToolIT](https://github.com/langchain4j/langchain4j-community/blob/main/code-execution-engines/langchain4j-community-code-execution-engine-local/src/test/java/dev/langchain4j/community/code/local/LocalScriptExecutionToolIT.java) +- [LocalScriptExecutionToolTest](https://github.com/langchain4j/langchain4j-community/blob/main/code-execution-engines/langchain4j-community-code-execution-engine-local/src/test/java/dev/langchain4j/community/code/local/LocalScriptExecutionToolTest.java) diff --git a/docs/docs/integrations/document-loaders/_category_.json b/docs/docs/integrations/document-loaders/_category_.json new file mode 100644 index 0000000..b5617b8 --- /dev/null +++ b/docs/docs/integrations/document-loaders/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Document Loaders", + "position": 7, + "link": { + "type": "generated-index", + "description": "Document Loaders" + } +} diff --git a/docs/docs/integrations/document-loaders/amazon-s3.md b/docs/docs/integrations/document-loaders/amazon-s3.md new file mode 100644 index 0000000..6b2b38a --- /dev/null +++ b/docs/docs/integrations/document-loaders/amazon-s3.md @@ -0,0 +1,26 @@ +--- +sidebar_position: 1 +--- + +# Amazon S3 + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-document-loader-amazon-s3 + 1.17.2-beta27 + +``` + + +## APIs + +- `AmazonS3DocumentLoader` + + +## Examples + +- [AmazonS3DocumentLoaderIT](https://github.com/langchain4j/langchain4j/blob/main/document-loaders/langchain4j-document-loader-amazon-s3/src/test/java/dev/langchain4j/data/document/loader/amazon/s3/AmazonS3DocumentLoaderIT.java) diff --git a/docs/docs/integrations/document-loaders/azure-blob-storage.md b/docs/docs/integrations/document-loaders/azure-blob-storage.md new file mode 100644 index 0000000..ef111c3 --- /dev/null +++ b/docs/docs/integrations/document-loaders/azure-blob-storage.md @@ -0,0 +1,26 @@ +--- +sidebar_position: 2 +--- + +# Azure Blob Storage + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-document-loader-azure-storage-blob + 1.17.2-beta27 + +``` + + +## APIs + +- `AzureBlobStorageDocumentLoader` + + +## Examples + +- [AzureBlobStorageDocumentLoaderIT](https://github.com/langchain4j/langchain4j/blob/main/document-loaders/langchain4j-document-loader-azure-storage-blob/src/test/java/dev/langchain4j/data/document/loader/azure/storage/blob/AzureBlobStorageDocumentLoaderIT.java) diff --git a/docs/docs/integrations/document-loaders/classpath.md b/docs/docs/integrations/document-loaders/classpath.md new file mode 100644 index 0000000..ca0566b --- /dev/null +++ b/docs/docs/integrations/document-loaders/classpath.md @@ -0,0 +1,25 @@ +--- +sidebar_position: 3 +--- + +# Classpath + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j + 1.17.2 + +``` + + +## APIs + +- `ClassPathSource` / `ClassPathDocumentLoader` + +## Examples + +- [ClassPathSourceTests](https://github.com/langchain4j/langchain4j/blob/main/langchain4j/src/test/java/dev/langchain4j/data/document/source/ClassPathSourceTests.java) +- [ClassPathDocumentLoaderTest](https://github.com/langchain4j/langchain4j/blob/main/langchain4j/src/test/java/dev/langchain4j/data/document/loader/ClassPathDocumentLoaderTest.java) diff --git a/docs/docs/integrations/document-loaders/file-system.md b/docs/docs/integrations/document-loaders/file-system.md new file mode 100644 index 0000000..f317690 --- /dev/null +++ b/docs/docs/integrations/document-loaders/file-system.md @@ -0,0 +1,25 @@ +--- +sidebar_position: 3 +--- + +# File System + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j + 1.17.2 + +``` + + +## APIs + +- `FileSystemSource` / `FileSystemDocumentLoader` + + +## Examples + +- [FileSystemDocumentLoaderTest](https://github.com/langchain4j/langchain4j/blob/main/langchain4j/src/test/java/dev/langchain4j/data/document/loader/FileSystemDocumentLoaderTest.java) diff --git a/docs/docs/integrations/document-loaders/github.md b/docs/docs/integrations/document-loaders/github.md new file mode 100644 index 0000000..0e8aced --- /dev/null +++ b/docs/docs/integrations/document-loaders/github.md @@ -0,0 +1,26 @@ +--- +sidebar_position: 4 +--- + +# GitHub + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-document-loader-github + 1.17.2-beta27 + +``` + + +## APIs + +- `GitHubDocumentLoader` + + +## Examples + +- [GitHubDocumentLoaderIT](https://github.com/langchain4j/langchain4j/blob/main/document-loaders/langchain4j-document-loader-github/src/test/java/dev/langchain4j/data/document/loader/github/GitHubDocumentLoaderIT.java) diff --git a/docs/docs/integrations/document-loaders/google-cloud-storage.md b/docs/docs/integrations/document-loaders/google-cloud-storage.md new file mode 100644 index 0000000..767dbf2 --- /dev/null +++ b/docs/docs/integrations/document-loaders/google-cloud-storage.md @@ -0,0 +1,83 @@ +--- +sidebar_position: 2 +--- + +# Google Cloud Storage + +A Google Cloud Storage (GCS) document loader that allows you to load documents from storage buckets. + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-document-loader-google-cloud-storage + 1.17.2-beta27 + +``` + +## APIs + +- `GoogleCloudStorageDocumentLoader` + +## Authentication + +The authentication should be handled transparently for you: +* If your application is running on Google Cloud Platform (Cloud Run, App Engine, Compute Engine, etc) +* When running locally on your machine, if you are already authenticated via Google's `gcloud` SDK + +You should just create a loader specifying just your project ID: + +```java +GoogleCloudStorageDocumentLoader gcsLoader = GoogleCloudStorageDocumentLoader.builder() + .project(System.getenv("GCP_PROJECT_ID")) + .build(); +``` + +Otherwise, it's possible to specify `Credentials`, if you have downloaded a service account key, and exported an environment variable pointing to it: + +```java +GoogleCloudStorageDocumentLoader gcsLoader = GoogleCloudStorageDocumentLoader.builder() + .project(System.getenv("GCP_PROJECT_ID")) + .credentials(GoogleCredentials.fromStream(new FileInputStream(System.getenv("GOOGLE_APPLICATION_CREDENTIALS")))) + .build(); +``` + +Learn more about [credentials](https://cloud.google.com/docs/authentication/application-default-credentials). + +When accessing a public bucket, you shouldn't need to authenticate. + +## Examples + +### Load a single file from a GCS bucket + +```java +GoogleCloudStorageDocumentLoader gcsLoader = GoogleCloudStorageDocumentLoader.builder() + .project(System.getenv("GCP_PROJECT_ID")) + .build(); + +Document document = gcsLoader.loadDocument("BUCKET_NAME", "FILE_NAME.txt", new TextDocumentParser()); +``` + +### Load all files from a GCS bucket + +```java +GoogleCloudStorageDocumentLoader gcsLoader = GoogleCloudStorageDocumentLoader.builder() + .project(System.getenv("GCP_PROJECT_ID")) + .build(); + +List documents = gcsLoader.loadDocuments("BUCKET_NAME", new TextDocumentParser()); +``` + +### Load all files from a GCS bucket with a glob pattern + +```java +GoogleCloudStorageDocumentLoader gcsLoader = GoogleCloudStorageDocumentLoader.builder() + .project(System.getenv("GCP_PROJECT_ID")) + .build(); + +List documents = gcsLoader.loadDocuments("BUCKET_NAME", "*.txt", new TextDocumentParser()); +``` + +For more code samples, please have a look at the integration test class: +- [GoogleCloudStorageDocumentLoaderIT](https://github.com/langchain4j/langchain4j/blob/main/document-loaders/langchain4j-document-loader-google-cloud-storage/src/test/java/dev/langchain4j/data/document/loader/gcs/GoogleCloudStorageDocumentLoaderIT.java) diff --git a/docs/docs/integrations/document-loaders/playwright.md b/docs/docs/integrations/document-loaders/playwright.md new file mode 100644 index 0000000..bb7e067 --- /dev/null +++ b/docs/docs/integrations/document-loaders/playwright.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 5 +--- + +# Playwright + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-document-loader-playwright + 1.17.2-beta27 + + +``` + + +## APIs + +- `PlaywrightDocumentLoader` + + +## Examples + +- [PlaywrightDocumentLoaderIT](https://github.com/langchain4j/langchain4j/blob/main/document-loaders/langchain4j-document-loader-playwright/src/test/java/dev/langchain4j/data/document/loader/playwright/PlaywrightDocumentLoaderIT.java) diff --git a/docs/docs/integrations/document-loaders/selenium.md b/docs/docs/integrations/document-loaders/selenium.md new file mode 100644 index 0000000..8d0523f --- /dev/null +++ b/docs/docs/integrations/document-loaders/selenium.md @@ -0,0 +1,26 @@ +--- +sidebar_position: 5 +--- + +# Selenium + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-document-loader-selenium + 1.17.2-beta27 + +``` + + +## APIs + +- `SeleniumDocumentLoader` + + +## Examples + +- [SeleniumDocumentLoaderIT](https://github.com/langchain4j/langchain4j/blob/main/document-loaders/langchain4j-document-loader-selenium/src/test/java/dev/langchain4j/data/document/loader/selenium/SeleniumDocumentLoaderIT.java) diff --git a/docs/docs/integrations/document-loaders/tencent-cos.md b/docs/docs/integrations/document-loaders/tencent-cos.md new file mode 100644 index 0000000..a493c88 --- /dev/null +++ b/docs/docs/integrations/document-loaders/tencent-cos.md @@ -0,0 +1,26 @@ +--- +sidebar_position: 6 +--- + +# Tencent COS + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-document-loader-tencent-cos + 1.17.2-beta27 + +``` + + +## APIs + +- `TencentCosDocumentLoader` + + +## Examples + +- [TencentCosDocumentLoaderIT](https://github.com/langchain4j/langchain4j/blob/main/document-loaders/langchain4j-document-loader-tencent-cos/src/test/java/dev/langchain4j/data/document/loader/tencent/cos/TencentCosDocumentLoaderIT.java) diff --git a/docs/docs/integrations/document-loaders/url.md b/docs/docs/integrations/document-loaders/url.md new file mode 100644 index 0000000..8486b75 --- /dev/null +++ b/docs/docs/integrations/document-loaders/url.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 7 +--- + +# URL + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j + 1.17.2 + +``` + + +## APIs + +- `UrlDocumentLoader` + + +## Examples + +- [UrlDocumentLoaderTest](https://github.com/langchain4j/langchain4j/blob/main/langchain4j/src/test/java/dev/langchain4j/data/document/loader/UrlDocumentLoaderTest.java) + diff --git a/docs/docs/integrations/document-parsers/_category_.json b/docs/docs/integrations/document-parsers/_category_.json new file mode 100644 index 0000000..c243ffb --- /dev/null +++ b/docs/docs/integrations/document-parsers/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Document Parsers", + "position": 7, + "link": { + "type": "generated-index", + "description": "Document Parsers" + } +} diff --git a/docs/docs/integrations/document-parsers/apache-pdfbox.md b/docs/docs/integrations/document-parsers/apache-pdfbox.md new file mode 100644 index 0000000..7e4213d --- /dev/null +++ b/docs/docs/integrations/document-parsers/apache-pdfbox.md @@ -0,0 +1,26 @@ +--- +sidebar_position: 4 +--- + +# Apache PDFBox + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-document-parser-apache-pdfbox + 1.17.2-beta27 + +``` + + +## APIs + +- `ApachePdfBoxDocumentParser` + + +## Examples + +- [ApachePdfBoxDocumentParserTest](https://github.com/langchain4j/langchain4j/blob/main/document-parsers/langchain4j-document-parser-apache-pdfbox/src/test/java/dev/langchain4j/data/document/parser/apache/pdfbox/ApachePdfBoxDocumentParserTest.java) diff --git a/docs/docs/integrations/document-parsers/apache-poi.md b/docs/docs/integrations/document-parsers/apache-poi.md new file mode 100644 index 0000000..f42c853 --- /dev/null +++ b/docs/docs/integrations/document-parsers/apache-poi.md @@ -0,0 +1,26 @@ +--- +sidebar_position: 3 +--- + +# Apache POI + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-document-parser-apache-poi + 1.17.2-beta27 + +``` + + +## APIs + +- `ApachePoiDocumentParser` + + +## Examples + +- [ApachePoiDocumentParserTest](https://github.com/langchain4j/langchain4j/blob/main/document-parsers/langchain4j-document-parser-apache-poi/src/test/java/dev/langchain4j/data/document/parser/apache/poi/ApachePoiDocumentParserTest.java) diff --git a/docs/docs/integrations/document-parsers/apache-tika.md b/docs/docs/integrations/document-parsers/apache-tika.md new file mode 100644 index 0000000..78dd064 --- /dev/null +++ b/docs/docs/integrations/document-parsers/apache-tika.md @@ -0,0 +1,26 @@ +--- +sidebar_position: 2 +--- + +# Apache Tika + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-document-parser-apache-tika + 1.17.2-beta27 + +``` + + +## APIs + +- `ApacheTikaDocumentParser` + + +## Examples + +- [ApacheTikaDocumentParserTest](https://github.com/langchain4j/langchain4j/blob/main/document-parsers/langchain4j-document-parser-apache-tika/src/test/java/dev/langchain4j/data/document/parser/apache/tika/ApacheTikaDocumentParserTest.java) diff --git a/docs/docs/integrations/document-parsers/docling.md b/docs/docs/integrations/document-parsers/docling.md new file mode 100644 index 0000000..df1542b --- /dev/null +++ b/docs/docs/integrations/document-parsers/docling.md @@ -0,0 +1,69 @@ +--- +sidebar_position: 7 +--- + +# Docling + +[Docling](https://docling.ai) is an IBM Research document processing engine that extracts text and structure from various document formats including PDF, DOCX, PPTX, and more. It provides advanced capabilities such as OCR, table extraction, and layout analysis. + +This integration communicates with a running [docling-serve](https://github.com/docling-project/docling-serve) instance via REST API and is built using the [official Docling Java library](https://docling-project.github.io/docling-java/current/). + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-document-parser-docling + 1.17.2-beta27 + +``` + +This module depends on `docling-serve-api` (the interface) and includes `docling-serve-client` (the reference HTTP client) as an optional runtime dependency. + +**If you are not using Spring Boot or Quarkus** (which may provide their own `DoclingServeApi` implementation), you must also add the reference client explicitly: + +```xml + + ai.docling + docling-serve-client + 0.5.1 + +``` + +Frameworks such as [Quarkus](https://quarkus.io) or [Spring Boot](https://spring.io/projects/spring-boot) provide their own integrations with Docling. See the [Docling Java Documentation](https://docling-project.github.io/docling-java/dev/docling-serve/serve-client/#when-to-use-this-module) for how to wire in those specific implementations. + + +## Usage + +Start a `docling-serve` instance (see [docling-serve docs](https://github.com/docling-project/docling-serve)), then build a `DoclingServeApi` client and pass it to the parser: + +```java +DoclingServeApi api = DoclingServeApi.builder() + .baseUrl("http://localhost:5001") + .build(); + +DoclingDocumentParser parser = new DoclingDocumentParser(api); +Document document = parser.parse(inputStream); +String text = document.text(); +``` + +To customize Docling processing, use the constructor that also accepts a [`ConvertDocumentOptions`](https://docling-project.github.io/docling-java/dev/docling-serve/serve-api/#requests-convertdocumentrequest): + +```java +ConvertDocumentOptions options = ConvertDocumentOptions.builder() + // configure options here + .build(); + +DoclingDocumentParser parser = new DoclingDocumentParser(api, options); +``` + +## APIs + +- `DoclingDocumentParser` + + +## Examples + +- [DoclingDocumentParserTest](https://github.com/langchain4j/langchain4j/blob/main/document-parsers/langchain4j-document-parser-docling/src/test/java/dev/langchain4j/data/document/parser/docling/DoclingDocumentParserTest.java) +- [DoclingDocumentParserIT](https://github.com/langchain4j/langchain4j/blob/main/document-parsers/langchain4j-document-parser-docling/src/test/java/dev/langchain4j/data/document/parser/docling/DoclingDocumentParserIT.java) diff --git a/docs/docs/integrations/document-parsers/markdown.md b/docs/docs/integrations/document-parsers/markdown.md new file mode 100644 index 0000000..6246e01 --- /dev/null +++ b/docs/docs/integrations/document-parsers/markdown.md @@ -0,0 +1,26 @@ +--- +sidebar_position: 4 +--- + +# Markdown + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-document-parser-markdown + 1.17.2-beta27 + +``` + + +## APIs + +- `MarkdownDocumentParser` + + +## Examples + +- [MarkdownDocumentParserTest](https://github.com/langchain4j/langchain4j/blob/main/document-parsers/langchain4j-document-parser-markdown/src/test/java/dev/langchain4j/data/document/parser/markdown/MarkdownDocumentParserTest.java) diff --git a/docs/docs/integrations/document-parsers/text.md b/docs/docs/integrations/document-parsers/text.md new file mode 100644 index 0000000..5fbf52d --- /dev/null +++ b/docs/docs/integrations/document-parsers/text.md @@ -0,0 +1,26 @@ +--- +sidebar_position: 1 +--- + +# Text + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j + 1.17.2 + +``` + + +## APIs + +- `TextDocumentParser` + + +## Examples + +- [TextDocumentParserTest](https://github.com/langchain4j/langchain4j/blob/main/langchain4j/src/test/java/dev/langchain4j/data/document/parser/TextDocumentParserTest.java) diff --git a/docs/docs/integrations/document-parsers/yaml.md b/docs/docs/integrations/document-parsers/yaml.md new file mode 100644 index 0000000..f066ee3 --- /dev/null +++ b/docs/docs/integrations/document-parsers/yaml.md @@ -0,0 +1,25 @@ +--- +sidebar_position: 4 +--- + +# YAML + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-document-parser-yaml + 1.17.2-beta27 + +``` + +## APIs + +- `YamlDocumentParser` + + +## Examples + +- [YamlDocumentParserTest](https://github.com/langchain4j/langchain4j/blob/main/document-parsers/langchain4j-document-parser-yaml/src/test/java/dev/langchain4j/data/document/parser/yaml/YamlDocumentParserTest.java) diff --git a/docs/docs/integrations/embedding-models/1-in-process.md b/docs/docs/integrations/embedding-models/1-in-process.md new file mode 100644 index 0000000..0d24b9f --- /dev/null +++ b/docs/docs/integrations/embedding-models/1-in-process.md @@ -0,0 +1,91 @@ +--- +sidebar_position: 1 +--- + +# In-process (ONNX) + +LangChain4j provides a few popular local embedding models packaged as maven dependencies. +They are powered by [ONNX runtime](https://onnxruntime.ai/docs/get-started/with-java.html) +and are running in the same java process. + +Each model is provided in 2 flavours: original and quantized (has a `-q` suffix in maven artifact name and `Quantized` in the class name). + +For example: +```xml + + dev.langchain4j + langchain4j-embeddings-all-minilm-l6-v2 + 1.17.2-beta27 + +``` +```java +EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel(); +Response response = embeddingModel.embed("test"); +Embedding embedding = response.content(); +``` + +Or quantized: +```xml + + dev.langchain4j + langchain4j-embeddings-all-minilm-l6-v2-q + 1.17.2-beta27 + +``` +```java +EmbeddingModel embeddingModel = new AllMiniLmL6V2QuantizedEmbeddingModel(); +Response response = embeddingModel.embed("test"); +Embedding embedding = response.content(); +``` + +The complete list of all embedding models can be found [here](https://github.com/langchain4j/langchain4j/tree/main/embeddings). + + +## Parallelization + +By default, the embedding process is parallelized using all available CPU cores, +so each `TextSegment` is embedded in a separate thread. + +The parallelization is done by using an `Executor`. +By default, in-process embedding models use a cached thread pool +with the number of threads equal to the number of available processors. +Threads are cached for 1 second. + +You can provide a custom instance of the `Executor` when creating a model: +```java +Executor = ...; +EmbeddingModel embeddingModel = new AllMiniLmL6V2QuantizedEmbeddingModel(executor); +``` + +Embedding using GPU is not supported yet. + +## Custom models + +Many models (e.g., from [Hugging Face](https://huggingface.co/)) can be used, +as long as they are in the ONNX format. + +Information on how to convert models into ONNX format can be found [here](https://huggingface.co/docs/optimum/exporters/onnx/usage_guides/export_a_model). + +Many models already converted to ONNX format are available [here](https://huggingface.co/Xenova). + +Example of using custom embedding model: +```xml + + dev.langchain4j + langchain4j-embeddings + 1.17.2-beta27 + +``` +```java +String pathToModel = "/home/langchain4j/model.onnx"; +String pathToTokenizer = "/home/langchain4j/tokenizer.json"; +PoolingMode poolingMode = PoolingMode.MEAN; +EmbeddingModel embeddingModel = new OnnxEmbeddingModel(pathToModel, pathToTokenizer, poolingMode); + +Response response = embeddingModel.embed("test"); +Embedding embedding = response.content(); +``` + +## Examples + +- [InProcessEmbeddingModelExamples](https://github.com/langchain4j/langchain4j-examples/blob/main/other-examples/src/main/java/embedding/model/InProcessEmbeddingModelExamples.java) diff --git a/docs/docs/integrations/embedding-models/_category_.json b/docs/docs/integrations/embedding-models/_category_.json new file mode 100644 index 0000000..ac0b9c6 --- /dev/null +++ b/docs/docs/integrations/embedding-models/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Embedding Models", + "position": 2, + "link": { + "type": "generated-index", + "description": "Embedding Models" + } +} diff --git a/docs/docs/integrations/embedding-models/amazon-bedrock.md b/docs/docs/integrations/embedding-models/amazon-bedrock.md new file mode 100644 index 0000000..7451900 --- /dev/null +++ b/docs/docs/integrations/embedding-models/amazon-bedrock.md @@ -0,0 +1,75 @@ +--- +sidebar_position: 2 +--- + +# Amazon Bedrock + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-bedrock + 1.17.2 + +``` + + +## AWS credentials +In order to use Amazon Bedrock embeddings, you need to configure AWS credentials. +One of the options is to set the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. +More information can be found [here](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html). + +## Cohere Models +- `BedrockCohereEmbeddingModel` + +## Cohere Embedding Models +Support is provided for Bedrock Cohere embedding models, enabling the use of the following versions: + +- **`cohere.embed-english-v3`** +- **`cohere.embed-multilingual-v3`** + +These models are ideal for generating high-quality text embeddings for English and multilingual text processing tasks. + +### Implementation Example + +Below is an example of how to configure and use a Bedrock embedding model: + +``` +BedrockCohereEmbeddingModel embeddingModel = BedrockCohereEmbeddingModel + .builder() + .region(Region.US_EAST_1) + .model("cohere.embed-multilingual-v3") + .inputType(BedrockCohereEmbeddingModel.InputType.SEARCH_QUERY) + .truncation(BedrockCohereEmbeddingModel.Truncate.NONE) + .build(); +``` + +## APIs + +- `BedrockTitanEmbeddingModel` +- `BedrockCohereEmbeddingModel` + +## Titan Multimodal Embeddings + +`BedrockTitanEmbeddingModel` supports Amazon Titan Multimodal Embeddings (`amazon.titan-embed-image-v1`): it +embeds text and/or a single image into one (fused) embedding. + +```java +EmbeddingModel model = BedrockTitanEmbeddingModel.builder() + .model("amazon.titan-embed-image-v1") + .region(Region.US_EAST_1) + .build(); + +EmbeddingResponse response = model.embed(EmbeddingRequest.builder() + .input(TextContent.from("a photo of a cat"), ImageContent.from(base64Data, "image/png")) + .build()); +``` + +Titan requires **base64** image data (a URL is not supported). Listeners can be configured via +`.listeners(...)`. See [Embedding Model](/tutorials/rag#embedding-model) for the request/response API. + +## Examples + +- [BedrockEmbeddingIT](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-bedrock/src/test/java/dev/langchain4j/model/bedrock/BedrockEmbeddingIT.java) diff --git a/docs/docs/integrations/embedding-models/azure-open-ai.md b/docs/docs/integrations/embedding-models/azure-open-ai.md new file mode 100644 index 0000000..4713559 --- /dev/null +++ b/docs/docs/integrations/embedding-models/azure-open-ai.md @@ -0,0 +1,73 @@ +--- +sidebar_position: 3 +--- + +# Azure OpenAI + +:::note + +This is the documentation for the `Azure OpenAI` integration, that uses the Azure SDK from Microsoft, and works best if you are using the Microsoft Java stack, including advanced Azure authentication mechanisms. + +LangChain4j provides 3 different integrations with OpenAI for using embedding models, and this is #3 : + +- [OpenAI](/integrations/language-models/open-ai) uses a custom Java implementation of the OpenAI REST API, that works best with Quarkus (as it uses the Quarkus REST client) and Spring (as it uses Spring's RestClient). +- [OpenAI Official SDK](/integrations/language-models/open-ai-official) uses the official OpenAI Java SDK. +- [Azure OpenAI](/integrations/language-models/azure-open-ai) uses the Azure SDK from Microsoft, and works best if you are using the Microsoft Java stack, including advanced Azure authentication mechanisms. + +::: + +Azure OpenAI provides a few embedding models (`text-embedding-3-small`, `text-embedding-ada-002`, etc.) +that can be used to transforms text or images into a dimensional vector space. + +## Maven Dependency + +### Plain Java +```xml + + dev.langchain4j + langchain4j-azure-open-ai + 1.17.2 + +``` + +### Spring Boot +```xml + + dev.langchain4j + langchain4j-azure-open-ai-spring-boot-starter + 1.17.2-beta27 + +``` + + +## Creating `AzureOpenAiEmbeddingModel` + +### Plain Java +```java +EmbeddingModel model = AzureOpenAiEmbeddingModel.builder() + .apiKey(System.getenv("AZURE_OPENAI_KEY")) + .deploymentName("text-embedding-3-small") + .endpoint("https://langchain4j.openai.azure.com/") + ... + .build(); +``` + +### Spring Boot +Add to the `application.properties`: +```properties +langchain4j.azure-open-ai.embedding-model.endpoint=https://langchain4j.openai.azure.com/ +langchain4j.azure-open-ai.embedding-model.service-version=... +langchain4j.azure-open-ai.embedding-model.api-key=${AZURE_OPENAI_KEY} +langchain4j.azure-open-ai.embedding-model.deployment-name=text-embedding-3-small +langchain4j.azure-open-ai.embedding-model.timeout=... +langchain4j.azure-open-ai.embedding-model.max-retries=... +langchain4j.azure-open-ai.embedding-model.log-requests-and-responses=... +langchain4j.azure-open-ai.embedding-model.user-agent-suffix=... +langchain4j.azure-open-ai.embedding-model.dimensions=... +langchain4j.azure-open-ai.embedding-model.customHeaders=... +``` + + +## Examples + +- [AzureOpenAiEmbeddingModelExamples](https://github.com/langchain4j/langchain4j-examples/blob/main/azure-open-ai-examples/src/main/java/AzureOpenAiEmbeddingModelExamples.java) diff --git a/docs/docs/integrations/embedding-models/cohere.md b/docs/docs/integrations/embedding-models/cohere.md new file mode 100644 index 0000000..bf064b5 --- /dev/null +++ b/docs/docs/integrations/embedding-models/cohere.md @@ -0,0 +1,33 @@ +--- +sidebar_position: 4 +--- + +# Cohere + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-cohere + 1.17.2-beta27 + +``` + +## APIs + +- `CohereEmbeddingModel` + +## Capabilities + +- **Multimodal** (`embed-v4.0`): embeds text and images into a shared vector space; interleaved text + image is + fused into a single embedding. Provide image inputs as `ImageContent` (URL or base64) in an `EmbeddingRequest`. +- **Per-call parameters**: `input_type` — embed queries and documents differently + (`EmbeddingInputType.QUERY` / `DOCUMENT`, mapped to Cohere's `search_query` / `search_document`). +- **Listeners**: configure via `CohereEmbeddingModel.builder().listeners(...)`. + +See [Embedding Model](/tutorials/rag#embedding-model) for the request/response API and multimodal usage. + +## Examples + +- [CohereEmbeddingModelIT](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-cohere/src/test/java/dev/langchain4j/model/cohere/CohereEmbeddingModelIT.java) diff --git a/docs/docs/integrations/embedding-models/dashscope.md b/docs/docs/integrations/embedding-models/dashscope.md new file mode 100644 index 0000000..9f546f8 --- /dev/null +++ b/docs/docs/integrations/embedding-models/dashscope.md @@ -0,0 +1,77 @@ +--- +sidebar_position: 5 +--- + +# DashScope + +[DashScope](https://dashscope.aliyun.com/) is a platform developed by [Alibaba Cloud](https://www.alibabacloud.com/). +It provides an interface for model visualization, monitoring, and debugging, particularly when working with AI/ML +models in production environments. The platform allows users to visualize performance metrics, track model behavior, and +identify potential issues early on in the deployment cycle. + +[Qwen](https://tongyi.aliyun.com/) models are a series of generative AI models developed +by [Alibaba Cloud](https://www.alibabacloud.com/). The Qwen family of models are specifically designed for tasks like +text generation, summarization, question answering, and various NLP tasks. + +You can refer +to [DashScope Document](https://help.aliyun.com/zh/model-studio/getting-started/?spm=a2c4g.11186623.help-menu-2400256.d_0.6655453aLIyxGp) +for more details. LangChain4j integrates with DashScope by +Using [DashScope Java SDK](https://help.aliyun.com/zh/dashscope/java-sdk-best-practices?spm=a2c4g.11186623.0.0.272a1507Ne69ja) + +## Maven Dependency + +:::note +Since `1.0.0-alpha1`, `langchain4j-dashscope` has migrated to `langchain4j-community` and is renamed to +`langchain4j-community-dashscope`. +::: + +Before `1.0.0-alpha1`: + +```xml + + + dev.langchain4j + langchain4j-dashscope + ${previous version here} + +``` + +`1.0.0-alpha1` and later: + +```xml + + + dev.langchain4j + langchain4j-community-dashscope + ${latest version here} + +``` + +Or, you can use BOM to manage dependencies consistently: + +```xml + + + + dev.langchain4j + langchain4j-community-bom + ${latest version here} + pom + import + + +``` + +## Configurable Parameters + +`QwenEmbeddingModel` has following parameters to configure when you initialize it: + +| Property | Description | Default Value | +|-----------|------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------| +| baseUrl | The URL to connect to. You can use HTTP or websocket to connect to DashScope | https://dashscope.aliyuncs.com/api/v1/services/embeddings/text-embedding/text-embedding | +| apiKey | The API Key | | +| modelName | The model to use. | text-embedding-v2 | + +## Examples + +- [QwenEmbeddingModelIT](https://github.com/langchain4j/langchain4j-community/blob/main/models/langchain4j-community-dashscope/src/test/java/dev/langchain4j/community/model/dashscope/QwenEmbeddingModelIT.java) diff --git a/docs/docs/integrations/embedding-models/github-models.md b/docs/docs/integrations/embedding-models/github-models.md new file mode 100644 index 0000000..742a7c4 --- /dev/null +++ b/docs/docs/integrations/embedding-models/github-models.md @@ -0,0 +1,74 @@ +--- +sidebar_position: 6 +--- + +# GitHub Models [Deprecated] + +This module is deprecated, please use the [OpenAI Official SDK module](/integrations/language-models/open-ai-official) instead. + +:::note + +This is the documentation for the `GitHub Models` integration, that uses the Azure AI Inference API to access GitHub Models. + +LangChain4j provides 4 different integrations with OpenAI for using embedding models, and this is #4 : + +- [OpenAI](/integrations/language-models/open-ai) uses a custom Java implementation of the OpenAI REST API, that works best with Quarkus (as it uses the Quarkus REST client) and Spring (as it uses Spring's RestClient). +- [OpenAI Official SDK](/integrations/language-models/open-ai-official) uses the official OpenAI Java SDK. +- [Azure OpenAI](/integrations/language-models/azure-open-ai) uses the Azure SDK from Microsoft, and works best if you are using the Microsoft Java stack, including advanced Azure authentication mechanisms. +- [GitHub Models](/integrations/language-models/github-models) uses the Azure AI Inference API to access GitHub Models. + +::: + +If you want to develop a generative AI application, you can use GitHub Models to find and experiment with AI models for free. +Once you are ready to bring your application to production, you can switch to a token from a paid Azure account. + +## GitHub Models Documentation + +- [GitHub Models Documentation](https://docs.github.com/en/github-models) +- [GitHub Models Marketplace](https://github.com/marketplace/models) + +## Maven Dependency + +### Plain Java + +```xml + + dev.langchain4j + langchain4j-github-models + 1.17.2-beta27 + +``` + +## GitHub token + +To use GitHub Models, you need to use a GitHub token for authentication. + +Token are created and managed in [GitHub Developer Settings > Personal access tokens](https://github.com/settings/tokens). + +Once you have a token, you can set it as an environment variable and use it in your code: + +```bash +export GITHUB_TOKEN="" +``` + +## Creating a `GitHubModelsEmbeddingModel` with a GitHub token + +```java +GitHubModelsEmbeddingModel model = GitHubModelsEmbeddingModel.builder() + .gitHubToken(System.getenv("GITHUB_TOKEN")) + .modelName(TEXT_EMBEDDING_3_SMALL) + .logRequestsAndResponses(true) + .build(); +``` + +This will create an instance of `GitHubModelsEmbeddingModel`. + +## Using the model + +```java +Response response = model.embed("Please embed this sentence."); +``` + +## Examples + +- [GitHub Models Examples](https://github.com/langchain4j/langchain4j-examples/tree/main/github-models-examples/src/main/java) diff --git a/docs/docs/integrations/embedding-models/google-ai-gemini.md b/docs/docs/integrations/embedding-models/google-ai-gemini.md new file mode 100644 index 0000000..9b417b6 --- /dev/null +++ b/docs/docs/integrations/embedding-models/google-ai-gemini.md @@ -0,0 +1,461 @@ +# Google AI Gemini Embeddings + +https://ai.google.dev/gemini-api/docs/embeddings + +## Table of Contents + +- [Maven Dependency](#maven-dependency) +- [API Key](#api-key) +- [Models Available](#models-available) +- [GoogleAiEmbeddingModel](#googleaiembeddingmodel) + - [Basic Usage](#basic-usage) + - [Embedding Multiple Texts](#embedding-multiple-texts) + - [Configuring the Embedding Model](#configuring-the-embedding-model) + - [Task Types](#task-types) + - [Using Metadata for Document Titles](#using-metadata-for-document-titles) + - [Output Dimensionality](#output-dimensionality) + - [Batch Processing](#batch-processing) +- [Batch Embedding Processing](#batch-embedding-processing) + - [GoogleAiGeminiBatchEmbeddingModel](#googleaigeminibatchembeddingmodel) + - [Creating Batch Embedding Jobs](#creating-batch-embedding-jobs) + - [Handling Batch Responses](#handling-batch-responses) + - [Polling for Results](#polling-for-results) + - [Managing Batch Jobs](#managing-batch-jobs) + - [File-Based Batch Processing](#file-based-batch-processing) + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-google-ai-gemini + 1.17.2 + +``` + +## API Key + +Get an API key for free here: https://ai.google.dev/gemini-api/docs/api-key . + +## Models available + +Check the list of [available models](https://ai.google.dev/gemini-api/docs/embeddings#model-versions) in the documentation. + +* `gemini-embedding-001` + * Input token limit: 2,048 + * Output dimension size: Flexible, supports: 128 - 3072, Recommended: 768, 1536, 3072 + +## GoogleAiEmbeddingModel + +The `GoogleAiEmbeddingModel` allows you to generate embeddings from text using Google AI Gemini's embedding models. + +### Basic Usage + +```java +EmbeddingModel embeddingModel = GoogleAiEmbeddingModel.builder() + .apiKey(System.getenv("GEMINI_AI_KEY")) + .modelName("gemini-embedding-001") + .build(); + +Response response = embeddingModel.embed("Hello, world!"); +Embedding embedding = response.content(); +``` + +### Embedding Multiple Texts + +```java +List segments = List.of( + TextSegment.from("First document"), + TextSegment.from("Second document"), + TextSegment.from("Third document") +); + +Response> response = embeddingModel.embedAll(segments); +List embeddings = response.content(); +``` + +### Configuring the Embedding Model + +```java +EmbeddingModel embeddingModel = GoogleAiEmbeddingModel.builder() + .apiKey(System.getenv("GEMINI_AI_KEY")) + .modelName("gemini-embedding-001") + .taskType(GoogleAiEmbeddingModel.TaskType.RETRIEVAL_DOCUMENT) + .outputDimensionality(768) + .titleMetadataKey("title") + .maxRetries(3) + .timeout(Duration.ofSeconds(30)) + .logRequestsAndResponses(true) + .build(); +``` + +### Task Types + +The `taskType` parameter optimizes the embedding for specific use cases: + +- `RETRIEVAL_QUERY`: For search queries +- `RETRIEVAL_DOCUMENT`: For documents to be retrieved (default for document indexing) +- `SEMANTIC_SIMILARITY`: For measuring text similarity +- `CLASSIFICATION`: For text classification tasks +- `CLUSTERING`: For grouping similar texts +- `QUESTION_ANSWERING`: For Q&A systems +- `FACT_VERIFICATION`: For fact-checking applications + +### Using Metadata for Document Titles + +When using `TaskType.RETRIEVAL_DOCUMENT`, you can provide document titles via metadata: + +```java +EmbeddingModel embeddingModel = GoogleAiEmbeddingModel.builder() + .apiKey(System.getenv("GEMINI_AI_KEY")) + .modelName("gemini-embedding-001") + .taskType(GoogleAiEmbeddingModel.TaskType.RETRIEVAL_DOCUMENT) + .titleMetadataKey("title") // defaults to "title" + .build(); + +TextSegment segment = TextSegment.from( + "This is the document content", + Metadata.from("title", "My Document Title") +); + +Response response = embeddingModel.embed(segment); +``` + +### Output Dimensionality + +You can specify the output dimensionality to reduce the embedding size: + +```java +EmbeddingModel embeddingModel = GoogleAiEmbeddingModel.builder() + .apiKey(System.getenv("GEMINI_AI_KEY")) + .modelName("gemini-embedding-001") + .outputDimensionality(256) // Reduce from default 768 dimensions + .build(); +``` + +### Batch Processing + +The model automatically batches requests when embedding multiple segments, with a maximum of 100 segments per batch for optimal performance. + +**Note:** This is not the discounted batch API, instead this is a convenience method for processing multiple embeddings. + +## Batch Embedding Processing + +The `GoogleAiGeminiBatchEmbeddingModel` provides an interface for processing large volumes of embedding requests asynchronously at a reduced cost (50% of standard pricing). It is ideal for non-urgent, large-scale embedding tasks with a 24-hour turnaround SLO. + +### Creating Batch Embedding Jobs + +**Inline batch creation:** + +```java +GoogleAiGeminiBatchEmbeddingModel batchModel = GoogleAiGeminiBatchEmbeddingModel.builder() + .apiKey(System.getenv("GEMINI_AI_KEY")) + .modelName("gemini-embedding-001") + .taskType(GoogleAiEmbeddingModel.TaskType.RETRIEVAL_DOCUMENT) + .outputDimensionality(768) + .build(); + +// Create batch of text segments +List segments = List.of( + TextSegment.from("First document to embed"), + TextSegment.from("Second document to embed"), + TextSegment.from("Third document to embed") +); + +// Submit the batch (generic API) +BatchResponse> response = batchModel.submit(new BatchRequest<>(segments)); + +// Or, to set a Gemini-specific display name and priority, use GeminiBatchRequest: +BatchResponse> response = batchModel.submit(GeminiBatchRequest.from( + segments, + "Document Embeddings Batch", // display name + 0L // priority (optional, defaults to 0) +)); +``` + +**File-based batch creation:** + +For larger batches, you can create a batch from an uploaded file: + +```java +// First, upload a file with batch requests +GeminiFiles filesApi = GeminiFiles.builder() + .apiKey(System.getenv("GEMINI_AI_KEY")) + .build(); + +GeminiFile uploadedFile = filesApi.uploadFile( + Paths.get("batch_embeddings.jsonl"), + "Batch Embedding Requests" +); + +// Wait for file to be active +while (uploadedFile.isProcessing()) { + Thread.sleep(1000); + uploadedFile = filesApi.getMetadata(uploadedFile.name()); +} + +// Create batch from file +BatchResponse> response = batchModel.submit("My Embedding Batch Job", uploadedFile); +``` + +### Handling Batch Responses + +A `BatchResponse` exposes the current `state()` together with the per-request `results()` and the +`responses()` / `errors()` convenience views. Branch on the `state()` (use `state().isTerminal()` +to tell whether the batch is still in progress): + +```java +BatchResponse> response = batchModel.submit(new BatchRequest<>(segments)); + +if (!response.state().isTerminal()) { + System.out.println("Batch is " + response.state()); + System.out.println("Batch ID: " + response.batchId()); +} else if (response.state() == BatchState.SUCCEEDED) { + System.out.println("Batch completed successfully!"); + for (Response embeddingResponse : response.responses()) { + Embedding embedding = embeddingResponse.content(); + System.out.println("Embedding dimensions: " + embedding.dimension()); + } +} else { + System.err.println("Batch " + response.state() + ": " + response.errors()); +} +``` + +`responses()` and `errors()` are convenience views and are never `null` (empty when there is nothing +to report). + +### Correlating Results with Requests + +`responses()` and `errors()` are flat views that lose track of which input produced which outcome. +When you need to map every outcome back to its originating segment, use `results()` instead: it +returns one `BatchItemResult` per request, **in the same order as the submitted segments**, so the +i-th result corresponds to the i-th segment. Each result is either a `BatchItemResult.Success` +(carrying the `response()`) or a `BatchItemResult.Failure` (carrying the `error()`): + +```java +List>> results = response.results(); +for (int i = 0; i < results.size(); i++) { + BatchItemResult> item = results.get(i); + if (item.isSuccess()) { + System.out.println("Segment #" + i + " -> " + item.response().content().dimension() + " dimensions"); + } else { + BatchError error = item.error(); + System.err.println("Segment #" + i + " failed: " + error.code() + " - " + error.message()); + } +} +``` + +### Polling for Results + +Since batch processing is asynchronous, you need to poll for results: + +```java +BatchResponse> result = batchModel.submit(new BatchRequest<>(segments)); +String batchId = result.batchId(); + +while (!result.state().isTerminal()) { + Thread.sleep(5000); // Wait 5 seconds between polls + result = batchModel.retrieve(batchId); +} + +// Process final result +if (result.state() == BatchState.SUCCEEDED) { + List> embeddings = result.responses(); + System.out.println("Generated " + embeddings.size() + " embeddings"); +} else { + System.err.println("Batch did not succeed: " + result.state()); +} +``` + +### Managing Batch Jobs + +**Cancel a batch job:** + +```java +String batchId = // ... obtained from submit(...) + +try { + batchModel.cancel(batchId); + System.out.println("Batch cancelled successfully"); +} catch (HttpException e) { + System.err.println("Failed to cancel batch: " + e.getMessage()); +} +``` + +**Delete a batch job:** + +```java +batchModel.deleteBatchJob(batchId); +System.out.println("Batch deleted successfully"); +``` + +**List batch jobs:** + +```java +// List first page of batch jobs +BatchPage> page = batchModel.list(new BatchPagination(10, null)); + +for (BatchResponse> batch : page.batches()) { + System.out.println("Batch: " + batch); +} + +// Get next page if available +if (page.nextPageToken() != null) { + BatchPage> nextPage = batchModel.list(new BatchPagination(10, page.nextPageToken())); +} +``` + +### File-Based Batch Processing + +For advanced use cases, you can write batch requests to a JSONL file and upload it: + +```java +// Create a JSONL file with batch requests +Path batchFile = Files.createTempFile("batch", ".jsonl"); + +try (JsonLinesWriter writer = new StreamingJsonLinesWriter(batchFile)) { + List> fileRequests = List.of( + new BatchFileRequest<>("segment-1", TextSegment.from("First document")), + new BatchFileRequest<>("segment-2", TextSegment.from("Second document")), + new BatchFileRequest<>("segment-3", TextSegment.from("Third document")) + ); + + batchModel.writeBatchToFile(writer, fileRequests); +} + +// Upload the file +GeminiFiles filesApi = GeminiFiles.builder() + .apiKey(System.getenv("GEMINI_AI_KEY")) + .build(); + +GeminiFile uploadedFile = filesApi.uploadFile(batchFile, "Batch Embedding Requests"); + +// Create batch from file +BatchResponse> response = batchModel.submit("File-Based Embedding Batch", uploadedFile); +``` + +### Using Metadata with Batch Embeddings + +When using `TaskType.RETRIEVAL_DOCUMENT`, you can include document titles via metadata: + +```java +GoogleAiGeminiBatchEmbeddingModel batchModel = GoogleAiGeminiBatchEmbeddingModel.builder() + .apiKey(System.getenv("GEMINI_AI_KEY")) + .modelName("gemini-embedding-001") + .taskType(GoogleAiEmbeddingModel.TaskType.RETRIEVAL_DOCUMENT) + .titleMetadataKey("title") + .build(); + +List segments = List.of( + TextSegment.from( + "Content of first document", + Metadata.from("title", "First Document Title") + ), + TextSegment.from( + "Content of second document", + Metadata.from("title", "Second Document Title") + ) +); + +BatchResponse> response = batchModel.submit(GeminiBatchRequest.from( + segments, "Documents with Titles")); +``` + +### Configuration + +The `GoogleAiGeminiBatchEmbeddingModel` supports the same configuration options as `GoogleAiEmbeddingModel`: + +```java +GoogleAiGeminiBatchEmbeddingModel batchModel = GoogleAiGeminiBatchEmbeddingModel.builder() + .apiKey(System.getenv("GEMINI_AI_KEY")) + .modelName("gemini-embedding-001") + .taskType(GoogleAiEmbeddingModel.TaskType.RETRIEVAL_DOCUMENT) + .outputDimensionality(768) + .titleMetadataKey("title") + .maxRetries(3) + .timeout(Duration.ofSeconds(30)) + .logRequestsAndResponses(true) + .build(); +``` + +### Important Constraints + +- **Size Limit**: The inline API supports a total request size of 20 MB or under +- **Batch Size**: Maximum of 100 segments per batch for optimal performance +- **Cost**: Batch processing offers 50% cost reduction compared to real-time requests +- **Turnaround**: 24-hour SLO, though completion is often much quicker +- **Use Cases**: Best for large-scale embedding generation for document indexing or semantic search + +### Example: Complete Workflow + +```java +GoogleAiGeminiBatchEmbeddingModel batchModel = GoogleAiGeminiBatchEmbeddingModel.builder() + .apiKey(System.getenv("GEMINI_AI_KEY")) + .modelName("gemini-embedding-001") + .taskType(GoogleAiEmbeddingModel.TaskType.RETRIEVAL_DOCUMENT) + .outputDimensionality(768) + .build(); + +// Prepare batch of text segments +List segments = new ArrayList<>(); +for (int i = 0; i < 500; i++) { + segments.add(TextSegment.from( + "Document content #" + i, + Metadata.from("title", "Document " + i) + )); +} + +// Submit batch +BatchResponse> result = batchModel.submit(GeminiBatchRequest.from( + segments, "Large Document Collection", 0L)); +String batchId = result.batchId(); + +// Poll for completion +int attempts = 0; +int maxAttempts = 720; // 1 hour with 5-second intervals +while (!result.state().isTerminal()) { + if (attempts++ >= maxAttempts) { + throw new RuntimeException("Batch processing timeout"); + } + Thread.sleep(5000); + result = batchModel.retrieve(batchId); + System.out.println("Status: " + result.state()); +} + +// Process results +if (result.state() == BatchState.SUCCEEDED) { + List> embeddings = result.responses(); + System.out.println("Generated " + embeddings.size() + " embeddings"); + + // Store embeddings in your vector database + for (int i = 0; i < embeddings.size(); i++) { + Embedding embedding = embeddings.get(i).content(); + System.out.println("Embedding " + i + " has " + embedding.dimension() + " dimensions"); + // vectorStore.add(embedding, segments.get(i)); + } +} else { + System.err.println("Batch did not succeed: " + result.state()); +} +``` + +## Request/response API and capabilities + +Besides the builder-level `taskType(...)` shown above, `GoogleAiEmbeddingModel` supports the request/response +API with per-call parameters: + +- **Per-call parameters**: `input_type` (`EmbeddingInputType.QUERY` / `DOCUMENT`) lets you embed queries and + documents differently without configuring two model instances. For `gemini-embedding-001` it is applied via + Gemini's `RETRIEVAL_QUERY` / `RETRIEVAL_DOCUMENT` task type. Gemini Embedding 2 does not accept the task type + parameter, so it is applied as a prompt instruction instead (`task: search result | query: ...` for a query, + `title: none | text: ...` for a document) — this happens automatically. +- **Multimodal** (`gemini-embedding-2-preview`, Gemini Embedding 2): natively embeds interleaved text + image + into a single embedding. Earlier models (e.g. `gemini-embedding-001`) are text-only. Images must be provided + as base64 (`ImageContent`). +- **Listeners**: configure via `GoogleAiEmbeddingModel.builder().listeners(...)`. + +See [Embedding Model](/tutorials/rag#embedding-model) for the request/response API and multimodal usage. + +## Learn more + +If you're interested in learning more about the Google AI Gemini embedding models, please have a look at the +[documentation](https://ai.google.dev/gemini-api/docs/embeddings). diff --git a/docs/docs/integrations/embedding-models/google-genai.md b/docs/docs/integrations/embedding-models/google-genai.md new file mode 100644 index 0000000..14e9161 --- /dev/null +++ b/docs/docs/integrations/embedding-models/google-genai.md @@ -0,0 +1,106 @@ +# Google Gen AI Embeddings (Experimental) + +https://github.com/googleapis/java-genai + +This integration uses the official Google Gen AI SDK for Java (`com.google.genai:google-genai`). It is marked +**Experimental**: the API and implementation may change in future releases. + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-google-genai + 1.17.2-beta27 + +``` + +## API Key + +Get an API key for free here: https://ai.google.dev/gemini-api/docs/api-key . + +## Models available + +See the [available embedding models](https://ai.google.dev/gemini-api/docs/embeddings#model-versions), for +example: + +* `gemini-embedding-001` — text-only; supports task types and output dimensionality (128–3072). +* `gemini-embedding-2` — natively multimodal; does not use the task type parameter (see + [Google AI Gemini Embeddings](/integrations/embedding-models/google-ai-gemini) for how task instructions work + with Gemini Embedding 2). + +## GoogleGenAiEmbeddingModel + +### Basic Usage + +```java +EmbeddingModel embeddingModel = GoogleGenAiEmbeddingModel.builder() + .apiKey(System.getenv("GOOGLE_AI_GEMINI_API_KEY")) + .modelName("gemini-embedding-001") + .build(); + +Response response = embeddingModel.embed("Hello, world!"); +Embedding embedding = response.content(); +``` + +### Configuring the Embedding Model + +```java +EmbeddingModel embeddingModel = GoogleGenAiEmbeddingModel.builder() + .apiKey(System.getenv("GOOGLE_AI_GEMINI_API_KEY")) + .modelName("gemini-embedding-001") + .taskType(GoogleGenAiEmbeddingModel.TaskTypeEnum.RETRIEVAL_DOCUMENT) // default task type + .outputDimensionality(768) // reduce the embedding size (for models that support it) + .titleMetadataKey("title") // metadata key used as the document title for RETRIEVAL_DOCUMENT + .maxRetries(3) + .timeout(Duration.ofSeconds(30)) + .build(); +``` + +## Request/response API and capabilities + +Besides the convenience methods and the builder-level `taskType(...)`, `GoogleGenAiEmbeddingModel` supports the +request/response API with per-call parameters: + +- **Input type**: `EmbeddingInputType.QUERY` / `DOCUMENT` is mapped to the SDK's `RETRIEVAL_QUERY` / + `RETRIEVAL_DOCUMENT` task type, so you can embed queries and documents differently without configuring two + model instances. (This applies to models that support task types, such as `gemini-embedding-001`.) +- **Dimensions**: a per-call `dimensions(...)` overrides the builder's `outputDimensionality`, for models that + support reducing the output size. +- **Multimodal** (`gemini-embedding-2`): natively embeds interleaved text + image into a single embedding. + Earlier models (e.g. `gemini-embedding-001`) are text-only. Images must be provided as base64 (`ImageContent`). +- **Listeners**: configure via `GoogleGenAiEmbeddingModel.builder().listeners(...)` to observe requests, + responses, and errors. + +```java +EmbeddingResponse response = embeddingModel.embed(EmbeddingRequest.builder() + .input("What is the capital of France?") + .inputType(EmbeddingInputType.QUERY) // embed as a query + .dimensions(256) // reduce output dimensionality + .build()); + +List embeddings = response.embeddings(); +``` + +Multimodal example (Gemini Embedding 2 — text and image fused into one embedding): + +```java +EmbeddingModel embeddingModel = GoogleGenAiEmbeddingModel.builder() + .apiKey(System.getenv("GOOGLE_AI_GEMINI_API_KEY")) + .modelName("gemini-embedding-2") + .build(); + +EmbeddingResponse response = embeddingModel.embed(EmbeddingRequest.builder() + .input(TextContent.from("a photo of a cat"), ImageContent.from(base64Image, "image/png")) + .build()); + +Embedding embedding = response.embeddings().get(0); +``` + +See [Embedding Model](/tutorials/rag#embedding-model) for the request/response API, and +[Observability](/tutorials/observability) for listeners. + +## Learn more + +For more details on the Gemini embedding models, see the +[documentation](https://ai.google.dev/gemini-api/docs/embeddings). diff --git a/docs/docs/integrations/embedding-models/google-vertex-ai.md b/docs/docs/integrations/embedding-models/google-vertex-ai.md new file mode 100644 index 0000000..0f0655d --- /dev/null +++ b/docs/docs/integrations/embedding-models/google-vertex-ai.md @@ -0,0 +1,131 @@ +--- +sidebar_position: 7 +--- + +# Google Vertex AI + +## Get started + +To get started follow the steps outlined in the `Get started` section of [Vertex AI Gemini integration tutorial](../language-models/google-vertex-ai-gemini) to create a +Google Cloud Platform account and establish a new project with access to Vertex AI API. + +## Add dependencies + +Add the following dependencies to your project's `pom.xml`: + +```xml + + dev.langchain4j + langchain4j-vertex-ai + 1.17.2-beta27 + +``` + +or project's `build.gradle`: + +```groovy +implementation 'dev.langchain4j:langchain4j-vertex-ai:1.17.2-beta27' +``` + +### Try out an example code: + +[An Example of using Vertex AI Embedding Model](https://github.com/langchain4j/langchain4j-examples/blob/main/other-examples/src/main/java/embedding/model/VertexAiEmbeddingModelExample.java) + +The `PROJECT_ID` field represents the variable you set when creating a new Google Cloud project. + +```java +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.output.Response; +import dev.langchain4j.model.vertexai.VertexAiEmbeddingModel; + +public class VertexAiEmbeddingModelExample { + + private static final String PROJECT_ID = "YOUR-PROJECT-ID"; + private static final String MODEL_NAME = "textembedding-gecko@latest"; + + public static void main(String[] args) { + + EmbeddingModel embeddingModel = VertexAiEmbeddingModel.builder() + .project(PROJECT_ID) + .location("us-central1") + .endpoint("us-central1-aiplatform.googleapis.com:443") + .publisher("google") + .modelName(MODEL_NAME) + .build(); + + Response response = embeddingModel.embed("Hello, how are you?"); + + Embedding embedding = response.content(); + + int dimension = embedding.dimension(); // 768 + float[] vector = embedding.vector(); // [-0.06050122, -0.046411075, ... + + System.out.println(dimension); + System.out.println(embedding.vectorAsList()); + } +} +``` + +### Available Embedding models + +|English models|Multilingual models| +|---|---| +|`textembedding-gecko@001`|`textembedding-gecko-multilingual@001`| +|`textembedding-gecko@003`|`text-multilingual-embedding-002`| +|`text-embedding-004`| | + +[List of supported languages for multi lingual model](https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-text-embeddings#language_coverage_for_textembedding-gecko-multilingual_models) + +Model names suffixed with `@latest` reference the most recent version of the model. + +By default, most embedding models output 768-dimensional vector embeddings (except for "Matryoshka" models that accept a configurable lower dimension). +The API accepts a maximum of 2,048 input tokens per segment to embed. +You can send upto 250 text segments. +The `VertexAiEmbeddingModel` class automatically and transparently splits the requests in batches when you ask for more than 250 segments to be embedded at the same time. +The embedding API is limited to a total of 20,000 tokens per call (across all segments). When that limit is reached, `VertexAiEmbeddingModel` will again batch the requests to avoid hitting that limit. + +### Configuring the embedding model + +```java +EmbeddingModel embeddingModel = VertexAiEmbeddingModel.builder() + .project(PROJECT_ID) + .location("us-central1") + .endpoint("us-central1-aiplatform.googleapis.com:443") // optional + .publisher("google") + .modelName(MODEL_NAME) + .maxRetries(2) // 2 by default + .maxSegmentsPerBatch(250) // up to 250 segments per batch + .maxTokensPerBatch(2048) // up to 2048 tokens per segment + .taskType() // see below for the different task types + .titleMetadataKey() // for the RETRIEVAL_DOCUMENT task, you can specify a title + // for the text segment to identify its document origin + .autoTruncate(false) // false by default: truncates segments longer than 2,048 input tokens + .outputDimensionality(512) // for models that support different output vector dimensions + .credentials(credentials) // custom Google Cloud credentials + .build(); +``` + +## Embedding task types + +Embedding models can be used for different use cases. +To get better embedding values, you can specify a _task_ among the following ones: + +* `RETRIEVAL_QUERY` +* `RETRIEVAL_DOCUMENT` +* `SEMANTIC_SIMILARITY` +* `CLASSIFICATION` +* `CLUSTERING` +* `QUESTION_ANSWERING` +* `FACT_VERIFICATION` +* `CODE_RETRIEVAL_QUERY` + +See the list of [supported models](https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/task-types). + +### References + +[Google Codelab on Vertex AI Embedding Model](https://codelabs.developers.google.com/codelabs/genai-chat-java-palm-langchain4j) + +[Available stable Embedding Models](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/text-embeddings#model_versions) + +[Latest Embedding Models version](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/model-versioning#palm-latest-models) diff --git a/docs/docs/integrations/embedding-models/hugging-face.md b/docs/docs/integrations/embedding-models/hugging-face.md new file mode 100644 index 0000000..ec39c8e --- /dev/null +++ b/docs/docs/integrations/embedding-models/hugging-face.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 8 +--- + +# Hugging Face + +- https://huggingface.co/docs/api-inference/index +- https://huggingface.co/docs/api-inference/detailed_parameters#feature-extraction-task + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-hugging-face + 1.17.2-beta27 + +``` + +## APIs + +- `HuggingFaceEmbeddingModel` + + +## Examples + +- [HuggingFaceEmbeddingModelExample](https://github.com/langchain4j/langchain4j-examples/blob/main/other-examples/src/main/java/embedding/model/HuggingFaceEmbeddingModelExample.java) diff --git a/docs/docs/integrations/embedding-models/jina.md b/docs/docs/integrations/embedding-models/jina.md new file mode 100644 index 0000000..c80234a --- /dev/null +++ b/docs/docs/integrations/embedding-models/jina.md @@ -0,0 +1,35 @@ +--- +sidebar_position: 9 +--- + +# Jina + +https://jina.ai/ + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-jina + 1.17.2-beta27 + +``` + +## APIs + +- `JinaEmbeddingModel` + +## Capabilities + +- **Multimodal** (`jina-clip-v2`, `jina-embeddings-v4` — auto-detected from the model name): embeds text and + images. Jina embeds one modality per input item (it does **not** fuse interleaved text + image); pass a single + `TextContent` or a single `ImageContent` (URL or base64) per input in an `EmbeddingRequest`. +- **Listeners**: configure via `JinaEmbeddingModel.builder().listeners(...)`. + +See [Embedding Model](/tutorials/rag#embedding-model) for the request/response API and multimodal usage. + +## Examples + +- [JinaEmbeddingModelIT](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-jina/src/test/java/dev/langchain4j/model/jina/JinaEmbeddingModelIT.java) diff --git a/docs/docs/integrations/embedding-models/jlama.md b/docs/docs/integrations/embedding-models/jlama.md new file mode 100644 index 0000000..96f119c --- /dev/null +++ b/docs/docs/integrations/embedding-models/jlama.md @@ -0,0 +1,114 @@ +--- +sidebar_position: 10 +--- + +# Jlama +[Jlama Project](https://github.com/tjake/Jlama) + +### Project setup + +To install langchain4j to your project, add the following dependency: + +For Maven project `pom.xml` + +```xml + + + dev.langchain4j + langchain4j + 1.17.2 + + + + dev.langchain4j + langchain4j-jlama + 1.17.2-beta27 + + + + com.github.tjake + jlama-native + + ${os.detected.name}-${os.detected.arch} + ${jlama.version} + +``` + +For Gradle project `build.gradle` + +```groovy +implementation 'dev.langchain4j:langchain4j:1.17.2' +implementation 'dev.langchain4j:langchain4j-jlama:1.17.2-beta27' +``` + +## Embedding +The Jlama Embeddings model allows you to embed sentences, and using it in your application is simple. +We provide a simple example to get you started with Jlama Embeddings model integration. +You can use any `bert` based model from [HuggingFace](https://huggingface.co/models?library=safetensors&sort=trending), and specify them using the `owner/model-name` format. + +Create a class and add the following code. + +```java +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.jlama.JlamaEmbeddingModel; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.store.embedding.EmbeddingMatch; +import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; + +import java.util.List; + +public class HelloWorld { + public static void main(String[] args) { + EmbeddingModel embeddingModel = JlamaEmbeddingModel + .modelName("intfloat/e5-small-v2") + .build(); + + // For simplicity, this example uses an in-memory store, but you can choose any external compatible store for production environments. + EmbeddingStore embeddingStore = new InMemoryEmbeddingStore<>(); + + TextSegment segment1 = TextSegment.from("I like football."); + Embedding embedding1 = embeddingModel.embed(segment1).content(); + embeddingStore.add(embedding1, segment1); + + TextSegment segment2 = TextSegment.from("The weather is good today."); + Embedding embedding2 = embeddingModel.embed(segment2).content(); + embeddingStore.add(embedding2, segment2); + + String userQuery = "What is your favourite sport?"; + Embedding queryEmbedding = embeddingModel.embed(userQuery).content(); + EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder() + .queryEmbedding(queryEmbedding) + .maxResults(1) + .build(); + EmbeddingSearchResult searchResult = embeddingStore.search(searchRequest); + EmbeddingMatch embeddingMatch = searchResult.matches().get(0); + + System.out.println("Question: " + userQuery); // What is your favourite sport? + System.out.println("Response: " + embeddingMatch.embedded().text()); // I like football. + } +} +``` +For this example, we'll add 2 text segments, but LangChain4j offers built-in support for loading documents from various sources: +File System, URL, Amazon S3, Azure Blob Storage, GitHub, Tencent COS. +Additionally, LangChain4j supports parsing multiple document types: +text, pdf, doc, xls, ppt. + +The output will be similar to this: + +```plaintext +Question: What is your favourite sport? +Response: I like football. +``` + +Of course, you can combine Jlama Embeddings with RAG (Retrieval-Augmented Generation) techniques. + +In [RAG](/tutorials/rag) you will learn how to use RAG techniques for ingestion, retrieval and Advanced Retrieval with LangChain4j. + +A lot of parameters are set behind the scenes, such as timeout, model type and model parameters. +In [Set Model Parameters](/tutorials/model-parameters) you will learn how to set these parameters explicitly. + +### More examples +If you want to check more examples, you can find them in the [langchain4j-examples](https://github.com/langchain4j/langchain4j-examples) project. diff --git a/docs/docs/integrations/embedding-models/local-ai.md b/docs/docs/integrations/embedding-models/local-ai.md new file mode 100644 index 0000000..f34ca1f --- /dev/null +++ b/docs/docs/integrations/embedding-models/local-ai.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 11 +--- + +# LocalAI + +https://localai.io/ + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-local-ai + 1.17.2-beta27 + +``` + +## APIs + +- `LocalAiEmbeddingModel` + + +## Examples + +- [LocalAiEmbeddingModelIT](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-local-ai/src/test/java/dev/langchain4j/model/localai/LocalAiEmbeddingModelIT.java) diff --git a/docs/docs/integrations/embedding-models/mistral-ai.md b/docs/docs/integrations/embedding-models/mistral-ai.md new file mode 100644 index 0000000..34bb46e --- /dev/null +++ b/docs/docs/integrations/embedding-models/mistral-ai.md @@ -0,0 +1,118 @@ +--- +sidebar_position: 12 +--- + +# Mistral AI +[MistralAI Documentation](https://docs.mistral.ai/) + +### Project setup + +To install langchain4j to your project, add the following dependency: + +For Maven project `pom.xml` + +```xml + + + dev.langchain4j + langchain4j + 1.17.2 + + + + dev.langchain4j + langchain4j-mistral-ai + 1.17.2 + +``` + +For Gradle project `build.gradle` + +```groovy +implementation 'dev.langchain4j:langchain4j:1.17.2' +implementation 'dev.langchain4j:langchain4j-mistral-ai:1.17.2' +``` +#### API Key setup +Add your MistralAI API key to your project, you can create a class ```ApiKeys.java``` with the following code + +```java +public class ApiKeys { + public static final String MISTRALAI_API_KEY = System.getenv("MISTRAL_AI_API_KEY"); +} +``` +Don't forget set your API key as an environment variable. +```shell +export MISTRAL_AI_API_KEY=your-api-key #For Unix OS based +SET MISTRAL_AI_API_KEY=your-api-key #For Windows OS +``` +More details on how to get your MistralAI API key can be found [here](https://docs.mistral.ai/#api-access) + +## Embedding +The MistralAI Embeddings model allows you to embed sentences, and using it in your application is simple. We provide a simple example to get you started with MistralAI Embeddings model integration. + +Create a class and add the following code. + +```java +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.mistralai.MistralAiEmbeddingModel; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.store.embedding.EmbeddingMatch; +import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; + +import java.util.List; + +public class HelloWorld { + public static void main(String[] args) { + EmbeddingModel embeddingModel = MistralAiEmbeddingModel.builder() + .apiKey(System.getenv("MISTRAL_AI_API_KEY")) + .modelName("mistral-embed") + .build(); + + // For simplicity, this example uses an in-memory store, but you can choose any external compatible store for production environments. + EmbeddingStore embeddingStore = new InMemoryEmbeddingStore<>(); + + TextSegment segment1 = TextSegment.from("I like football."); + Embedding embedding1 = embeddingModel.embed(segment1).content(); + embeddingStore.add(embedding1, segment1); + + TextSegment segment2 = TextSegment.from("The weather is good today."); + Embedding embedding2 = embeddingModel.embed(segment2).content(); + embeddingStore.add(embedding2, segment2); + + String userQuery = "What is your favourite sport?"; + Embedding queryEmbedding = embeddingModel.embed(userQuery).content(); + EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder() + .queryEmbedding(queryEmbedding) + .maxResults(1) + .build(); + EmbeddingSearchResult searchResult = embeddingStore.search(searchRequest); + EmbeddingMatch embeddingMatch = searchResult.matches().get(0); + + System.out.println("Question: " + userQuery); // What is your favourite sport? + System.out.println("Response: " + embeddingMatch.embedded().text()); // I like football. + } +} +``` +For this example, we'll add 2 text segments, but LangChain4j offers built-in support for loading documents from various sources: +File System, URL, Amazon S3, Azure Blob Storage, GitHub, Tencent COS. +Additionally, LangChain4j supports parsing multiple document types: +text, pdf, doc, xls, ppt. + +The output will be similar to this: + +```plaintext +Question: What is your favourite sport? +Response: I like football. +``` + +Of course, you can combine MistralAI Embeddings with RAG (Retrieval-Augmented Generation) techniques. + +In [RAG](/tutorials/rag) you will learn how to use RAG techniques for ingestion, retrieval and Advanced Retrieval with LangChain4j. + +A lot of parameters are set behind the scenes, such as timeout, model type and model parameters. +In [Set Model Parameters](/tutorials/model-parameters) you will learn how to set these parameters explicitly. + +### More examples +If you want to check more examples, you can find them in the [langchain4j-examples](https://github.com/langchain4j/langchain4j-examples) project. diff --git a/docs/docs/integrations/embedding-models/nomic.md b/docs/docs/integrations/embedding-models/nomic.md new file mode 100644 index 0000000..afa1817 --- /dev/null +++ b/docs/docs/integrations/embedding-models/nomic.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 13 +--- + +# Nomic + +https://home.nomic.ai/ + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-nomic + 1.17.2-beta27 + +``` + +## APIs + +- `NomicEmbeddingModel` + + +## Examples + +- [NomicEmbeddingModelIT](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-nomic/src/test/java/dev/langchain4j/model/nomic/NomicEmbeddingModelIT.java) diff --git a/docs/docs/integrations/embedding-models/ollama.md b/docs/docs/integrations/embedding-models/ollama.md new file mode 100644 index 0000000..5b756ae --- /dev/null +++ b/docs/docs/integrations/embedding-models/ollama.md @@ -0,0 +1,59 @@ +--- +sidebar_position: 14 +--- + +# Ollama + +https://ollama.com/ + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-ollama + 1.17.2 + +``` + +## APIs + +- `OllamaEmbeddingModel` + +## Usage + +```java +EmbeddingModel embeddingModel = OllamaEmbeddingModel.builder() + .baseUrl("http://localhost:11434") + .modelName("all-minilm") + .build(); + +Response response = embeddingModel.embed("Hello, world!"); +Embedding embedding = response.content(); +``` + +## Request/response API and observability + +`OllamaEmbeddingModel` supports `embed(EmbeddingRequest)` in addition to the convenience methods. Ollama's +embedding API is text-only, so a request that sets an input type or an image input fails fast with +`UnsupportedFeatureException`. Token usage (`prompt_eval_count`) is reported in the response metadata. + +Ollama's optional output `dimensions` is model-dependent (only some models support reducing the output size), +so it is configured on the builder via `.dimensions(...)` rather than as a per-call parameter. + +Attach [listeners](/tutorials/observability#embeddingmodel-observability) to observe requests, responses, and +errors: + +```java +EmbeddingModel embeddingModel = OllamaEmbeddingModel.builder() + .baseUrl("http://localhost:11434") + .modelName("all-minilm") + .listeners(List.of(myEmbeddingModelListener)) + .build(); +``` + + +## Examples + +- [OllamaEmbeddingModelIT](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-ollama/src/test/java/dev/langchain4j/model/ollama/OllamaEmbeddingModelIT.java) diff --git a/docs/docs/integrations/embedding-models/open-ai-official.md b/docs/docs/integrations/embedding-models/open-ai-official.md new file mode 100644 index 0000000..ac72eb0 --- /dev/null +++ b/docs/docs/integrations/embedding-models/open-ai-official.md @@ -0,0 +1,101 @@ +--- +sidebar_position: 16 +--- + +# OpenAI Official SDK + +:::note + +This is the documentation for the `OpenAI Official SDK` integration, that uses the [official OpenAI Java SDK](https://github.com/openai/openai-java). + +LangChain4j provides 3 different integrations with OpenAI for using embedding models, and this is #2 : + +- [OpenAI](/integrations/language-models/open-ai) uses a custom Java implementation of the OpenAI REST API, that works best with Quarkus (as it uses the Quarkus REST client) and Spring (as it uses Spring's RestClient). +- [OpenAI Official SDK](/integrations/language-models/open-ai-official) uses the official OpenAI Java SDK. +- [Azure OpenAI](/integrations/language-models/azure-open-ai) uses the Azure SDK from Microsoft, and works best if you are using the Microsoft Java stack, including advanced Azure authentication mechanisms. + +::: + +## Use cases for this integration + +This integration uses the [OpenAI Java SDK GitHub Repository](https://github.com/openai/openai-java), and will work for all OpenAI models which can be provided by: + +- OpenAI +- Azure OpenAI +- GitHub Models + +It will also work with models supporting the OpenAI API. + +## OpenAI Documentation + +- [OpenAI Java SDK GitHub Repository](https://github.com/openai/openai-java) +- [OpenAI API Documentation](https://platform.openai.com/docs/introduction) +- [OpenAI API Reference](https://platform.openai.com/docs/api-reference) + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-open-ai-official + 1.17.2-beta27 + +``` + +## Configuring the models + +To use OpenAI models, you usually need an endpoint URL, an API key, and a model name. This depends on where the model is hosted, and this integration tries +to make it easier with some auto-configuration: + +### Generic configuration + +```java +import com.openai.models.embeddings.EmbeddingModel; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.openaiofficial.OpenAiOfficialEmbeddingModel; + +import static com.openai.models.embeddings.EmbeddingModel.TEXT_EMBEDDING_3_SMALL; + +// .... + +EmbeddingModel model = OpenAiOfficialEmbeddingModel.builder() + .baseUrl(System.getenv("AZURE_OPENAI_ENDPOINT")) + .apiKey(System.getenv("AZURE_OPENAI_KEY")) + .modelName(TEXT_EMBEDDING_3_SMALL) + .build(); +``` + +### Specific configurations for Azure OpenAI and GitHub Models. + +Similar to configuring the [OpenAI Official Chat Model](/integrations/language-models/open-ai-official), you can configure the `OpenAiOfficialEmbeddingModel` with +Azure OpenAI and GitHub Models, using the `isAzure()` and `isGitHubModels()` methods. + +#### Azure OpenAI + +```java +EmbeddingModel model = OpenAiOfficialEmbeddingModel.builder() + .baseUrl(System.getenv("AZURE_OPENAI_ENDPOINT")) + .apiKey(System.getenv("AZURE_OPENAI_KEY")) + .modelName(TEXT_EMBEDDING_3_SMALL) + .isAzure(true) // Not necessary if the base URL ends with `openai.azure.com` + .build(); +``` + +You can also use "passwordless" authentication, as described in the [OpenAI Official Chat Model](/integrations/language-models/open-ai-official) documentation. + +#### GitHub Models + +```java +EmbeddingModel model = OpenAiOfficialEmbeddingModel.builder() + .modelName(TEXT_EMBEDDING_3_SMALL) + .isGitHubModels(true) + .build(); +``` + +## Using the models + +Once the model is configured, you can use it to create embeddings: + +```java +Response response = model.embed("Please embed this sentence."); +``` diff --git a/docs/docs/integrations/embedding-models/open-ai.md b/docs/docs/integrations/embedding-models/open-ai.md new file mode 100644 index 0000000..cb116a2 --- /dev/null +++ b/docs/docs/integrations/embedding-models/open-ai.md @@ -0,0 +1,98 @@ +--- +sidebar_position: 15 +--- + +# OpenAI + +:::note + +This is the documentation for the `OpenAI` integration, that uses a custom Java implementation of the OpenAI REST API, that works best with Quarkus (as it uses the Quarkus REST client) and Spring (as it uses Spring's RestClient). + +LangChain4j provides 3 different integrations with OpenAI for using embedding models, and this is #1 : + +- [OpenAI](/integrations/language-models/open-ai) uses a custom Java implementation of the OpenAI REST API, that works best with Quarkus (as it uses the Quarkus REST client) and Spring (as it uses Spring's RestClient). +- [OpenAI Official SDK](/integrations/language-models/open-ai-official) uses the official OpenAI Java SDK. +- [Azure OpenAI](/integrations/language-models/azure-open-ai) uses the Azure SDK from Microsoft, and works best if you are using the Microsoft Java stack, including advanced Azure authentication mechanisms. + +::: + +- https://platform.openai.com/docs/guides/embeddings +- https://platform.openai.com/docs/api-reference/embeddings + +## Maven Dependency + +### Plain Java +```xml + + dev.langchain4j + langchain4j-open-ai + 1.17.2 + +``` + +### Spring Boot +```xml + + dev.langchain4j + langchain4j-open-ai-spring-boot-starter + 1.17.2-beta27 + +``` + +## Creating `OpenAiEmbeddingModel` + +### Plain Java +```java +EmbeddingModel model = OpenAiEmbeddingModel.builder() + .apiKey(System.getenv("OPENAI_API_KEY")) + .modelName("text-embedding-3-small") + .build(); +``` + +### Spring Boot +Add to the `application.properties`: +```properties +# Mandatory properties: +langchain4j.open-ai.embedding-model.api-key=${OPENAI_API_KEY} +langchain4j.open-ai.embedding-model.model-name=text-embedding-3-small + +# Optional properties: +langchain4j.open-ai.embedding-model.base-url=... +langchain4j.open-ai.embedding-model.custom-headers=... +langchain4j.open-ai.embedding-model.dimensions=... +langchain4j.open-ai.embedding-model.log-requests=... +langchain4j.open-ai.embedding-model.log-responses=... +langchain4j.open-ai.embedding-model.max-retries=... +langchain4j.open-ai.embedding-model.organization-id=... +langchain4j.open-ai.embedding-model.project-id=... +langchain4j.open-ai.embedding-model.timeout=... +langchain4j.open-ai.embedding-model.user=... +``` + +## Setting custom embedding request parameters + +When using `OpenAiEmbeddingModel`, you can configure custom parameters for the embedding request +within the HTTP request's JSON body. This is useful for OpenAI-compatible providers that require +provider-specific embedding parameters: +```java +EmbeddingModel model = OpenAiEmbeddingModel.builder() + .baseUrl("https://integrate.api.nvidia.com/v1") + .apiKey(System.getenv("NVIDIA_API_KEY")) + .modelName("nvidia/nv-embedqa-e5-v5") + .customParameters(Map.of("input_type", "passage")) + .build(); +``` + +## Capabilities + +- **Per-call parameters** (via `EmbeddingRequest`): `dimensions` — reduce output dimensionality on + `text-embedding-3` models. OpenAI-specific `user`, `encoding_format` and arbitrary passthrough parameters are + available via `OpenAiEmbeddingRequestParameters` (used, for example, for NVIDIA's `input_type`). +- Text-only (no image inputs). +- **Listeners**: configure via `OpenAiEmbeddingModel.builder().listeners(...)`. + +See [Embedding Model](/tutorials/rag#embedding-model) for the request/response API. + +## Examples + +- [OpenAiEmbeddingModelExamples](https://github.com/langchain4j/langchain4j-examples/blob/main/open-ai-examples/src/main/java/OpenAiEmbeddingModelExamples.java) diff --git a/docs/docs/integrations/embedding-models/ovh-ai.md b/docs/docs/integrations/embedding-models/ovh-ai.md new file mode 100644 index 0000000..4650332 --- /dev/null +++ b/docs/docs/integrations/embedding-models/ovh-ai.md @@ -0,0 +1,111 @@ +--- +sidebar_position: 17 +--- + +# OVHcloud AI Endpoints + +- [OVHclous AI Endpoints Documentation](https://www.ovhcloud.com/en/public-cloud/ai-endpoints/) +- [OVHcloud AI Endpoints API Reference](https://www.ovhcloud.com/en/public-cloud/ai-endpoints/catalog/) + +## Project setup + +OVHcloud AI Endpoints models are compatible with the OpenAI API, so you can use the same code as for OpenAI models, just changing the base URL and the API key. + +### Maven Dependency + +```xml + + dev.langchain4j + langchain4j-open-ai + 1.13.0 + +``` + +### API Key setup +Add your OVHcloud AI API key to your project. + +```java +public static final String OVHAI_AI_API_KEY = System.getenv("OVHAI_AI_API_KEY"); +``` +Don't forget set your API key as an environment variable. +```shell +export OVHAI_AI_API_KEY=your-api-key #For Unix OS based +SET OVHAI_AI_API_KEY=your-api-key #For Windows OS +``` +More details on how to get your OVHcloud AI API key can be found [here](https://help.ovhcloud.com/csm/en-public-cloud-ai-endpoints-getting-started?id=kb_article_view&sysparm_article=KB0065403) + +## Embedding +The OVHcloud AI Embeddings model allows you to embed sentences, and using it in your application is simple. We provide a simple example to get you started with OVHcloud AI Embeddings model integration. + +Create a class and add the following code. + +```java +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.ovhai.OvhAiEmbeddingModel; +import dev.langchain4j.store.embedding.EmbeddingMatch; +import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; + +import java.util.List; + +public class OvhAiEmbeddingSimpleExample { + + public static void main(String[] args) { + EmbeddingModel model = OpenAiEmbeddingModel.builder() + .baseUrl("https://oai.endpoints.kepler.ai.cloud.ovh.net/v1") + .apiKey(System.getenv("OVHAI_AI_API_KEY")) + .modelName("Qwen3-Embedding-8B") + .logRequests(true) + .logResponses(false) // embeddings are huge in logs + .build(); + + // For simplicity, this example uses an in-memory store, but you can choose any external compatible store for production environments. + EmbeddingStore embeddingStore = new InMemoryEmbeddingStore<>(); + + TextSegment segment1 = TextSegment.from("I like football."); + Embedding embedding1 = embeddingModel.embed(segment1).content(); + embeddingStore.add(embedding1, segment1); + + TextSegment segment2 = TextSegment.from("The weather is good today."); + Embedding embedding2 = embeddingModel.embed(segment2).content(); + embeddingStore.add(embedding2, segment2); + + String userQuery = "What is your favourite sport?"; + Embedding queryEmbedding = embeddingModel.embed(userQuery).content(); + EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder() + .queryEmbedding(queryEmbedding) + .maxResults(1) + .build(); + EmbeddingSearchResult searchResult = embeddingStore.search(searchRequest); + EmbeddingMatch embeddingMatch = searchResult.matches().get(0); + + System.out.println("Question: " + userQuery); // What is your favourite sport? + System.out.println("Response: " + embeddingMatch.embedded().text()); // I like football. + } + +} +``` + +For this example, we'll add 2 text segments, but LangChain4j offers built-in support for loading documents from various sources: +File System, URL, Amazon S3, Azure Blob Storage, GitHub, Tencent COS. +Additionally, LangChain4j supports parsing multiple document types: +text, pdf, doc, xls, ppt. + +The output will be similar to this: + +```plaintext +Question: What is your favourite sport? +Response: I like football. +``` + +Of course, you can combine OVHCloud Embeddings with RAG (Retrieval-Augmented Generation) techniques. + +In [RAG](/tutorials/rag) you will learn how to use RAG techniques for ingestion, retrieval and Advanced Retrieval with LangChain4j. + +A lot of parameters are set behind the scenes, such as timeout, model type and model parameters. +In [Set Model Parameters](/tutorials/model-parameters) you will learn how to set these parameters explicitly. + +### More examples +If you want to check more examples, you can find them in the [langchain4j-examples](https://github.com/langchain4j/langchain4j-examples) project. diff --git a/docs/docs/integrations/embedding-models/qianfan.md b/docs/docs/integrations/embedding-models/qianfan.md new file mode 100644 index 0000000..1a7518c --- /dev/null +++ b/docs/docs/integrations/embedding-models/qianfan.md @@ -0,0 +1,91 @@ +--- +sidebar_position: 18 +--- + +# Qianfan + +[百度智能云千帆大模型](https://console.bce.baidu.com/qianfan/ais/console/applicationConsole/application) +![image](https://github.com/langchain4j/langchain4j/assets/95265298/600f8006-4484-4a75-829c-c8c16a3130c2) + +## Maven Dependency + +You can use DashScope with LangChain4j in plain Java or Spring Boot applications. + +### Plain Java + +:::note +Since `1.0.0-alpha1`, `langchain4j-qianfan` has migrated to `langchain4j-community` and is renamed to `langchain4j-community-qianfan`. +::: + +Before `1.0.0-alpha1`: + +```xml + + dev.langchain4j + langchain4j-qianfan + ${previous version here} + +``` + +`1.0.0-alpha1` and later: + +```xml + + dev.langchain4j + langchain4j-community-qianfan + ${latest version here} + +``` + +### Spring Boot + +:::note +Since `1.0.0-alpha1`, `langchain4j-qianfan-spring-boot-starter` has migrated to `langchain4j-community` and is renamed +to `langchain4j-community-qianfan-spring-boot-starter`. +::: + +Before `1.0.0-alpha1`: + +```xml + + + dev.langchain4j + langchain4j-qianfan-spring-boot-starter + ${previous version here} + +``` + +`1.0.0-alpha1` and later: + +```xml + + + dev.langchain4j + langchain4j-community-qianfan-spring-boot-starter + ${latest version here} + +``` + +Or, you can use BOM to manage dependencies consistently: + +```xml + + + + dev.langchain4j + langchain4j-community-bom + ${latest version here} + pom + import + + +``` + +## APIs + +- `QianfanEmbeddingModel` + + +## Examples + +- [QianfanEmbeddingModelIT](https://github.com/langchain4j/langchain4j-community/blob/main/models/langchain4j-community-qianfan/src/test/java/dev/langchain4j/community/model/qianfan/QianfanEmbeddingModelIT.java) diff --git a/docs/docs/integrations/embedding-models/voyage-ai.md b/docs/docs/integrations/embedding-models/voyage-ai.md new file mode 100644 index 0000000..e7a7576 --- /dev/null +++ b/docs/docs/integrations/embedding-models/voyage-ai.md @@ -0,0 +1,34 @@ +--- +sidebar_position: 19 +--- + +# Voyage AI + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-voyage-ai + 1.17.2-beta27 + +``` + +## APIs + +- `VoyageAiEmbeddingModel` + +## Capabilities + +- **Multimodal** (`voyage-multimodal-3`, `voyage-multimodal-3.5` — auto-detected from the model name): embeds + text and images into a shared vector space; interleaved text + image is fused into a single embedding. + Provide image inputs as `ImageContent` (URL or base64) in an `EmbeddingRequest`. +- **Per-call parameters**: `input_type` (`EmbeddingInputType.QUERY` / `DOCUMENT`). +- **Listeners**: configure via `VoyageAiEmbeddingModel.builder().listeners(...)`. + +See [Embedding Model](/tutorials/rag#embedding-model) for the request/response API and multimodal usage. + +## Examples + +- [VoyageAiEmbeddingModelIT](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-voyage-ai/src/test/java/dev/langchain4j/model/voyageai/VoyageAiEmbeddingModelIT.java) +- [VoyageAiEmbeddingModelExample](https://github.com/langchain4j/langchain4j-examples/blob/main/voyage-ai-examples/src/main/java/VoyageAiEmbeddingModelExample.java) diff --git a/docs/docs/integrations/embedding-models/watsonx.md b/docs/docs/integrations/embedding-models/watsonx.md new file mode 100644 index 0000000..1791b77 --- /dev/null +++ b/docs/docs/integrations/embedding-models/watsonx.md @@ -0,0 +1,137 @@ +--- +sidebar_position: 23 +--- + +# watsonx.ai + +- [watsonx.ai API Reference](https://cloud.ibm.com/apidocs/watsonx-ai#text-embeddings) +- [watsonx.ai Java SDK](https://github.com/IBM/watsonx-ai-java-sdk) + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-watsonx + 1.17.2-beta27 + +``` + +## Authentication + +Watsonx.ai supports authentication via the `Authenticator` interface. + +This allows you to use different authentication mechanisms depending on your deployment: + +- **IBMCloudAuthenticator** – authenticates with **IBM Cloud** using an API key. This is the simplest approach and is used when you provide the `apiKey(...)` builder method. +- **CP4DAuthenticator** – authenticates with **Cloud Pak for Data** deployments. +- **Custom authenticators** – any implementation of the `Authenticator` interface can be used. + +The `WatsonxEmbeddingModel` and other service builders accept either a shortcut via `.apiKey(...)` or a full `Authenticator` instance via `.authenticator(...)`. + +### Example +```java +WatsonxEmbeddingModel.builder() + .baseUrl(CloudRegion.FRANKFURT) + .apiKey("your-api-key") // Simple IBM Cloud authentication + .projectId("your-project-id") + .modelName("ibm/granite-embedding-278m-multilingual") + .build(); + +WatsonxEmbeddingModel.builder() + .baseUrl("https://my-instance-url") + .authenticator( // For Cloud Pak for Data deployments + CP4DAuthenticator.builder() + .baseUrl("https://my-instance-url") + .username("username") + .apiKey("api-key") + .build() + ) + .projectId("my-project-id") + .modelName("ibm/granite-embedding-278m-multilingual") + .build(); +``` + +### Custom HttpClient and SSL Configuration + +#### Using a custom HttpClient + +All services and authenticators support a custom `HttpClient` instance through the builder pattern. This is particularly useful for Cloud Pak for Data environments where you may need to configure custom TLS/SSL settings, proxy configuration, or other HTTP client properties. + +```java +HttpClient httpClient = HttpClient.newBuilder() + .sslContext(createCustomSSLContext()) + .executor(ExecutorProvider.ioExecutor()) + .build(); + +EmbeddingModel embeddingModel = WatsonxEmbeddingModel.builder() + .baseUrl("https://my-instance-url") + .modelName("ibm/granite-embedding-278m-multilingual") + .projectId("project-id") + .httpClient(httpClient) // Custom HttpClient + .authenticator( + CP4DAuthenticator.builder() + .baseUrl("https://my-instance-url") + .username("username") + .apiKey("api-key") + .httpClient(httpClient) // Custom HttpClient + .build() + ) + .build(); +``` + +> **Note:** When using a custom `HttpClient` with Cloud Pak for Data, make sure to set it on both the service builder and the authenticator builder to ensure consistent HTTP behavior across all requests. + +#### Disabling SSL verification + +If you only need to disable SSL certificate verification, you can use the `verifySsl(false)` option instead of providing a custom `HttpClient`: + +```java +EmbeddingModel embeddingModel = WatsonxEmbeddingModel.builder() + .baseUrl("https://my-instance-url") + .modelName("ibm/granite-embedding-278m-multilingual") + .projectId("project-id") + .verifySsl(false) // Disable SSL verification + .authenticator( + CP4DAuthenticator.builder() + .baseUrl("https://my-instance-url") + .username("username") + .apiKey("api-key") + .verifySsl(false) // Disable SSL verification + .build() + ) + .build(); +``` + +### How to create an IBM Cloud API Key + +You can create an API key at [https://cloud.ibm.com/iam/apikeys](https://cloud.ibm.com/iam/apikeys) by clicking **Create +**. + +### How to find your Project ID + +1. Visit [https://dataplatform.cloud.ibm.com/projects/?context=wx](https://dataplatform.cloud.ibm.com/projects/?context=wx) +2. Open your project +3. Go to the **Manage** tab +4. Copy the **Project ID** from the **Details** section + +## WatsonxEmbeddingModel + +The `WatsonxEmbeddingModel` enables you to generate embeddings using IBM watsonx.ai and integrate them with LangChain4j's vector-based operations such as search, retrieval-augmented generation (RAG), and similarity comparison. + +It implements the LangChain4j `EmbeddingModel` interface. + +```java +EmbeddingModel embeddingModel = WatsonxEmbeddingModel.builder() + .baseUrl(CloudRegion.FRANKFURT) + .apiKey("your-api-key") + .projectId("your-project-id") + .modelName("ibm/granite-embedding-278m-multilingual") + .build(); + +System.out.println(embeddingModel.embed("Hello from watsonx.ai")); +``` +> 🔗 [View available embedding model IDs](https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/fm-models-embed.html?context=wx&audience=wdp#embed) + +## Examples + +- [WatsonxEmbeddingModelTest](https://github.com/langchain4j/langchain4j-examples/blob/main/watsonx-ai-examples/src/main/java/WatsonxEmbeddingModelTest.java) \ No newline at end of file diff --git a/docs/docs/integrations/embedding-models/workers-ai.md b/docs/docs/integrations/embedding-models/workers-ai.md new file mode 100644 index 0000000..9f1efde --- /dev/null +++ b/docs/docs/integrations/embedding-models/workers-ai.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 20 +--- + +# Cloudflare Workers AI + +https://developers.cloudflare.com/workers-ai/ + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-workers-ai + 1.17.2-beta27 + +``` + +## APIs + +- `WorkersAiEmbeddingModel` + + +## Examples + +- [WorkersAiEmbeddingModelIT](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-workers-ai/src/test/java/dev/langchain4j/model/workersai/WorkersAiEmbeddingModelIT.java) diff --git a/docs/docs/integrations/embedding-models/xinference.md b/docs/docs/integrations/embedding-models/xinference.md new file mode 100644 index 0000000..fd5c342 --- /dev/null +++ b/docs/docs/integrations/embedding-models/xinference.md @@ -0,0 +1,43 @@ +--- +sidebar_position: 22 +--- + +# Xinference + +- https://inference.readthedocs.io/ + + +## Maven Dependency + +`1.0.0-alpha1` and later: + +```xml + + dev.langchain4j + langchain4j-community-xinference + ${latest version here} + +``` + +Or, you can use BOM to manage dependencies consistently: + +```xml + + + dev.langchain4j + langchain4j-community-bom + ${latest version here} + pom + import + + +``` + +## APIs + +- `XinferenceEmbeddingModel` + + +## Examples + +- [XinferenceEmbeddingModelIT](https://github.com/langchain4j/langchain4j-community/blob/main/models/langchain4j-community-xinference/src/test/java/dev/langchain4j/community/model/xinference/XinferenceEmbeddingModelIT.java) diff --git a/docs/docs/integrations/embedding-models/zhipu-ai.md b/docs/docs/integrations/embedding-models/zhipu-ai.md new file mode 100644 index 0000000..3d7e432 --- /dev/null +++ b/docs/docs/integrations/embedding-models/zhipu-ai.md @@ -0,0 +1,67 @@ +--- +sidebar_position: 21 +--- + +# Zhipu AI + +[ZhiPu AI](https://www.zhipuai.cn/) is a platform to provide model service including text generation, text embedding, +image generation and so on. You can refer to [ZhiPu AI Open Platform](https://open.bigmodel.cn/) for more details. +LangChain4j integrates with ZhiPu AI by using [HTTP endpoint](https://bigmodel.cn/dev/api/normal-model/glm-4). We are +consider migrating it from HTTP endpoint to official SDK and are appreciated of any help! + +## Maven Dependency + +You can use ZhiPu AI with LangChain4j in plain Java or Spring Boot applications. + +### Plain Java + +:::note +Since `1.0.0-alpha1`, `langchain4j-zhipu-ai` has migrated to `langchain4j-community` and is renamed to +`langchain4j-community-zhipu-ai` +::: + +Before `1.0.0-alpha1`: + +```xml + + + dev.langchain4j + langchain4j-zhipu-ai + ${previous version here} + +``` + +`1.0.0-alpha1` and later: + +```xml + + + dev.langchain4j + langchain4j-community-zhipu-ai + ${latest version here} + +``` + +Or, you can use BOM to manage dependencies consistently: + +```xml + + + + dev.langchain4j + langchain4j-community-bom + ${latest version here} + pom + import + + +``` + +## APIs + +- `ZhipuAiEmbeddingModel` + + +## Examples + +- [ZhipuAiEmbeddingModelIT](https://github.com/langchain4j/langchain4j-community/blob/main/models/langchain4j-community-zhipu-ai/src/test/java/dev/langchain4j/community/model/zhipu/ZhipuAiEmbeddingModelIT.java) diff --git a/docs/docs/integrations/embedding-stores/1-in-memory.md b/docs/docs/integrations/embedding-stores/1-in-memory.md new file mode 100644 index 0000000..c809288 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/1-in-memory.md @@ -0,0 +1,46 @@ +--- +sidebar_position: 1 +--- + +# In-memory + +LangChain4j provides a simple in-memory implementation of an `EmbeddingStore` interface: +`InMemoryEmbeddingStore`. +It is useful for fast prototyping and simple use cases. +It keeps `Embedding`s and associated `TextSegment`s in memory. +Search is also performed in memory. +It can also be persisted and restored to/from a JSON string or a file. + +### Maven Dependency + +```xml + + dev.langchain4j + langchain4j + 1.17.2 + +``` + +## APIs + +- `InMemoryEmbeddingStore` + + +## Persisting + +`InMemoryEmbeddingStore` can be serialized to a json string or a file: +```java +InMemoryEmbeddingStore embeddingStore = new InMemoryEmbeddingStore<>(); +embeddingStore.addAll(embeddings, embedded); + +String serializedStore = embeddingStore.serializeToJson(); +InMemoryEmbeddingStore deserializedStore = InMemoryEmbeddingStore.fromJson(serializedStore); + +String filePath = "/home/me/store.json"; +embeddingStore.serializeToFile(filePath); +InMemoryEmbeddingStore deserializedStore = InMemoryEmbeddingStore.fromFile(filePath); +``` + +## Examples + +- [InMemoryEmbeddingStoreExample](https://github.com/langchain4j/langchain4j-examples/blob/main/other-examples/src/main/java/embedding/store/InMemoryEmbeddingStoreExample.java) diff --git a/docs/docs/integrations/embedding-stores/_category_.json b/docs/docs/integrations/embedding-stores/_category_.json new file mode 100644 index 0000000..daaa76f --- /dev/null +++ b/docs/docs/integrations/embedding-stores/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Embedding Stores", + "position": 3, + "link": { + "type": "generated-index", + "description": "Embedding Stores" + } +} diff --git a/docs/docs/integrations/embedding-stores/alloydb.md b/docs/docs/integrations/embedding-stores/alloydb.md new file mode 100644 index 0000000..925531a --- /dev/null +++ b/docs/docs/integrations/embedding-stores/alloydb.md @@ -0,0 +1,86 @@ +# Google AlloyDB for PostgreSQL + +[AlloyDB](https://cloud.google.com/alloydb) is a fully managed relational database service that offers high performance, seamless integration, and impressive scalability. AlloyDB is 100% compatible with PostgreSQL. Extend your database application to build AI-powered experiences leveraging AlloyDB's Langchain integrations. + +This module implements `EmbeddingStore` backed by an AlloyDB for PostgreSQL database. + +### Maven Dependency + +```xml + + dev.langchain4j + langchain4j-community-alloydb-pg + ${latest version here} + +``` + +## AlloyDBEmbeddingStore Usage + +Use a vector store to store text embedded data and perform vector search. Instances of `AlloyDBEmbeddingStore` can be created by configuring provided `Builder`, it requires the following: + +- `AlloyDBEngine` instance +- table name +- schema name (optional, default: "public") +- content column (optional, default: "content") +- embedding column (optional, default: "embedding") +- id column (optional, default: "langchain_id") +- metadata column names (optional) +- additional metadata json column (optional, default: "langchain_metadata") +- ignored metadata column names (optional) +- distance strategy (optional, default:DistanceStrategy.COSINE_DISTANCE) +- query options (optional) + +example usage: +```java +import dev.langchain4j.data.document.Metadata; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.store.embedding.EmbeddingMatch; +import dev.langchain4j.store.embedding.EmbeddingSearchRequest; +import dev.langchain4j.store.embedding.EmbeddingSearchResult; +import dev.langchain4j.engine.EmbeddingStoreConfig; +import dev.langchain4j.engine.AlloyDBEngine; +import dev.langchain4j.engine.MetadataColumn; +import dev.langchain4j.store.embedding.alloydb.AlloyDBEmbeddingStore; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +AlloyDBEngine engine = new AlloyDBEngine.Builder() + .projectId("") + .region("") + .cluster("") + .instance("") + .database("") + .build(); + +AlloyDBEmbeddingStore store = new AlloyDBEmbeddingStore.Builder(engine, TABLE_NAME) + .build(); + +List testTexts = Arrays.asList("cat", "dog", "car", "truck"); +List embeddings = new ArrayList<>(); +List textSegments = new ArrayList<>(); +EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel(); + +for (String text : testTexts) { + Map metaMap = new HashMap<>(); + metaMap.put("my_metadata", "string"); + Metadata metadata = new Metadata(metaMap); + textSegments.add(new TextSegment(text, metadata)); + embeddings.add(MyEmbeddingModel.embed(text).content()); +} +List ids = store.addAll(embeddings, textSegments); +// search for "cat" +EmbeddingSearchRequest request = EmbeddingSearchRequest.builder() + .queryEmbedding(embeddings.get(0)) + .maxResults(10) + .minScore(0.9) + .build(); +List> result = store.search(request).matches(); +// remove "cat" +store.removeAll(singletonList(result.get(0).embeddingId())); +``` diff --git a/docs/docs/integrations/embedding-stores/amazon-s3-vectors.md b/docs/docs/integrations/embedding-stores/amazon-s3-vectors.md new file mode 100644 index 0000000..46ad5c0 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/amazon-s3-vectors.md @@ -0,0 +1,132 @@ +--- +sidebar_position: 29 +--- + +# Amazon S3 Vectors + +The Amazon S3 Vectors Embedding Store integrates with [Amazon S3 Vectors](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors.html), a purpose-built vector storage capability within Amazon S3 designed for storing and querying vector embeddings at scale. + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-community-s3-vectors + ${latest version here} + +``` + +## APIs + +- `S3VectorsEmbeddingStore` + +## Features + +- Store embeddings with metadata +- Vector similarity search with cosine or euclidean distance +- Filter search results by metadata fields +- Automatic index creation on first embedding insertion +- Standard AWS credential provider support + +## Usage + +### Basic Configuration + +```java +S3VectorsEmbeddingStore embeddingStore = S3VectorsEmbeddingStore.builder() + .vectorBucketName("my-vector-bucket") // S3 Vectors bucket name (required) + .indexName("my-index") // Index name within the bucket (required) + .region("us-west-2") // AWS region (default: us-east-1) + .distanceMetric(DistanceMetric.COSINE) // Distance metric (default: COSINE) + .createIndexIfNotExists(true) // Auto-create index (default: true) + .timeout(Duration.ofSeconds(60)) // API call timeout (default: 30 seconds) + .credentialsProvider(myCredentialsProvider) // Custom AWS credentials + .build(); +``` + +### Using an Existing S3VectorsClient + +If you already have a configured S3VectorsClient, you can pass it directly to the builder: + +```java +S3VectorsClient customClient = S3VectorsClient.builder() + .region(Region.US_WEST_2) + .credentialsProvider(myCredentialsProvider) + .build(); + +S3VectorsEmbeddingStore embeddingStore = S3VectorsEmbeddingStore.builder() + .s3VectorsClient(customClient) + .vectorBucketName("my-vector-bucket") + .indexName("my-index") + .build(); +``` + +## Distance Metrics + +S3 Vectors embedding store supports two distance metrics. The distance values are automatically converted to relevance scores in the range [0, 1], where 1 represents the most relevant match. + +### Cosine Distance (Default) + +**Best for:** Text embeddings, semantic similarity search + +- Measures the cosine of the angle between vectors +- Converted to relevance score: `score = (1 - distance + 1) / 2` +- Results are independent of vector magnitude + +```java +.distanceMetric(DistanceMetric.COSINE) // Default, recommended for text embeddings +``` + +### Euclidean Distance + +**Best for:** When both direction and magnitude matter + +- Measures straight-line distance between vectors +- Range: [0, ∞) +- Converted to relevance score: `score = 1 / (1 + distance)` + +```java +.distanceMetric(DistanceMetric.EUCLIDEAN) +``` + +## Filtering + +S3 Vectors embedding store supports filtering search results by metadata fields. + +### Supported Filter Operations + +- `isEqualTo`: Equal comparison +- `isNotEqualTo`: Not equal comparison +- `isGreaterThan`: Greater than comparison +- `isGreaterThanOrEqualTo`: Greater than or equal comparison +- `isLessThan`: Less than comparison +- `isLessThanOrEqualTo`: Less than or equal comparison +- `isIn`: IN operator (multiple values) +- `isNotIn`: NOT IN operator +- `And`: Logical AND +- `Or`: Logical OR +- `Not`: Logical NOT + +## Implementation Details + +### Credentials + +By default, the store uses `DefaultCredentialsProvider` which follows the standard AWS credential resolution chain (environment variables, system properties, credential files, EC2 instance profile, etc.). You can provide a custom `AwsCredentialsProvider` via the builder. + +### Index Creation + +When `createIndexIfNotExists` is set to `true` (default), the index is created automatically on the first embedding insertion. The index dimension and distance metric are set based on the first embedding added and the configured distance metric. + +### Resource Cleanup + +`S3VectorsEmbeddingStore` implements `AutoCloseable`. When you're done using the store, call `close()` to release the underlying S3VectorsClient resources, or use try-with-resources. + +## Limitations + +- **Maximum Results**: S3 Vectors limits search results to 100 per query (topK range: 1-100) +- **Remove by Filter**: `removeAll(Filter)` is not supported; use `removeAll(Collection ids)` instead +- **Remove All**: `removeAll()` deletes the entire index + +## Examples + +- [S3VectorsEmbeddingStoreIT](https://github.com/langchain4j/langchain4j-community/blob/main/embedding-stores/langchain4j-community-s3-vectors/src/test/java/dev/langchain4j/community/store/embedding/s3/S3VectorsEmbeddingStoreIT.java) diff --git a/docs/docs/integrations/embedding-stores/arcadedb.md b/docs/docs/integrations/embedding-stores/arcadedb.md new file mode 100644 index 0000000..902d439 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/arcadedb.md @@ -0,0 +1,267 @@ +--- +sidebar_position: 30 +--- + +# ArcadeDB + +https://arcadedb.com/ + +ArcadeDB is a multi-model NoSQL database that supports graph, document, key-value, time-series, and vector data. It provides a built-in LSM_VECTOR index (powered by JVector/HNSW) for high-performance approximate nearest neighbor (ANN) vector search. + +The langchain4j integration supports two operation modes: + +- **Remote mode** — connects to an ArcadeDB server over HTTP. Suitable for production deployments and shared infrastructure. +- **Embedded mode** — runs ArcadeDB in-process inside the same JVM. No server or Docker container required; the database is stored on the local filesystem. Ideal for testing, desktop applications, or single-process workloads. + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-community-arcadedb + ${latest version here} + +``` + +Note: This is a community integration module. You may need to add the langchain4j-community repository to your project configuration. + +## APIs + +- `ArcadeDBEmbeddingStore` + +## Features + +- **Two Operation Modes**: Remote (HTTP client to an ArcadeDB server) or embedded (ArcadeDB runs in-process, no server needed) +- **Multi-Model Database**: Stores embeddings as vertices in ArcadeDB's graph model alongside documents, key-value, and time-series data +- **HNSW Vector Index**: Uses ArcadeDB's LSM_VECTOR index (JVector-based) for fast approximate nearest neighbor search +- **Metadata Filtering**: Supports filtering search results by metadata using comparison and logical operators +- **Persistent Storage**: Data is durable across restarts in both remote and embedded modes +- **Auto Schema Creation**: Automatically creates the vertex type, properties, and vector index on first use +- **Multiple Similarity Functions**: Supports COSINE (default), EUCLIDEAN, and SQUARED_EUCLIDEAN distance metrics (remote mode) +- **Batch Operations**: Add multiple embeddings in a single call +- **Flexible Removal**: Remove embeddings by ID, by filter, or clear all + +## Basic Usage + +### Remote Mode + +Remote mode connects to a running ArcadeDB server. See [Running ArcadeDB with Docker](#running-arcadedb-with-docker) to start one locally. + +#### Connect to an Existing Database + +```java +EmbeddingStore embeddingStore = ArcadeDBEmbeddingStore.builder() + .host("localhost") + .port(2480) + .databaseName("my_database") + .username("root") + .password("playwithdata") + .dimension(384) // Must match your embedding model's dimension + .build(); +``` + +#### Auto-Create the Database + +```java +EmbeddingStore embeddingStore = ArcadeDBEmbeddingStore.builder() + .host("localhost") + .port(2480) + .databaseName("my_database") + .username("root") + .password("playwithdata") + .dimension(384) + .createDatabase(true) // Create database if it doesn't exist + .build(); +``` + +### Embedded Mode + +Embedded mode runs ArcadeDB inside the same JVM. No server is needed — just provide a path on the local filesystem where the database should be stored. The database is created automatically if it does not already exist. + +Always call `close()` when you are finished to release resources. + +```java +ArcadeDBEmbeddingStore embeddingStore = ArcadeDBEmbeddingStore.embeddedBuilder() + .databasePath("/path/to/my-database") + .dimension(384) // Must match your embedding model's dimension + .build(); + +// ... use the store ... + +embeddingStore.close(); +``` + +Use try-finally (or try-with-resources via a wrapper) to ensure `close()` is always called: + +```java +ArcadeDBEmbeddingStore embeddingStore = ArcadeDBEmbeddingStore.embeddedBuilder() + .databasePath("/path/to/my-database") + .dimension(384) + .build(); +try { + // ... use the store ... +} finally { + embeddingStore.close(); +} +``` + +### Add and Search Embeddings + +The search API is identical in both modes: + +```java +// Add a text segment with its embedding +TextSegment segment = TextSegment.from("Hello, world!", Metadata.from("source", "example")); +Embedding embedding = embeddingModel.embed(segment).content(); +embeddingStore.add(embedding, segment); + +// Search for similar embeddings +EmbeddingSearchRequest request = EmbeddingSearchRequest.builder() + .queryEmbedding(queryEmbedding) + .maxResults(5) + .minScore(0.7) + .build(); + +List> matches = embeddingStore.search(request).matches(); +``` + +## Configuration Options + +### Remote Mode + +```java +EmbeddingStore embeddingStore = ArcadeDBEmbeddingStore.builder() + .host("localhost") // Required: ArcadeDB server hostname + .port(2480) // Default: 2480 (HTTP port) + .databaseName("my_database") // Required: database name + .username("root") // Required: username + .password("playwithdata") // Required: password + .typeName("EmbeddingDocument") // Default: "EmbeddingDocument" — vertex type name + .dimension(384) // Required: embedding vector dimension + .similarityFunction("COSINE") // Default: "COSINE" — similarity metric + .maxConnections(16) // Default: 16 — HNSW graph connections per node + .beamWidth(100) // Default: 100 — HNSW search beam width + .createDatabase(false) // Default: false — auto-create the database + .metadataPrefix("meta_") // Default: "meta_" — prefix for metadata properties + .build(); +``` + +### Embedded Mode + +```java +ArcadeDBEmbeddingStore embeddingStore = ArcadeDBEmbeddingStore.embeddedBuilder() + .databasePath("/path/to/my-database") // Required: local filesystem path for the database + .typeName("EmbeddingDocument") // Default: "EmbeddingDocument" — vertex type name + .dimension(384) // Default: 384 — embedding vector dimension + .maxConnections(16) // Default: 16 — HNSW graph connections per node + .beamWidth(100) // Default: 100 — HNSW search beam width + .metadataPrefix("") // Default: "" (no prefix) — prefix for metadata properties + .build(); +``` + +### Parameter Guidelines + +**Shared parameters (both modes):** + +- **typeName**: The vertex type used to store embedding documents. Changing this allows multiple embedding stores within the same database +- **dimension**: Must match your embedding model's output dimension exactly +- **maxConnections**: Controls graph connectivity in the HNSW index. Higher values improve recall but increase memory and index build time. Recommended: 16–128 +- **beamWidth**: Controls the quality of the HNSW index construction and search. Higher values produce better recall at the cost of speed. Recommended: 100–500 +- **metadataPrefix**: Prefix applied to metadata keys when stored as vertex properties. Change if your metadata keys conflict with built-in properties + +**Remote-only parameters:** + +- **host**: Hostname or IP address of the ArcadeDB server (required) +- **port**: HTTP port for the ArcadeDB REST API (default: 2480) +- **databaseName**: The database to connect to or create (required) +- **username / password**: ArcadeDB credentials (required) +- **similarityFunction**: + - `COSINE` — Cosine similarity; best for normalized vectors (default) + - `EUCLIDEAN` — Euclidean distance + - `SQUARED_EUCLIDEAN` — Squared Euclidean distance; faster than EUCLIDEAN +- **createDatabase**: Set to `true` to automatically create the database if it does not exist + +**Embedded-only parameters:** + +- **databasePath**: Path to the directory where the embedded database is stored. Created automatically if it does not exist +- **database**: Alternatively, supply an existing `com.arcadedb.database.Database` instance directly instead of a path + +## Metadata Filtering + +ArcadeDB supports filtering search results by metadata. Filters are applied after the vector index lookup. + +```java +// Filter by a single metadata value +Filter filter = new IsEqualTo("source", "wikipedia"); + +EmbeddingSearchRequest request = EmbeddingSearchRequest.builder() + .queryEmbedding(queryEmbedding) + .maxResults(5) + .filter(filter) + .build(); + +List> matches = embeddingStore.search(request).matches(); +``` + +### Supported Filter Types + +**Comparison operators:** +- `IsEqualTo`, `IsNotEqualTo` +- `IsGreaterThan`, `IsGreaterThanOrEqualTo` +- `IsLessThan`, `IsLessThanOrEqualTo` +- `IsIn`, `IsNotIn` + +**Logical operators:** +- `And`, `Or`, `Not` + +## Removal Operations + +```java +// Remove by list of IDs +embeddingStore.removeAll(List.of("id1", "id2")); + +// Remove by metadata filter +embeddingStore.removeAll(new IsEqualTo("source", "old-source")); + +// Remove all embeddings +embeddingStore.removeAll(); +``` + +## Current Limitations + +- **Approximate Search**: The HNSW index is approximate. With very large result sets containing many near-identical vectors, some documents may not be returned +- **In-Memory Filter Application**: Metadata filters are applied in-memory after the vector search rather than at the index level. The store fetches up to 5× the requested number of results to account for filter reduction +- **Floating-Point Precision**: ArcadeDB returns vectors as JSON doubles, which may introduce minor floating-point precision differences compared to the original stored values. `Double.MIN_VALUE` (4.9E-324) underflows to 0.0 and cannot be stored precisely +- **No String Content Filters**: String-based content filters (e.g., `ContainsString`) are not supported; only the metadata filter types listed above are available +- **No Similarity Function Choice in Embedded Mode**: The embedded builder does not expose a `similarityFunction` option; the index uses its default metric + +## Running ArcadeDB with Docker + +Required for remote mode. The quickest way to get started: + +```bash +docker run -d \ + --name arcadedb \ + -p 2480:2480 \ + -e JAVA_OPTS="-Darcadedb.server.rootPassword=playwithdata" \ + arcadedata/arcadedb:latest +``` + +Then connect your store: + +```java +EmbeddingStore embeddingStore = ArcadeDBEmbeddingStore.builder() + .host("localhost") + .port(2480) + .databaseName("embeddings") + .username("root") + .password("playwithdata") + .dimension(384) + .createDatabase(true) + .build(); +``` + +## Examples + +- For integration test examples, check the test files in the [langchain4j-community-arcadedb module](https://github.com/langchain4j/langchain4j-community/tree/main/embedding-stores/langchain4j-community-arcadedb/src/test/java) +- You can also find a few examples in the [langchain4j-examples project](https://github.com/langchain4j/langchain4j-examples) diff --git a/docs/docs/integrations/embedding-stores/astra-db.md b/docs/docs/integrations/embedding-stores/astra-db.md new file mode 100644 index 0000000..d91eebd --- /dev/null +++ b/docs/docs/integrations/embedding-stores/astra-db.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 2 +--- + +# Astra DB + +[Astra DB](https://www.datastax.com/products/datastax-astra) + +Tutorial coming soon \ No newline at end of file diff --git a/docs/docs/integrations/embedding-stores/azure-ai-search.md b/docs/docs/integrations/embedding-stores/azure-ai-search.md new file mode 100644 index 0000000..bd2895e --- /dev/null +++ b/docs/docs/integrations/embedding-stores/azure-ai-search.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 3 +--- + +# Azure AI Search + +https://azure.microsoft.com/en-us/products/ai-services/ai-search/ + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-azure-ai-search + 1.17.2-beta27 + +``` + +## APIs + +- `AzureAiSearchEmbeddingStore` - supports vector search +- `AzureAiSearchContentRetriever` - supports vector, full-text, hybrid searches and re-ranking + + +## Examples + +- [AzureAiSearchEmbeddingStoreIT](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-azure-ai-search/src/test/java/dev/langchain4j/store/embedding/azure/search/AzureAiSearchEmbeddingStoreIT.java) +- [AzureAiSearchContentRetrieverIT](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-azure-ai-search/src/test/java/dev/langchain4j/rag/content/retriever/azure/search/AzureAiSearchContentRetrieverIT.java) diff --git a/docs/docs/integrations/embedding-stores/azure-cosmos-mongo-vcore.md b/docs/docs/integrations/embedding-stores/azure-cosmos-mongo-vcore.md new file mode 100644 index 0000000..e7934c3 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/azure-cosmos-mongo-vcore.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 4 +--- + +# Azure CosmosDB Mongo vCore + +https://azure.microsoft.com/en-us/products/cosmos-db/ + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-azure-cosmos-mongo-vcore + 1.17.2-beta27 + +``` + +## APIs + +- `AzureCosmosDbMongoVCoreEmbeddingStore` + + +## Examples + +- [AzureCosmosDBMongoVCoreEmbeddingStoreIT](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-azure-cosmos-mongo-vcore/src/test/java/dev/langchain4j/store/embedding/azure/cosmos/mongo/vcore/AzureCosmosDBMongoVCoreEmbeddingStoreIT.java) diff --git a/docs/docs/integrations/embedding-stores/azure-cosmos-nosql.md b/docs/docs/integrations/embedding-stores/azure-cosmos-nosql.md new file mode 100644 index 0000000..0a7a5c8 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/azure-cosmos-nosql.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 5 +--- + +# Azure CosmosDB NoSQL + +https://azure.microsoft.com/en-us/products/cosmos-db/ + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-azure-cosmos-nosql + 1.17.2-beta27 + +``` + +## APIs + +- `AzureCosmosDbNoSqlEmbeddingStore` + + +## Examples + +- [AzureCosmosDbNoSqlEmbeddingStoreIT](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-azure-cosmos-nosql/src/test/java/dev/langchain4j/store/embedding/azure/cosmos/nosql/AzureCosmosDbNoSqlEmbeddingStoreIT.java) diff --git a/docs/docs/integrations/embedding-stores/cassandra.md b/docs/docs/integrations/embedding-stores/cassandra.md new file mode 100644 index 0000000..cf8c19f --- /dev/null +++ b/docs/docs/integrations/embedding-stores/cassandra.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 6 +--- + +# Cassandra + +[Cassandra](https://cassandra.apache.org/) + +Tutorial coming soon diff --git a/docs/docs/integrations/embedding-stores/chroma.md b/docs/docs/integrations/embedding-stores/chroma.md new file mode 100644 index 0000000..530f185 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/chroma.md @@ -0,0 +1,49 @@ +--- +sidebar_position: 7 +--- + +# Chroma + +https://www.trychroma.com/ + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-chroma + 1.17.2-beta27 + +``` + +## APIs + +- `ChromaEmbeddingStore` + + +## Examples + +- [ChromaEmbeddingStoreExample](https://github.com/langchain4j/langchain4j-examples/blob/main/chroma-example/src/main/java/ChromaEmbeddingStoreExample.java) + +## Supported API Versions +Chroma has multiple REST API versions: +- until version 0.5.16: only API V1 is supported +- versions 0.5.16 to 0.6.3: API V1 and V2 are supported (there are some bugs in V1 API introduced around 0.6.2) +- versions after 0.7.0: only API V2 is supported, so you need to select the proper version +when configuring the `ChromaEmbeddingStore`: +```java +ChromaEmbeddingStore.builder() + .apiVersion(ChromaApiVersion.V2) + .baseUrl(...) + .tenantName(...) + .databaseName(...) + .collectionName(...) + .build(); +``` + +## Current Limitations + +- Chroma cannot filter by greater and less than of alphanumeric metadata, only int and float are supported +- Chroma filters by *not* as following: if you filter by "key" not equals "a", + then in fact all items with "key" != "a" value are returned, but no items without "key" metadata! diff --git a/docs/docs/integrations/embedding-stores/clickhouse.md b/docs/docs/integrations/embedding-stores/clickhouse.md new file mode 100644 index 0000000..aca4416 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/clickhouse.md @@ -0,0 +1,54 @@ +--- +sidebar_position: 8 +--- + +# ClickHouse + +[ClickHouse](https://clickhouse.com/) is the fastest and most resource efficient open-source +database for real-time apps and analytics with full SQL support and a wide range of functions to +assist users in writing analytical queries. Lately added data structures and distance search +functions (like cosineDistance) as well +as [approximate nearest neighbor search indexes](https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/annindexes) +enable ClickHouse to be used as a high performance and scalable vector database to store and search +vectors with SQL. + +## Maven Dependency + +```xml + + + dev.langchain4j + langchain4j-community-clickhouse + ${latest version here} + +``` + +## APIs + +LangChain4j uses `client-v2` as ClickHouse client. To create `ClickHouseEmbeddingStore` instance, you need to provide a `ClickHouseSettings`: + +```java +// Mapping metadata key to ClickHouse data type. +Map metadataTypeMap = new HashMap<>(); + +ClickHouseSettings settings = ClickHouseSettings.builder() + .url("http://localhost:8123") + .table("langchain4j_table") + .username(System.getenv("USERNAME")) + .password(System.getenv("PASSWORD")) + .dimension(embeddingModel.dimension()) + .metadataTypeMap(metadataTypeMap) + .build(); +``` + +Then you can create the embedding store: + +```java +ClickHouseEmbeddingStore embeddingStore = ClickHouseEmbeddingStore.builder() + .settings(settings) + .build(); +``` + +## Examples + +- [ClickHouseEmbeddingStoreIT](https://github.com/langchain4j/langchain4j-community/blob/main/langchain4j-community-clickhouse/src/test/java/dev/langchain4j/community/store/embedding/clickhouse/ClickHouseEmbeddingStoreIT.java) diff --git a/docs/docs/integrations/embedding-stores/cloud-sql.md b/docs/docs/integrations/embedding-stores/cloud-sql.md new file mode 100644 index 0000000..ed3f571 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/cloud-sql.md @@ -0,0 +1,95 @@ +# Google Cloud SQL for PostgreSQL + +[Cloud SQL](https://cloud.google.com/sql/docs/postgres) is a fully managed relational database service that offers high performance, seamless integration, and impressive scalability. Cloud SQL is 100% compatible with PostgreSQL. Extend your database application to build AI-powered experiences leveraging Cloud SQL's Langchain integrations. + +This module implements `EmbeddingStore` backed by an Cloud SQL for PostgreSQL database. + +## Before You Begin + +In order to use this library, you first need to go through the following +steps: + +1. [Select or create a Cloud Platform project.](https://console.cloud.google.com/project) +2. [Enable billing for your project.](https://cloud.google.com/billing/docs/how-to/modify-project#enable_billing_for_a_project) +3. [Enable the CloudSQL API.](https://console.cloud.google.com/flows/enableapi?apiid=sql.googleapis.com) +4. [Setup Authentication.](https://googleapis.dev/python/google-api-core/latest/auth.html) + +### Maven Dependency + +```xml + + dev.langchain4j + langchain4j-community-cloud-sql-pg + ${latest version here} + +``` + +## PostgresEmbeddingStore Usage + +Use a vector store to store text embedded data and perform vector search. Instances of `PostgresEmbeddingStore` can be created by configuring provided `Builder`, it requires the following: + +- `PostgresEngine` instance +- table name +- schema name (optional, default: "public") +- content column (optional, default: "content") +- embedding column (optional, default: "embedding") +- id column (optional, default: "langchain_id") +- metadata column names (optional) +- additional metadata json column (optional, default: "langchain_metadata") +- ignored metadata column names (optional) +- distance strategy (optional, default:DistanceStrategy.COSINE_DISTANCE) +- query options (optional) + +example usage: +```java +import dev.langchain4j.data.document.Metadata; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.store.embedding.EmbeddingMatch; +import dev.langchain4j.store.embedding.EmbeddingSearchRequest; +import dev.langchain4j.store.embedding.EmbeddingSearchResult; +import dev.langchain4j.engine.EmbeddingStoreConfig; +import dev.langchain4j.engine.PostgresEngine; +import dev.langchain4j.engine.MetadataColumn; +import dev.langchain4j.store.embedding.cloudsql.PostgresEmbeddingStore; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +PostgresEngine engine = new PostgresEngine.Builder() + .projectId("") + .region("") + .instance("") + .database("") + .build(); + +PostgresEmbeddingStore store = new PostgresEmbeddingStore.Builder(engine, TABLE_NAME) + .build(); + +List testTexts = Arrays.asList("cat", "dog", "car", "truck"); +List embeddings = new ArrayList<>(); +List textSegments = new ArrayList<>(); +EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel(); + +for (String text : testTexts) { + Map metaMap = new HashMap<>(); + metaMap.put("my_metadata", "string"); + Metadata metadata = new Metadata(metaMap); + textSegments.add(new TextSegment(text, metadata)); + embeddings.add(MyEmbeddingModel.embed(text).content()); +} +List ids = store.addAll(embeddings, textSegments); +// search for "cat" +EmbeddingSearchRequest request = EmbeddingSearchRequest.builder() + .queryEmbedding(embeddings.get(0)) + .maxResults(10) + .minScore(0.9) + .build(); +List> result = store.search(request).matches(); +// remove "cat" +store.removeAll(singletonList(result.get(0).embeddingId())); +``` diff --git a/docs/docs/integrations/embedding-stores/cockroachdb.md b/docs/docs/integrations/embedding-stores/cockroachdb.md new file mode 100644 index 0000000..6e864ff --- /dev/null +++ b/docs/docs/integrations/embedding-stores/cockroachdb.md @@ -0,0 +1,494 @@ +--- +sidebar_position: 33 +--- + +# CockroachDB + +[CockroachDB](https://www.cockroachlabs.com/) is a distributed SQL database that +speaks the PostgreSQL wire protocol. Since v24.2 it ships a native `VECTOR` +column type, and since v25.2 it offers a distributed approximate nearest +neighbour index called **C-SPANN**. The `langchain4j-community-cockroachdb` +module integrates both with LangChain4j as: + +- a vector `EmbeddingStore` (`CockroachDbEmbeddingStore`) +- a `ChatMemoryStore` (`CockroachDbChatMemoryStore`) + +The Java module mirrors the feature set of the official Python +[`langchain-cockroachdb`](https://github.com/cockroachdb/langchain-cockroachdb) +library where the Java equivalents exist. + +## Version Requirements + +| Feature | Minimum CockroachDB version | +| --- | --- | +| `VECTOR(n)` column type | v24.2 | +| `CREATE VECTOR INDEX` (C-SPANN) | v25.2 | +| Row-level TTL via `ttl_expiration_expression` | v23.1 | + +On CockroachDB v25.2, vector indexes are gated by a cluster setting. Enable it +once per cluster before creating a store with a `CSpannIndex`: + +```sql +SET CLUSTER SETTING feature.vector_index.enabled = true; +``` + +## Maven Dependency + +:::note +Since CockroachDB support is part of `langchain4j-community`, it will be +available starting from version `1.17.2-beta27` or later. +::: + +```xml + + dev.langchain4j + langchain4j-community-cockroachdb + 1.17.2-beta27 + +``` + +If you import the Community BOM, you can omit the version. + +## APIs + +The module exposes four public classes: + +### `CockroachDbEngine` + +Wraps a HikariCP `DataSource` and handles connection pooling. Builds from +individual `host`/`port`/`database`/`username`/`password` fields, from a full +connection string (the Python-style `cockroachdb://` scheme is rewritten to +`jdbc:postgresql://` automatically), or from an existing `DataSource` via +`CockroachDbEngine.from(dataSource)`. + +### `CockroachDbSchema` + +Encapsulates the embedding table layout: table and column names, vector +dimension, distance metric, optional namespace column for multi-tenancy, the +chosen vector index strategy, and an optional generated `tsvector` column for +future hybrid search. + +### `CockroachDbEmbeddingStore` + +Implements LangChain4j's `EmbeddingStore` against the native +CockroachDB `VECTOR` column. Supports batch insert, JSONB metadata +filtering, removal by id / by `Filter` / in bulk, optional namespace scoping, +and optional per-query `vector_search_beam_size` tuning for C-SPANN. + +### `CockroachDbChatMemoryStore` + +Implements LangChain4j's `ChatMemoryStore`. Persists serialised chat messages +in a JSONB column ordered by an explicit insertion index, with optional +row-level TTL. + +## Connecting + +`CockroachDbEngine` wraps a `HikariDataSource`. You can build one from a +connection string or from individual fields. + +```java +import dev.langchain4j.community.store.embedding.cockroachdb.CockroachDbEngine; + +CockroachDbEngine engine = CockroachDbEngine.builder() + .host("localhost") + .port(26257) + .database("defaultdb") + .username("root") + .password("") + .sslMode("disable") + .build(); +``` + +The builder also accepts a full connection string. The Python-style +`cockroachdb://` scheme is rewritten to `jdbc:postgresql://` automatically, +so you can paste the same URL the Python library uses: + +```java +CockroachDbEngine engine = CockroachDbEngine.fromConnectionString( + "cockroachdb://root@localhost:26257/defaultdb?sslmode=disable"); +``` + +If you already have a `DataSource`, use `CockroachDbEngine.from(dataSource)`. + +## Vector store + +A minimal vector store uses sequential scan (`NoIndex`), which is appropriate +for small datasets and tests: + +```java +import dev.langchain4j.community.store.embedding.cockroachdb.CockroachDbEmbeddingStore; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.embedding.onnx.allminilml6v2q.AllMiniLmL6V2QuantizedEmbeddingModel; + +EmbeddingModel model = new AllMiniLmL6V2QuantizedEmbeddingModel(); + +CockroachDbEmbeddingStore store = CockroachDbEmbeddingStore.builder() + .engine(engine) + .dimension(model.dimension()) + .tableName("embeddings") + .build(); + +TextSegment segment = TextSegment.from("Cockroaches are surprisingly resilient."); +Embedding embedding = model.embed(segment).content(); +store.add(embedding, segment); +``` + +For production workloads on CockroachDB v25.2+, add a C-SPANN vector index: + +```java +import dev.langchain4j.community.store.embedding.cockroachdb.index.CSpannIndex; + +CockroachDbEmbeddingStore store = CockroachDbEmbeddingStore.builder() + .engine(engine) + .dimension(model.dimension()) + .vectorIndex(CSpannIndex.builder() + .minPartitionSize(16) + .maxPartitionSize(128) + .build()) + .build(); +``` + +The DDL emitted for the index is: + +```sql +CREATE VECTOR INDEX IF NOT EXISTS embeddings_embedding_vector_idx + ON public.embeddings (embedding) + WITH (min_partition_size = 16, max_partition_size = 128); +``` + +C-SPANN picks the distance function from the query operator (`<=>` for cosine, +`<->` for L2, `<#>` for inner product), so `MetricType` is selected at query +time on the store, not bound to the index. + +### Searching + +`EmbeddingSearchRequest` works the same as in any other LangChain4j store: + +```java +import dev.langchain4j.store.embedding.EmbeddingSearchRequest; +import dev.langchain4j.store.embedding.EmbeddingSearchResult; + +EmbeddingSearchResult result = store.search( + EmbeddingSearchRequest.builder() + .queryEmbedding(model.embed("resilience").content()) + .maxResults(5) + .minScore(0.6) + .build()); + +result.matches().forEach(m -> + System.out.printf("%s (%.3f) %s%n", m.embeddingId(), m.score(), m.embedded().text())); +``` + +### Tuning C-SPANN at query time + +CockroachDB exposes a session variable, `vector_search_beam_size`, that +controls the recall/latency tradeoff. Set it on the store builder to wrap +each search in a transaction that scopes the setting with `SET LOCAL`: + +```java +CockroachDbEmbeddingStore store = CockroachDbEmbeddingStore.builder() + .engine(engine) + .dimension(model.dimension()) + .vectorIndex(CSpannIndex.builder().build()) + .searchBeamSize(32) + .build(); +``` + +Higher values trade latency for recall. The default beam size is decided by +CockroachDB if you leave the field unset. + +### Metadata filtering + +Metadata is stored in a JSONB column and filtered at query time using +LangChain4j `Filter` expressions: + +```java +import dev.langchain4j.store.embedding.filter.MetadataFilterBuilder; + +EmbeddingSearchResult result = store.search( + EmbeddingSearchRequest.builder() + .queryEmbedding(query) + .maxResults(10) + .filter(MetadataFilterBuilder.metadataKey("category").isEqualTo("biology") + .and(MetadataFilterBuilder.metadataKey("year").isGreaterThan(2020))) + .build()); +``` + +Comparison filters (`>`, `>=`, `<`, `<=`) cast the JSONB value to `numeric`. +Equality on strings compares JSON text. The filter key must contain only +alphanumeric characters, dots, underscores or hyphens. + +### Multi-tenancy with a namespace column + +To scope rows by tenant, add a `namespaceColumn` to the schema and configure a +namespace value on each store instance. The column is added as a prefix to the +C-SPANN index so per-tenant queries stay fast: + +```java +CockroachDbEmbeddingStore tenantA = CockroachDbEmbeddingStore.builder() + .engine(engine) + .dimension(model.dimension()) + .namespaceColumn("tenant_id") + .namespace("acme") + .vectorIndex(CSpannIndex.builder().build()) + .build(); +``` + +The generated index becomes `CREATE VECTOR INDEX ... ON embeddings (tenant_id, embedding)`, +and every read/write performed through this store is filtered to `tenant_id = 'acme'`. + +### Optional full-text column + +If you intend to combine vector search with full-text search later, enable a +generated `tsvector` column at table creation time. A GIN index is created +alongside it: + +```java +CockroachDbEmbeddingStore store = CockroachDbEmbeddingStore.builder() + .engine(engine) + .dimension(model.dimension()) + .createTsvectorColumn(true) + .build(); +``` + +Hybrid (vector + FTS) query execution is not yet implemented; the column is +created so it can be used by application code or a future release. + +## Chat memory + +`CockroachDbChatMemoryStore` implements `ChatMemoryStore` and persists +serialised chat messages in a JSONB column ordered by insertion time: + +```java +import dev.langchain4j.community.store.memory.chat.cockroachdb.CockroachDbChatMemoryStore; + +CockroachDbChatMemoryStore memory = CockroachDbChatMemoryStore.builder() + .engine(engine) + .tableName("chat_memory") + .build(); +``` + +The schema is: + +```sql +CREATE TABLE chat_memory ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + session_id TEXT NOT NULL, + message JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX chat_memory_session_idx ON chat_memory (session_id, created_at); +``` + +`updateMessages` replaces the full session inside a transaction, so partial +writes are not visible to readers. + +### Row-level TTL + +CockroachDB can expire rows automatically. Pass a `ttl` duration to enable +[row-level TTL](https://www.cockroachlabs.com/docs/stable/row-level-ttl) on +the chat memory table: + +```java +import java.time.Duration; + +CockroachDbChatMemoryStore memory = CockroachDbChatMemoryStore.builder() + .engine(engine) + .tableName("chat_memory") + .ttl(Duration.ofDays(7)) + .ttlJobCron("@daily") + .build(); +``` + +The schema setup emits: + +```sql +ALTER TABLE chat_memory SET ( + ttl_expiration_expression = $$(created_at + '7 days')$$, + ttl_job_cron = '@daily' +); +``` + +To disable TTL on an existing table: + +```java +memory.disableTtl(); +``` + +## Retries + +CockroachDB returns SQLSTATE `40001` when a transaction must be retried under +its default `SERIALIZABLE` isolation. The store wraps each unit of work in a +retry loop with exponential backoff and jitter (5 attempts by default, +starting at 100 ms, doubling up to 10 seconds). No additional configuration +is needed. + +## Connection string formats + +The following forms are all accepted by `CockroachDbEngine.fromConnectionString`: + +| Form | Example | +| --- | --- | +| Python style | `cockroachdb://root@localhost:26257/defaultdb?sslmode=disable` | +| psycopg style | `cockroachdb+psycopg://user:pw@host:26257/db` | +| libpq style | `postgresql://user@host:26257/db` | +| JDBC style | `jdbc:postgresql://localhost:26257/defaultdb` | + +For CockroachDB Cloud, use the connection string from the cluster console, +typically: + +``` +cockroachdb://USER:PASSWORD@HOST:26257/DATABASE?sslmode=verify-full +``` + +## Parameter Summary + +### `CockroachDbEngine` parameters + +| Parameter | Description | Default | Required/Optional | +| --- | --- | --- | --- | +| `host` | Hostname of the CockroachDB server | `localhost` | Required (if no `connectionString`) | +| `port` | Port number of the CockroachDB server | `26257` | Required (if no `connectionString`) | +| `database` | Database to connect to | `defaultdb` | Required (if no `connectionString`) | +| `username` | Username for authentication | `root` | Required | +| `password` | Password for authentication | `""` (empty) | Optional | +| `schema` | Default schema name | `public` | Optional | +| `sslMode` | SSL mode (`disable`, `require`, `verify-full`, etc.) | `disable` | Optional | +| `maxPoolSize` | Maximum HikariCP pool size | `10` | Optional | +| `minPoolSize` | Minimum idle connections | `5` | Optional | +| `connectionTimeoutMs` | Connection timeout in milliseconds | `10000` | Optional | +| `idleTimeoutMs` | Idle timeout in milliseconds | `300000` | Optional | +| `maxLifetimeMs` | Maximum connection lifetime in milliseconds | `3600000` | Optional | +| `connectionString` | Full URL; overrides individual host/port/db when set | `null` | Optional | + +### `CockroachDbEmbeddingStore` parameters + +| Parameter | Description | Default | Required/Optional | +| --- | --- | --- | --- | +| `engine` | `CockroachDbEngine` instance | None | **Required** | +| `dimension` | Embedding vector dimension | None | **Required** | +| `tableName` | Embeddings table name | `embeddings` | Optional | +| `schemaName` | Database schema name | `public` | Optional | +| `metricType` | Distance metric: `COSINE`, `EUCLIDEAN`, or `DOT_PRODUCT` | `COSINE` | Optional | +| `vectorIndex` | `CSpannIndex` or `NoIndex` | `NoIndex` (sequential scan) | Optional | +| `namespaceColumn` | Tenant column name for multi-tenancy | `null` (disabled) | Optional | +| `namespace` | Tenant value applied on every read and write | `null` | Optional, requires `namespaceColumn` | +| `searchBeamSize` | Per-query `vector_search_beam_size` session variable | `null` (CockroachDB default) | Optional | +| `createTableIfNotExists` | Create the table at build time | `true` | Optional | +| `createTsvectorColumn` | Add a generated `tsvector` column + GIN index | `false` | Optional | + +### `CSpannIndex` parameters (CockroachDB v25.2+) + +| Parameter | Description | Default | Required/Optional | +| --- | --- | --- | --- | +| `name` | Custom index name | `{table}_{column}_vector_idx` | Optional | +| `minPartitionSize` | Minimum partition size (emitted via `WITH`) | CockroachDB default | Optional | +| `maxPartitionSize` | Maximum partition size (emitted via `WITH`) | CockroachDB default | Optional | + +### `CockroachDbChatMemoryStore` parameters + +| Parameter | Description | Default | Required/Optional | +| --- | --- | --- | --- | +| `engine` | `CockroachDbEngine` instance | None | **Required** | +| `tableName` | Chat history table name | `message_store` | Optional | +| `schemaName` | Database schema name | `public` | Optional | +| `ttl` | Row-level TTL duration; enables CockroachDB TTL when set | `null` (disabled) | Optional | +| `ttlJobCron` | TTL job schedule | `@daily` | Optional, requires `ttl` | +| `createTableIfNotExists` | Create the table at build time | `true` | Optional | + +## Example + +A minimal end-to-end RAG demo that boots a CockroachDB Testcontainer, +indexes two text segments, and runs a similarity search: + +```java +import dev.langchain4j.community.store.embedding.cockroachdb.CockroachDbEmbeddingStore; +import dev.langchain4j.community.store.embedding.cockroachdb.CockroachDbEngine; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel; +import dev.langchain4j.store.embedding.EmbeddingMatch; +import dev.langchain4j.store.embedding.EmbeddingSearchRequest; +import dev.langchain4j.store.embedding.EmbeddingStore; +import java.util.List; +import org.testcontainers.containers.CockroachContainer; + +public class CockroachDbEmbeddingStoreExample { + + public static void main(String[] args) { + try (CockroachContainer cockroach = new CockroachContainer("cockroachdb/cockroach:latest-v25.2")) { + cockroach.start(); + + CockroachDbEngine engine = CockroachDbEngine.builder() + .connectionString(cockroach.getJdbcUrl()) + .username(cockroach.getUsername()) + .password(cockroach.getPassword()) + .build(); + + EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel(); + + EmbeddingStore embeddingStore = CockroachDbEmbeddingStore.builder() + .engine(engine) + .dimension(embeddingModel.dimension()) + .tableName("demo_embeddings") + .build(); + + TextSegment segment1 = TextSegment.from("I like football."); + Embedding embedding1 = embeddingModel.embed(segment1).content(); + embeddingStore.add(embedding1, segment1); + + TextSegment segment2 = TextSegment.from("The weather is good today."); + Embedding embedding2 = embeddingModel.embed(segment2).content(); + embeddingStore.add(embedding2, segment2); + + Embedding queryEmbedding = embeddingModel.embed("What is your favourite sport?").content(); + EmbeddingSearchRequest request = EmbeddingSearchRequest.builder() + .queryEmbedding(queryEmbedding) + .maxResults(1) + .build(); + + List> matches = embeddingStore.search(request).matches(); + EmbeddingMatch match = matches.get(0); + + System.out.println(match.score()); // ~0.81 + System.out.println(match.embedded().text()); // I like football. + + engine.close(); + } + } +} +``` + +The example uses the default sequential-scan index so it runs on any +CockroachDB v24.2 or later without extra cluster setup. To switch to the +C-SPANN distributed ANN index on v25.2 or later, enable the feature flag once +per cluster and pass `CSpannIndex.builder().build()` to the store via +`.vectorIndex(...)`: + +```sql +SET CLUSTER SETTING feature.vector_index.enabled = true; +``` + +A more complete runnable version lives under +[langchain4j-examples/cockroachdb-example](https://github.com/langchain4j/langchain4j-examples/blob/main/cockroachdb-example/src/main/java/CockroachDbEmbeddingStoreExample.java). + +## Known Limitations + +- C-SPANN vector indexes require CockroachDB v25.2 or later, and the + `feature.vector_index.enabled` cluster setting must be enabled. +- Vector values are sent as text and cast with `?::vector` because + CockroachDB's pgwire layer does not accept the binary format for the + `VECTOR` type. +- Hybrid (vector + full-text) query execution is not implemented yet. The + tsvector column and GIN index can be created via `createTsvectorColumn` + for use by application code or a future release. +- The Python `langchain-cockroachdb` library also ships a LangGraph + checkpointer (`CockroachDBSaver` and `AsyncCockroachDBSaver`). The + Java equivalent lives in the third-party [langgraph4j](https://github.com/langgraph4j/langgraph4j) + project as `langgraph4j-cockroachdb-saver`. langgraph4j's checkpoint + contract has no async API, so only the sync `CockroachDBSaver` is + provided; callers on JDK 21 or later can invoke it from a virtual + thread for non-blocking concurrency. diff --git a/docs/docs/integrations/embedding-stores/coherence.md b/docs/docs/integrations/embedding-stores/coherence.md new file mode 100644 index 0000000..8ae7c81 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/coherence.md @@ -0,0 +1,58 @@ +--- +sidebar_position: 9 +--- + +# Oracle Coherence + +https://coherence.community/ + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-coherence + 1.17.2-beta27 + +``` + +The `langchain4j-coherence` module has Coherence as a provided dependency as it works with various Coherence versions. +Developers should include the relevant Coherence dependency, either Community Edition or Commercial version. +Coherence CE has a groupId of `com.oracle.coherence.ce` and commercial versions have a groupId of `com.oracle.coherence`. + +For example, to use Community Edition (CE), add the Coherence BOM to the dependency management section then add Coherence as a dependency. Other Coherence modules can then be added to the project as required. + +```xml + + + + com.oracle.coherence.ce + coherence-bom + 24.09 + pom + import + + + + + + + dev.langchain4j + langchain4j-coherence + 1.17.2-beta27 + + + com.oracle.coherence.ce + coherence + + +``` + +## APIs + +- `CoherenceEmbeddingStore` +- `CoherenceChatMemoryStore` + +## Examples + +- [CoherenceEmbeddingStoreExample](https://github.com/langchain4j/langchain4j-examples/blob/main/oracle-coherence-example/src/main/java/CoherenceEmbeddingStoreExample.java) diff --git a/docs/docs/integrations/embedding-stores/couchbase.md b/docs/docs/integrations/embedding-stores/couchbase.md new file mode 100644 index 0000000..c5e7cf1 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/couchbase.md @@ -0,0 +1,160 @@ +--- +sidebar_position: 10 +--- + +# Couchbase + +https://www.couchbase.com/ + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-couchbase + 1.17.2-beta27 + +``` + +## APIs + +- `CouchbaseEmbeddingStore` + + +## Examples + +- [CouchbaseEmbeddingStoreExample](https://github.com/langchain4j/langchain4j-examples/blob/main/couchbase-example/src/main/java/CouchbaseEmbeddingStoreExample.java) + +## Couchbase Embedding Store +Couchbase langchain4j integration stores each embedding in a separate document and uses an FTS vector index to perform +queries against stored vectors. Currently, it supports storing embeddings and their metadata, as well as removing +embeddings. Filtering selected by vector search embeddings by their metadata was not supported at the moment of writing +this tutorial. Please note that the embedding store integration is still under active development and the default +configurations it comes with are not recommended for production usage. + +### Connecting To Couchbase Cluster +A builder class can be used to initialize couchbase embedding store. The following parameters are required for +initialization: +- cluster connection string +- cluster username +- cluster password +- name of the bucket in which embeddings should be stored +- name of the scope in which embeddings should be stored +- name of the collection in which embeddings should be stored +- name of an FTS vector index to be used by the embedding store +- dimensionality (length) of vectors to be stored + +The following sample code illustrates how to initialize an embedding store that connects to a locally running Couchbase +server: + +```java +CouchbaseEmbeddingStore embeddingStore = CouchbaseEmbeddingStore.builder() + .clusterUrl("localhost:8091") + .username("Administrator") + .password("password") + .bucketName("langchain4j") + .scopeName("_default") + .collectionName("_default") + .searchIndexName("test") + .dimensions(512) + .build(); +``` + +The sample source code starts a dedicated to it Couchbase server using `testcontainers` library: + +```java +CouchbaseContainer couchbaseContainer = + new CouchbaseContainer(DockerImageName.parse("couchbase:enterprise").asCompatibleSubstituteFor("couchbase/server")) + .withCredentials("Administrator", "password") + .withBucket(testBucketDefinition) + .withStartupTimeout(Duration.ofMinutes(1)); + +CouchbaseEmbeddingStore embeddingStore = CouchbaseEmbeddingStore.builder() + .clusterUrl(couchbaseContainer.getConnectionString()) + .username(couchbaseContainer.getUsername()) + .password(couchbaseContainer.getPassword()) + .bucketName(testBucketDefinition.getName()) + .scopeName("_default") + .collectionName("_default") + .searchIndexName("test") + .dimensions(384) + .build(); +``` + +### Vector Index +The embedding store uses an FTS vector index in order to perform vector similarity lookups. If provided with a name for +vector index that does not exist on the cluster, the store will attempt to create a new index with default +configuration based on the provided initialization settings. It is recommended to manually review the settings for the +created index and adjust them according to specific use cases. More information about vector search and FTS index +configuration can be found at [Couchbase Documentation](https://docs.couchbase.com/server/current/vector-search/vector-search.html). + +### Embedding Documents +The integration automatically assigns unique `UUID`-based identifiers to all stored embeddings. Here is +an example embedding document (with vector field values truncated for readability): + +```json +{ + "id": "f4831648-07ca-4c77-a031-75acb6c1cf2f", + "vector": [ + ... + 0.037255168, + -0.001608681 + ], + "text": "text", + "metadata": { + "some": "value" + }, + "score": 0 +} +``` + +These embeddings are generated with a selected by developers embedding model and resulting vector values are model-specific. + +## Storing Embeddings in Couchbase +Embeddings generated with an embedding model can be stored in couchbase using `add` and `addAll` methods of the `CouchbaseEmbeddingStore` +class: +```java +EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel(); + +TextSegment segment1 = TextSegment.from("I like football."); +Embedding embedding1 = embeddingModel.embed(segment1).content(); +embeddingStore.add(embedding1, segment1); + +TextSegment segment2 = TextSegment.from("The weather is good today."); +Embedding embedding2 = embeddingModel.embed(segment2).content(); +embeddingStore.add(embedding2, segment2); + +Thread.sleep(1000); // to be sure that embeddings were persisted +``` + +## Querying Relevant Embeddings +After adding some embeddings into the store, a query vector can be used to find relevant to it embeddings in the store. +Here, we're using the embedding model to generate a vector for the phrase "what is your favorite sport?". The obtained +vector is then being used to find the most relevant answer in the database: +```java +Embedding queryEmbedding = embeddingModel.embed("What is your favourite sport?").content(); +EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder() + .queryEmbedding(queryEmbedding) + .maxResults(1) + .build(); +EmbeddingSearchResult searchResult = embeddingStore.search(searchRequest); +EmbeddingMatch embeddingMatch = searchResult.matches().get(0); +``` + +The relevancy score and text of the selected answer can then be printed to the application output: +```java +System.out.println(embeddingMatch.score()); // 0.81442887 +System.out.println(embeddingMatch.embedded().text()); // I like football. +``` + +## Deleting Embeddings +Couchbase embedding store also supports removing embeddings by their identifiers, for example: +```java +embeddingStore.remove(embeddingMatch.id()) +``` + +Or, to remove all embeddings: +```java +embeddingStore.removeAll(); +``` diff --git a/docs/docs/integrations/embedding-stores/duckdb.md b/docs/docs/integrations/embedding-stores/duckdb.md new file mode 100644 index 0000000..f4ea2b2 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/duckdb.md @@ -0,0 +1,54 @@ +--- +sidebar_position: 11 +--- + +# DuckDB + +https://duckdb.org/ + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-community-duckdb + ${latest version here} + +``` + +## APIs + +- `DuckDBEmbeddingStore` + +## Examples + +```java +// Init Model and Store +var embeddingStore = DuckDBEmbeddingStore.inMemory(); +var embeddingModel = new AllMiniLmL6V2QuantizedEmbeddingModel(); + +//Create embeddings +Stream.of( + "DuckDB is an amazing database engine!", + "Python really lack of typing :D") + .forEach(text -> { + var segment = TextSegment.from(text); + var embedding = embeddingModel.embed(segment).content(); + embeddingStore.add(embedding, segment); + }); + + +// Search request +var queryEmbedding = embeddingModel.embed("What is the best database engine").content(); +var request = EmbeddingSearchRequest.builder() + .queryEmbedding(queryEmbedding) + .maxResults(1) + .build(); + +var relevant = embeddingStore.search(request); +EmbeddingMatch embeddingMatch = relevant.matches().get(0); + +// Show results +System.out.println(embeddingMatch.score()); // 0.8416415629618381 +System.out.println(embeddingMatch.embedded().text()); //DuckDB is an amazing database engine! +``` diff --git a/docs/docs/integrations/embedding-stores/elasticsearch.md b/docs/docs/integrations/embedding-stores/elasticsearch.md new file mode 100644 index 0000000..6f126bf --- /dev/null +++ b/docs/docs/integrations/embedding-stores/elasticsearch.md @@ -0,0 +1,274 @@ +--- +sidebar_position: 12 +--- + +# Elasticsearch + +https://www.elastic.co/ + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-elasticsearch + 1.17.2-beta27 + +``` + +## Overview + +The `langchain4j-elasticsearch` module provides integration with Elasticsearch as an embedding store and content +retriever. + +It comes with two main classes: + +- [`ElasticsearchEmbeddingStore`](#elasticsearchembeddingstore): an implementation of the `EmbeddingStore` interface + that uses Elasticsearch to store and retrieve embeddings. +- [`ElasticsearchContentRetriever`](#elasticsearchcontentretriever): an implementation of the `ContentRetriever` + interface that uses Elasticsearch to retrieve relevant documents based on vector similarity search. + +Both classes need an [Elasticsearch Client](https://www.elastic.co/docs/reference/elasticsearch/clients/java) to +connect to the Elasticsearch server. + +```java +String apiKey = "VnVhQ2ZHY0JDZGJrU..."; +ElasticsearchClient client = ElasticsearchClient.of(ec -> ec + .host("https://localhost:9200") + .apiKey(apiKey)); +``` + +**Note:** + +> See the [Elasticsearch documentation](https://www.elastic.co/docs/reference/elasticsearch/clients/java/setup/connecting) +> on how to create an ElasticsearchClient instance. + +## ElasticsearchEmbeddingStore + +To create the `ElasticsearchEmbeddingStore` instance, you need to provide an `ElasticsearchClient`: + +```java +ElasticsearchEmbeddingStore store = ElasticsearchEmbeddingStore.builder() + .client(client) + .build(); +``` + +It comes with the following options: + +* `indexName`: the name of the Elasticsearch index to use. Default is `default`. +* `configuration`: the `ElasticsearchConfiguration` to use. Default is `ElasticsearchConfigurationKnn`. + +The previous code is equivalent to: + +```java +ElasticsearchEmbeddingStore store = ElasticsearchEmbeddingStore.builder() + .client(client) + .configuration(ElasticsearchConfigurationKnn.builder().build()) + .indexName("default") + .build(); +``` + +## ElasticsearchContentRetriever + +A ContentRetriever needs an embedding model: + +```java +EmbeddingModel embeddingModel = new AllMiniLmL6V2QuantizedEmbeddingModel(); +``` + +To create an `ElasticsearchContentRetriever` instance, you need to provide the `ElasticsearchClient` and +the `EmbeddingModel`: + +```java +ElasticsearchContentRetriever contentRetriever = ElasticsearchContentRetriever.builder() + .client(client) + .embeddingModel(embeddingModel) + .build(); +``` + +It comes with the following options: + +* `configuration`: the `ElasticsearchConfiguration` to use (see [below](#elasticsearchconfiguration)). Default is `ElasticsearchConfigurationKnn`. +* `indexName`: the name of the Elasticsearch index to use. Default is `default`. Index will be created automatically + if not exists. +* `maxResults`: the maximum number of results to retrieve. Default is `3`. +* `minScore`: the minimum score threshold for retrieved results. Default is `0.0`. +* `filter`: a `Filter` to apply during retrieval if any. Default is `null`. + +The previous code is equivalent to: + +```java +ElasticsearchContentRetriever contentRetriever = ElasticsearchContentRetriever.builder() + .client(client) + .embeddingModel(embeddingModel) + .configuration(ElasticsearchConfigurationKnn.builder().build()) + .indexName("default") + .maxResults(3) + .minScore(0.0) + .filter(null) + .build(); +``` + +## ElasticsearchConfiguration + +An `ElasticsearchConfiguration` defines how the embedding store or content retriever will interact with the +Elasticsearch server. You can create your own configuration by implementing the `ElasticsearchConfiguration` interface, +or use one of the provided implementations: + +- [`ElasticsearchConfigurationKnn`](#elasticsearchconfigurationknn): uses approximate [kNN queries](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-knn-query.html) + (default). +- [`ElasticsearchConfigurationScript`](#elasticsearchconfigurationscript): uses [scriptScore queries](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-script-score-query.html). + Note that this implementation is using cosine similarity. +- [`ElasticsearchConfigurationFullText`](#elasticsearchconfigurationfulltext): uses [full text search](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-match-query) + (for content retriever only). +- [`ElasticsearchConfigurationHybrid`](#elasticsearchconfigurationhybrid): uses [hybrid search](https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search) + (for content retriever only, requires paid license). It combines a kNN vector query with a full text query. + +To create a configuration instance, you can use the builder provided by each implementation. For example: + +```java +ElasticsearchConfiguration configuration = ElasticsearchConfigurationKnn.builder().build(); +``` + +### ElasticsearchConfigurationKnn + +The `ElasticsearchConfigurationKnn` uses approximate kNN queries to perform vector similarity search. + +It is the default configuration used by both [`ElasticsearchEmbeddingStore`](#elasticsearchembeddingstore) +and [`ElasticsearchContentRetriever`](#elasticsearchcontentretriever). + +To create an instance, you can use the builder: + +```java +ElasticsearchConfiguration configuration = ElasticsearchConfigurationKnn.builder().build(); +``` + +It comes with the following options: + +* `numCandidates`: the number of candidate neighbors to consider during the search. Default is `null`, meaning using + the default Elasticsearch value. +* `includeVectorResponse`: whether to include vector fields in the search response. Default is `false`. + +> **Note:** +> From version 9.2 of the elasticsearch server, vector fields are excluded from the response by default. To include +> vector fields in the responses (not recommended), set the `includeVectorResponse` in the builder: +> +> ```java +> ElasticsearchConfigurationKnn configuration = ElasticsearchConfigurationKnn.builder() +> .includeVectorResponse(true) +> .build(); +> ``` + +### ElasticsearchConfigurationScript + +The `ElasticsearchConfigurationScript` uses scriptScore queries to perform vector similarity search. Note that this +implementation is using cosine similarity. + +It is available for both [`ElasticsearchEmbeddingStore`](#elasticsearchembeddingstore) +and [`ElasticsearchContentRetriever`](#elasticsearchcontentretriever). + +To create an instance, you can use the builder: + +```java +ElasticsearchConfiguration configuration = ElasticsearchConfigurationScript.builder().build(); +``` + +It comes with the following options: + +* `includeVectorResponse`: whether to include vector fields in the search response. Default is `false`. + +> **Note:** +> From version 9.2 of the elasticsearch server, vector fields are excluded from the response by default. To include +> vector fields in the responses (not recommended), set the `includeVectorResponse` in the builder: +> +> ```java +> ElasticsearchConfiguration configuration = ElasticsearchConfigurationScript.builder() +> .includeVectorResponse(true) +> .build(); +> ``` + +### ElasticsearchConfigurationFullText + +The `ElasticsearchConfigurationFullText` uses full text search to retrieve relevant documents. + +It is available [`ElasticsearchContentRetriever`](#elasticsearchcontentretriever) only. + +To create an instance, you can use the builder: + +```java +ElasticsearchConfiguration configuration = ElasticsearchConfigurationFullText.builder().build(); +``` + +### ElasticsearchConfigurationHybrid + +The `ElasticsearchConfigurationHybrid` uses hybrid search to combine a kNN vector query with a full text query. Note +that hybrid search requires an elasticsearch enterprise license or a trial. + +It is available [`ElasticsearchContentRetriever`](#elasticsearchcontentretriever) only. + +To create an instance, you can use the builder: + +```java +ElasticsearchConfiguration configuration = ElasticsearchConfigurationHybrid.builder().build(); +``` + +It comes with the following options: + +* `numCandidates`: the number of candidate neighbors to consider during the search. Default is `null`, meaning using + the default Elasticsearch value. +* `includeVectorResponse`: whether to include vector fields in the search response. Default is `false`. + +> **Note:** +> From version 9.2 of the elasticsearch server, vector fields are excluded from the response by default. To include +> vector fields in the responses (not recommended), set the `includeVectorResponse` in the builder: +> +> ```java +> ElasticsearchConfiguration configuration = ElasticsearchConfigurationHybrid.builder() +> .includeVectorResponse(true) +> .build(); +> ``` + +### Creating Custom Configurations + +You can create your own Elasticsearch configuration by implementing the `ElasticsearchConfiguration` interface. For example: + +```java +public class MyElasticsearchConfiguration implements ElasticsearchConfiguration { + @Override + SearchResponse vectorSearch( + ElasticsearchClient client, + String indexName, + EmbeddingSearchRequest embeddingSearchRequest) { + // Your optional custom vector search implementation here + } + + @Override + SearchResponse fullTextSearch( + ElasticsearchClient client, + String indexName, + String textQuery) { + // Your optional custom full text search implementation here + } + + @Override + SearchResponse hybridSearch( + ElasticsearchClient client, + String indexName, + EmbeddingSearchRequest embeddingSearchRequest, + String textQuery) { + // Your optional custom hybrid search implementation here + } +} +``` + +Please note that you can implement only the methods relevant to your use case: + +* `vectorSearch` for vector similarity search (used by both `ElasticsearchEmbeddingStore` and `ElasticsearchContentRetriever`). +* `fullTextSearch` for full text search (used by `ElasticsearchContentRetriever` only). +* `hybridSearch` for hybrid search (used by `ElasticsearchContentRetriever` only). + +## Examples + +- [ElasticsearchEmbeddingStoreExample](https://github.com/langchain4j/langchain4j-examples/blob/main/elasticsearch-example/src/main/java/ElasticsearchEmbeddingStoreExample.java) +- [ElasticsearchEmbeddingStoreWithScriptExample](https://github.com/langchain4j/langchain4j-examples/blob/main/elasticsearch-example/src/main/java/ElasticsearchEmbeddingStoreWithScriptExample.java) diff --git a/docs/docs/integrations/embedding-stores/hazelcast.md b/docs/docs/integrations/embedding-stores/hazelcast.md new file mode 100644 index 0000000..e40406f --- /dev/null +++ b/docs/docs/integrations/embedding-stores/hazelcast.md @@ -0,0 +1,181 @@ +--- +sidebar_position: 34 +--- + +# Hazelcast + +[Hazelcast](https://hazelcast.com/) is a distributed in-memory data grid and computing platform. +LangChain4j integrates with Hazelcast through two modules, split so the open-source path +stays free of any Enterprise/licensing requirement: + +- **`langchain4j-community-hazelcast`** (open source) — provides `HazelcastChatMemoryStore`, a + `ChatMemoryStore` backed by a Hazelcast `IMap`. Runs against the open-source Community Edition + with no license. +- **`langchain4j-community-hazelcast-enterprise`** (requires Hazelcast Enterprise) — provides + `HazelcastEmbeddingStore` (vector search via `VectorCollection`) and + `HazelcastCPMapChatMemoryStore` (a strongly-consistent, CP-Subsystem-backed chat memory store). + This module re-exports `langchain4j-community-hazelcast`, so Enterprise consumers also get the + `IMap`-based store from the single dependency. + +## Maven Dependency + +### Open source (Chat Memory Store) + +```xml + + dev.langchain4j + langchain4j-community-hazelcast + ${latest version here} + +``` + +The Hazelcast dependency is `provided`, so add the edition you run against. By default this is the +open-source Community Edition: + +```xml + + com.hazelcast + hazelcast + 5.7.0 + +``` + +### Hazelcast Enterprise (Embedding Store + CP chat memory) + +```xml + + dev.langchain4j + langchain4j-community-hazelcast-enterprise + ${latest version here} + +``` + +Or use the BOM to manage versions consistently: + +```xml + + + + dev.langchain4j + langchain4j-community-bom + ${latest version here} + pom + import + + + +``` + +:::note +`HazelcastEmbeddingStore` and `HazelcastCPMapChatMemoryStore` require **Hazelcast Enterprise**. +`com.hazelcast:hazelcast-enterprise` is not on Maven Central — it is pulled in transitively by +`langchain4j-community-hazelcast-enterprise`, but you must add the Hazelcast release repository and a +valid Enterprise license key: + +```xml + + + hazelcast-release + Hazelcast Release Repository + https://repository.hazelcast.com/release/ + + +``` + +Provide the license key via `config.setLicenseKey(...)` or the `HZ_LICENSEKEY` environment variable. +`HazelcastCPMapChatMemoryStore` additionally requires the CP Subsystem to be enabled; it fails fast +otherwise. +::: + +## Chat Memory Store + +`HazelcastChatMemoryStore` (open source) stores each chat memory as a JSON-serialised list of +`ChatMessage`s in a Hazelcast `IMap`. The `HazelcastInstance` you supply may be an embedded member or +a thin client — the builder does not distinguish. + +```java +// Embedded member +HazelcastInstance hz = Hazelcast.newHazelcastInstance(new Config()); + +ChatMemoryStore store = HazelcastChatMemoryStore.builder() + .hazelcastInstance(hz) + .name("chatMemory") // optional, defaults to "chatMemory" + .build(); +``` + +```java +// Client connecting to an external cluster +ClientConfig clientConfig = new ClientConfig(); +clientConfig.getNetworkConfig().addAddress("hazelcast-host:5701"); +HazelcastInstance hzClient = HazelcastClient.newHazelcastClient(clientConfig); + +ChatMemoryStore store = HazelcastChatMemoryStore.builder() + .hazelcastInstance(hzClient) + .build(); +``` + +You can also wrap a pre-configured `IMap` directly with +`HazelcastChatMemoryStore.create(IMap)`. + +### Strongly-consistent variant (Enterprise) + +`HazelcastCPMapChatMemoryStore` (Enterprise) is an alternative backed by a `CPMap` in the +[CP Subsystem](https://docs.hazelcast.com/hazelcast/latest/cp-subsystem/cp-subsystem). It is +**linearizable** (strongly consistent, Raft-backed), avoiding lost updates when the same `memoryId` +is updated concurrently. The CP Subsystem must be enabled on the instance. + +```java +Config config = new Config(); +config.getCPSubsystemConfig().setCPMemberCount(3); // CP Subsystem must be enabled +HazelcastInstance hz = Hazelcast.newHazelcastInstance(config); + +ChatMemoryStore store = HazelcastCPMapChatMemoryStore.builder() + .hazelcastInstance(hz) + .name("chatMemory") + .build(); +``` + +Trade-offs versus the `IMap` store: a `CPMap` is **not partitioned** (it must fit within each CP +member's RAM, default 100 MB total) and has **no TTL/eviction**. It suits many small conversations; +prefer the `IMap`-based store for unbounded history across a very large user population. + +## Embedding Store + +`HazelcastEmbeddingStore` (Enterprise) is backed by a Hazelcast `VectorCollection`. The vector index +dimension must match the embedding model in use; the metric defaults to `COSINE`. + +```java +HazelcastInstance hz = Hazelcast.newHazelcastInstance(new Config()); + +EmbeddingStore store = HazelcastEmbeddingStore.builder() + .hazelcastInstance(hz) + .collectionName("embeddings") // optional, defaults to "embeddings" + .dimension(384) // required, must match the embedding model + .metric(Metric.COSINE) // optional, defaults to COSINE + .build(); +``` + +You can also wrap a pre-configured `VectorCollection` with +`HazelcastEmbeddingStore.create(VectorCollection)`. The relevance score +returned by `search(...)` is Hazelcast's already-normalised COSINE score, used directly. + +### Limitations + +- `removeAll(Filter)` is **not supported** — `VectorCollection` has no server-side predicate delete; + it throws `UnsupportedFeatureException`. Removal by id, `removeAll(Collection)` and + `removeAll()` are supported. +- Metadata filtering during search is **not** performed server-side. If `EmbeddingSearchRequest` + carries a filter, it is applied **client-side after retrieval** (a warning is logged), which may + return fewer than `maxResults` matches. + +## APIs + +- `HazelcastChatMemoryStore` — `IMap`-based (AP), open source +- `HazelcastCPMapChatMemoryStore` — `CPMap`-based (CP, linearizable), Enterprise +- `HazelcastEmbeddingStore` — `VectorCollection`-based vector store, Enterprise + +## Examples + +- [HazelcastChatMemoryStoreExample](https://github.com/langchain4j/langchain4j-examples/blob/main/hazelcast-example/src/main/java/HazelcastChatMemoryStoreExample.java) +- [HazelcastCPMapChatMemoryStoreExample](https://github.com/langchain4j/langchain4j-examples/blob/main/hazelcast-example/src/main/java/HazelcastCPMapChatMemoryStoreExample.java) +- [HazelcastEmbeddingStoreExample](https://github.com/langchain4j/langchain4j-examples/blob/main/hazelcast-example/src/main/java/HazelcastEmbeddingStoreExample.java) diff --git a/docs/docs/integrations/embedding-stores/hibernate.md b/docs/docs/integrations/embedding-stores/hibernate.md new file mode 100644 index 0000000..61454f8 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/hibernate.md @@ -0,0 +1,540 @@ +--- +sidebar_position: 19 +--- + +# Hibernate + +LangChain4j integrates seamlessly with [Hibernate](https://github.com/hibernate/hibernate-orm), allowing developers to store +and query vector embeddings directly in all databases that Hibernate supports. This integration is ideal for applications like semantic search, +RAG, and more. + +## Maven Dependency + +```xml + + + dev.langchain4j + langchain4j-hibernate + 1.17.2-beta27 + +``` + +## Gradle Dependency + +```implementation 'dev.langchain4j:langchain4j-hibernate:1.17.2-beta27'``` + +## APIs + +- `HibernateEmbeddingStore` + +## Parameter Summary + +### Generic store + +When using just the `EmbeddingStore` API without wanting to worry about Hibernate specifics, like entity class definition +and the configuration of Hibernate, this kind of store is preferred. + +To configure it, use either `HibernateEmbeddingStore.dynamicBuilder()` or `HibernateEmbeddingStore.dynamicDatasourceBuilder()`. + +| Plain Java Property | Description | Default Value | Required/Optional | +|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `datasource` | The `DataSource` object used for database connections. Available only in the `HibernateEmbeddingStore.dynamicDatasourceBuilder()` builder variant. If not provided, `jdbcUrl`, `user` and `password` must be provided individually in the `HibernateEmbeddingStore.dynamicBuilder()` builder variant. | None | Required if `jdbcUrl`, `user` and `password` are not provided individually. | +| `jdbcUrl` | JDBC URL of the database server. Required if `DataSource` and `host`, `port`, `database` are not provided. Available only in the `HibernateEmbeddingStore.dynamicBuilder()` builder variant. | None | Required if `DataSource` or `host`, `port`, `database` are not provided | +| `host` | Hostname of the database server. Required if neither `DataSource` or `jdbcUrl` are not provided. Available only in the `HibernateEmbeddingStore.dynamicBuilder()` builder variant. | None | Required if neither `DataSource` or `jdbcUrl` are not provided | +| `port` | Port number of the database server. Required if neither `DataSource` or `jdbcUrl` are not provided. Available only in the `HibernateEmbeddingStore.dynamicBuilder()` builder variant. | None | Required if neither `DataSource` or `jdbcUrl` are not provided | +| `database` | Name of the database to connect to. Required if neither `DataSource` or `jdbcUrl` are not provided. Available only in the `HibernateEmbeddingStore.dynamicBuilder()` builder variant. | None | Required if neither `DataSource` or `jdbcUrl` are not provided | +| `databaseKind` | The database kind. Required if `DataSource` is provided or the kind can't be inferred from the `jdbcUrl`. | None | Required if `DataSource` is provided or the kind can't be inferred from the `jdbcUrl` | +| `user` | Username for database authentication. Required if `DataSource` is not provided. Available only in the `HibernateEmbeddingStore.dynamicBuilder()` builder variant. | None | Required if `DataSource` is not provided | +| `password` | Password for database authentication. Required if `DataSource` is not provided. Available only in the `HibernateEmbeddingStore.dynamicBuilder()` builder variant. | None | Required if `DataSource` is not provided | +| `table` | The name of the database table used for storing embeddings. | None | Required | +| `dimension` | The dimensionality of the embedding vectors. This should match the embedding model being used. Use `embeddingModel.dimension()` to dynamically set it. | None | Required | +| `createIndex` | Specifies whether to automatically create an index for the vector embedding. | `false` | Optional | +| `indexType` | The database specific type of index to use e.g. `ivfflat`, `hnsw`. An IVFFlat index divides vectors into lists, and then searches a subset of those lists closest to the query vector. It has faster build times and uses less memory than HNSW but has lower query performance (in terms of speed-recall tradeoff). Should use [IVFFlat](https://github.com/pgvector/pgvector#ivfflat) index. | None | Optional. Defaults to the preferred index type e.g. `ivfflat` on PostgreSQL | +| `indexOptions` | The options to configure on the index for the vector embedding. | None | When Required: If `createIndex` is `true` and the index type is `ivfflat`, on PostgreSQL the `lists = 1` option must be provided and must be greater than zero. Otherwise, the program will throw an exception during table initialization. When Optional: If `createIndex` is `false`, this property is ignored and doesn’t need to be set. | +| `createTable` | Specifies whether to automatically create the embeddings table. | `false` | Optional | +| `dropTableFirst` | Specifies whether to drop the table before recreating it (useful for tests). | `false` | Optional | +| `distanceFunction` | The distance function to use for vector search. Supports varies based on database:
  • **COSINE**
  • **EUCLIDEAN**
  • **EUCLIDEAN_SQUARED**
  • **MANHATTAN**
  • **INNER_PRODUCT**
  • **NEGATIVE_INNER_PRODUCT**
  • **HAMMING**
  • **JACCARD**
| `COSINE` | Optional. If not set, a default configuration is used with `COSINE`. | + +### Entity store + +To make use of an existing Hibernate entity model in the `EmbeddingStore` API, or to apply data model customizations, +the entity store is preferred. + +To configure it, use either `HibernateEmbeddingStore.builder()`. + +| Plain Java Property | Description | Default Value | Required/Optional | +|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `sessionFactory` | The `SessionFactory` object where the `entityClass` is part of. | None | Required | +| `databaseKind` | The database kind. Required if kind can't be inferred from the Hibernate ORM dialect. | None | Required if kind can't be inferred from the Hibernate ORM dialect | +| `entityClass` | Specifies the entity class of the `SessionFactory` to use for the `EmbeddingStore`. | None | Required | +| `embeddingAttributeName` | Specifies the name of the entity attribute that represents the vector embedding. | None | Optional. If not set, the entity is scanned for an attribute annotated with `@EmbeddingVector` | +| `embeddedTextAttributeName` | Specifies the name of the entity attribute that represents the source text of the vector embedding. | None | Optional. If not set, the entity is scanned for an attribute annotated with `@EmbeddedText` | +| `unmappedMetadataAttributeName` | Specifies the name of the entity attribute that represents the JSON column where unmapped metadata is stored. | None | Optional. If not set, the entity is scanned for an attribute annotated with `@UnmappedMetadata` | +| `metadataAttributeNames` | Specifies the names of the entity attributes that are explicitly mapped to text metadata. | None | Optional. If not set, the entity is scanned for an attribute annotated with `@MetadataAttribute` | +| `distanceFunction` | The distance function to use for vector search. Supports varies based on database:
  • **COSINE**
  • **EUCLIDEAN**
  • **EUCLIDEAN_SQUARED**
  • **MANHATTAN**
  • **INNER_PRODUCT**
  • **NEGATIVE_INNER_PRODUCT**
  • **HAMMING**
  • **JACCARD**
| `COSINE` | Optional. If not set, the entity is scanned for an attribute annotated with `@EmbeddingVector` and the `distance` value is used, or if that is missing, the default configuration is used with `COSINE`. | + +## Examples + +To demonstrate the capabilities, you can e.g. use a Dockerized PostgreSQL setup. It leverages Testcontainers to +run PostgreSQL with PGVector. + +#### Quick Start with Docker + +To quickly set up a PostgreSQL instance with the PGVector extension, you can use the following Docker command: + +``` +docker run --rm --name langchain4j-postgres-test-container -p 5432:5432 -e POSTGRES_USER=my_user -e POSTGRES_PASSWORD=my_password pgvector/pgvector +``` + +#### Explanation of the Command: + +- ```docker run```: Runs a new container. +- ```--rm```: Automatically removes the container after it stops, ensuring no residual data. +- ```--name langchain4j-postgres-test-container```: Names the container langchain4j-postgres-test-container for easy + identification. +- ```-p 5432:5432```: Maps port 5432 on your local machine to port 5432 in the container. +- ```-e POSTGRES_USER=my_user```: Sets the PostgreSQL username to my_user. +- ```-e POSTGRES_PASSWORD=my_password```: Sets the PostgreSQL password to my_password. +- ```pgvector/pgvector```: Specifies the Docker image to use, pre-configured with the PGVector extension. + +Here are two code examples showing how to create a `HibernateEmbeddingStore`. The first uses only the required parameters, +while the second configures all available parameters. + +1. Only Required Parameters + +```java +HibernateEmbeddingStore embeddingStore = HibernateEmbeddingStore.dynamicBuilder() + .databaseKind(DatabaseKind.POSTGRESQL) // Required: The database kind + .host("localhost") // Required: Host of the database server + .port(5432) // Required: Port of the database server + .database("postgres") // Required: Database name + .user("my_user") // Required: Database user + .password("my_password") // Required: Database password + .table("my_embeddings") // Required: Table name to store embeddings + .dimension(embeddingModel.dimension()) // Required: Dimension of embeddings + .build(); +``` + +2. All Parameters Set + +In this variant, we include all the commonly used optional parameters like createIndex, indexOptions, +createTable, dropTableFirst, and distanceFunction. Adjust these values as needed: + + ```java +HibernateEmbeddingStore embeddingStore = HibernateEmbeddingStore.dynamicBuilder() + // Required parameters + .databaseKind(DatabaseKind.POSTGRESQL) + .host("localhost") + .port(5432) + .database("postgres") + .user("my_user") + .password("my_password") + .table("my_embeddings") + .dimension(embeddingModel.dimension()) + + // Optional parameters + .createIndex(true) // Enable vector index creation + .indexType("ivfflat") // Index type IVFFlat + .indexOptions("lists = 100") // Number of lists for IVFFlat index + .createTable(true) // Automatically create the table if it doesn’t exist + .dropTableFirst(false) // Don’t drop the table first (set to true if you want a fresh start) + .distanceFunction(DistanceFunction.MANHATTEN) // Use MANHATTAN distance function for vector search + + .build(); +``` + +Use the first example if you just want the minimal configuration to get started quickly. +The second example shows how you can leverage all available builder parameters for more control and customization. + +Don't forget to close the `HibernateEmbeddingStore` when you don't need it anymore, to close the underlying Hibernate resources. + +#### Custom Hibernate entity + +When you want to customize the data model or want to reuse an existing entity as source for the `EmbeddingStore`, +you can make use of the annotations `@EmbeddingVector`, `@EmbeddedText`, `@UnmappedMetadata` and `@MetadataAttribute` to mark the +entity attributes to use by the Hibernate `EmbeddingStore` implementation. + +```java +@Entity +public class MyEmbeddingEntity { + @Id + UUID id; + @EmbeddingVector + @Array(length = 384) // The dimension of the embedding vector based on the embedding model + float[] embedding; + @EmbeddedText + String text; + @UnmappedMetadata + Map metadata; // Can be either a Map or a String + + @MetadataAttribute + String mimeType; // Explicitly mapped. Synchronizes TextSegment#metadata with this attribute + @MetadataAttribute + String fileName; // Explicitly mapped. Synchronizes TextSegment#metadata with this attribute +} +``` + +The builder will then look for these annotations and derive the attribute names. + +```java +HibernateEmbeddingStore embeddingStore = HibernateEmbeddingStore.builder() + .sessionFactory(sessionFactory) // Required: The SessionFactory containing your entity class + .entityClass(MyEmbeddingEntity.class) // Required: The embedding entity class + .build(); +``` + +Alternatively, if annotating the entity model is not desired, the attribute names can also be provided explicitly. + +```java +HibernateEmbeddingStore embeddingStore = HibernateEmbeddingStore.builder() + .sessionFactory(sessionFactory) + .entityClass(MyEmbeddingEntity.class) + .embeddingAttributeName("embedding") + .embeddedTextAttributeName("text") + .unmappedMetadataAttributeName("metadata") + .metadataAttributeNames("mimeType", "fileName") + .build(); +``` + +Metadata can also be nested within `@OneToOne`, `@ManyToOne` or `@Embedded` attributes that are also annotated with `@MetadataAttribute`, +or by specifying an explicit attribute path with the `.` (dot) separator. + +```java +@Entity +public class Book { + @Id + private Long id; + private String title; + private String content; + @MetadataAttribute + @Embedded + private BookDetails details = new BookDetails(); + @MetadataAttribute + @ManyToOne(fetch = FetchType.LAZY) + private Author author; + + @EmbeddingVector + @Array(length = 384) + private float[] embedding; + @UnmappedMetadata + private Map metadata; +} +@Entity +public class Author { + @Id + @MetadataAttribute + @GeneratedValue + private Long id; + private String firstname; + private String lastname; +} +@Embeddable +public class BookDetails { + @MetadataAttribute + private String language; + private String abstractText; +} +``` + +The equivalent attribute paths are `details.language` and `author.id`, which are then available for filtering, +by specifying these paths as metadata keys, e.g. + +```java +MetadataFilterBuilder.metadataKey("details.language").isEqualTo("English") +``` + +or + +```java +MetadataFilterBuilder.metadataKey("author.id").isEqualTo(2L) +``` + +Alternatively, the `HibernateEmbeddingStore` API also provides `search` methods that allow you to use the type-safe Hibernate ORM `Restriction` API. + +```java +HibernateEmbeddingStore embeddingStore = embeddingStore(); +embeddingStore.search( + embedding, + Path.from(Book.class) + .to(Book_.details) + .to(BookDetails_.language) + .equalTo("English")); +``` + +or + +```java +HibernateEmbeddingStore embeddingStore = embeddingStore(); +embeddingStore.search( + embedding, + Path.from(Book.class) + .to(Book_.author) + .to(Author_.id) + .equalTo(2L)); +``` + +## Complete RAG Example with Hibernate + +This section demonstrates how to build a complete Retrieval-Augmented Generation (RAG) system using the Hibernate integration +with PostgreSQL with the PGVector extension for semantic search. + +### Overview + +A RAG system consists of two main stages: +1. **Indexing Stage (Offline)**: Load documents, split into chunks, generate embeddings, and store in pgvector +2. **Retrieval Stage (Online)**: Embed user query, search similar chunks, inject context into LLM prompt + +### Prerequisites + +Ensure you have a PostgreSQL instance with PGVector running (see Docker setup above). + +### 1. Document Ingestion (Indexing Stage) + +This example shows how to load documents, split them into chunks, and store embeddings in pgvector: + +```java +import dev.langchain4j.data.document.Document; +import dev.langchain4j.data.document.DocumentParser; +import dev.langchain4j.data.document.DocumentSplitter; +import dev.langchain4j.data.document.parser.apache.pdfbox.ApachePdfBoxDocumentParser; +import dev.langchain4j.data.document.splitter.DocumentSplitters; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel; +import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.EmbeddingStoreIngestor; + +import static dev.langchain4j.data.document.loader.FileSystemDocumentLoader.loadDocument; + +// Load document (PDF, TXT, etc.) +Document document = loadDocument("/path/to/document.pdf", new ApachePdfBoxDocumentParser()); + +// Split document into smaller chunks +// 300 tokens per chunk, 50 tokens overlap for context continuity +DocumentSplitter splitter = DocumentSplitters.recursive(300, 50); + +// Create embedding model (384 dimensions for AllMiniLmL6V2) +EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel(); + +// Create pgvector embedding store +HibernateEmbeddingStore embeddingStore = HibernateEmbeddingStore.dynamicBuilder() + .databaseKind(DatabaseKind.POSTGRESQL) + .host("localhost") + .port(5432) + .database("postgres") + .user("my_user") + .password("my_password") + .table("document_embeddings") + .dimension(embeddingModel.dimension()) // 384 for AllMiniLmL6V2 + .build(); + +// Ingest: split document, generate embeddings, and store in pgvector +EmbeddingStoreIngestor.builder() + .documentSplitter(splitter) + .embeddingModel(embeddingModel) + .embeddingStore(embeddingStore) + .build() + .ingest(document); + +System.out.println("Document ingested successfully!"); +``` + +### 2. Querying (Retrieval Stage) + +This example shows how to query the RAG system with a user question: + +```java +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.openai.OpenAiChatModel; +import dev.langchain4j.store.embedding.EmbeddingMatch; + +import java.util.List; +import java.util.stream.Collectors; + +// User's question +String question = "What is the refund policy?"; + +// Generate embedding for the question +Embedding questionEmbedding = embeddingModel.embed(question).content(); + +// Search for the most similar text segments (top 3 results) +EmbeddingSearchResult result = embeddingStore.search( + EmbeddingSearchRequest.builder() + .queryEmbedding(questionEmbedding) + .maxResults(3) // Retrieve top 3 most similar chunks + .build() +); + +// Build context from retrieved segments +String context = result.matches().stream() + .map(match -> match.embedded().text()) + .collect(Collectors.joining("\n\n")); + +// Create prompt with retrieved context +String promptWithContext = String.format(""" + Answer the question based on the following context. + If the context doesn't contain relevant information, say "I don't have enough information to answer." + + Context: + %s + + Question: %s + + Answer: + """, context, question); + +// Send to LLM with context +ChatModel chatModel = OpenAiChatModel.builder() + .apiKey(System.getenv("OPENAI_API_KEY")) + .modelName("gpt-4") + .build(); + +String answer = chatModel.generate(promptWithContext); +System.out.println("Answer: " + answer); +``` + +### Production Considerations + +Based on real-world usage, here are important considerations for production deployments: + +#### 1. Connection Pooling +For production environments, use a `DataSource` with connection pooling instead of individual connection parameters: + +```java +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +HikariConfig config = new HikariConfig(); +config.setJdbcUrl("jdbc:postgresql://localhost:5432/postgres"); +config.setUsername("my_user"); +config.setPassword("my_password"); +config.setMaximumPoolSize(10); + +HikariDataSource dataSource = new HikariDataSource(config); + +EmbeddingStore embeddingStore = HibernateEmbeddingStore.dynamicDatasourceBuilder() + .databaseKind(DatabaseKind.POSTGRESQL) + .datasource(dataSource) + .table("document_embeddings") + .dimension(384) + .build(); +``` + +#### 2. Index Optimization +For large datasets (>100k embeddings), enable IVFFlat indexing on PostgreSQL to improve query performance: + +```java +HibernateEmbeddingStore embeddingStore = HibernateEmbeddingStore.dynamicBuilder() + // ... other config ... + .createIndex(true) + .indexOptions("lists = 100") // Adjust based on dataset size + .build(); +``` + +**Note**: Index creation can take time on large datasets. Balance between query speed and index build time. +**Note**: Index maintenance can slow down data ingestion, so consider dropping and recreating indexes when ingesting big amounts of data. + +#### 3. Chunk Size Tuning +Experiment with different chunk sizes based on your use case: +- **Smaller chunks (200-300 tokens)**: Better precision, more specific answers +- **Larger chunks (500-800 tokens)**: More context, but may reduce relevance + +#### 4. Error Handling +Always handle database connection failures gracefully: + +```java +try { + embeddingStore.add(embedding, textSegment); +} catch (Exception e) { + logger.error("Failed to store embedding", e); + // Implement retry logic or fallback behavior +} +``` + +#### 5. Custom Hibernate entity DDL +When using a custom Hibernate entity, you are in charge of managing the DDL. +Consider creating an `import.sql` file to create indexes, e.g. for PostgreSQL: + +```sql +create index if not exists my_entity_ivfflat_index + on my_entity using ivfflat(embedding vector_cosine_ops) with (lists = 1); +``` + +Refer to [Hibernate ORM documentation](https://docs.hibernate.org/orm/7.2/userguide/html_single/) for details about the configuration of `SessionFactory`. + +Vector indexes for other databases have different syntax and options. Refer to the documentation of the respective database provider for details. + +##### DB2 + +See the [vector index article](https://community.ibm.com/community/user/blogs/christian-garcia-arellano/2025/10/04/vector-indexes-in-db2-an-early-preview) +for details. + +```sql +create vector index my_entity_vector_index + on my_entity(embedding) with distance cosine; +``` + +##### MariaDB + +See the [`create index` statement documentation](https://mariadb.com/docs/server/reference/sql-statements/data-definition/create/create-index) +for details. + +```sql +create vector index if not exists my_entity_vector_index + on my_entity(embedding) distance=cosine; +``` + +##### MySQL + +MySQL HeatWave [creates indexes automatically](https://dev.mysql.com/doc/heatwave/en/mys-hw-genai-vector-index-creation.html) and doesn't require a manual index creation. + +##### PostgreSQL + +See the [pgvector documentation](https://github.com/pgvector/pgvector?tab=readme-ov-file#indexing) for details. + +```sql +create index if not exists my_entity_ivfflat_index + on my_entity using ivfflat(embedding vector_cosine_ops) with (lists = 1); +``` + +##### CockroachDB + +See the [CockroachDB documentation]([https://github.com/pgvector/pgvector?tab=readme-ov-file#indexing](https://www.cockroachlabs.com/docs/v26.2/vector-indexes)) for details. + +```sql +create vector index if not exists my_entity_ivfflat_index + on my_entity (embedding vector_cosine_ops); +``` + +##### Oracle + +See the [`create index` statement documentation](https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/create-vector-index.html) +for details. + +```sql +create vector index my_entity_vector_index + on my_entity(embedding) organization neighbor partitions with distance cosine; +``` + +##### SQL Server + +See the [`create vector index` statement documentation](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-vector-index-transact-sql?view=sql-server-ver17) +for details. + +```sql +create vector index my_entity_vector_index + on my_entity(embedding) with (metric='cosine'); +``` + +##### SAP HANA + +See the [`create vector index` statement documentation]([https://learn.microsoft.com/en-us/sql/t-sql/statements/create-vector-index-transact-sql?view=sql-server-ver17](https://help.sap.com/docs/hana-cloud-database/sap-hana-cloud-sap-hana-database-sql-reference-guide/create-vector-index-statement-data-definition?locale=en-US)) +for details. + +```sql +create hnsw vector index my_entity_vector_index + on my_entity(embedding) with similarity function cosine_similarity; +``` diff --git a/docs/docs/integrations/embedding-stores/index.md b/docs/docs/integrations/embedding-stores/index.md new file mode 100644 index 0000000..c9b51c5 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/index.md @@ -0,0 +1,46 @@ +--- +title: Comparison table of all supported Embedding Stores +hide_title: false +sidebar_position: 0 +--- + +| Embedding Store | Storing Metadata | Filtering by Metadata | Removing Embeddings | +|---------------------------------------------------------------------------------------|------------------|-----------------------|---------------------| +| [In-memory](/integrations/embedding-stores/in-memory) | ✅ | ✅ | ✅ | +| [AlloyDB for Postgres](/integrations/embedding-stores/alloydb) | ✅ | ✅ | ✅ | +| [Amazon S3 Vectors](/integrations/embedding-stores/amazon-s3-vectors) | ✅ | ✅ | ✅ | +| [ArcadeDB](/integrations/embedding-stores/arcadedb) | ✅ | ✅ | ✅ | +| [Astra DB](/integrations/embedding-stores/astra-db) | ✅ | | | +| [Azure AI Search](/integrations/embedding-stores/azure-ai-search) | ✅ | ✅ | ✅ | +| [Azure CosmosDB Mongo vCore](/integrations/embedding-stores/azure-cosmos-mongo-vcore) | ✅ | | | +| [Azure CosmosDB NoSQL](/integrations/embedding-stores/azure-cosmos-nosql) | ✅ | | | +| [Cassandra](/integrations/embedding-stores/cassandra) | ✅ | | | +| [Chroma](/integrations/embedding-stores/chroma) | ✅ | ✅ | ✅ | +| [ClickHouse](/integrations/embedding-stores/clickhouse) | ✅ | ✅ | ✅ | +| [Cloud SQL for Postgres](/integrations/embedding-stores/cloud-sql) | ✅ | ✅ | ✅ | +| [CockroachDB](/integrations/embedding-stores/cockroachdb) | ✅ | ✅ | ✅ | +| [Coherence](/integrations/embedding-stores/coherence) | ✅ | ✅ | ✅ | +| [Couchbase](/integrations/embedding-stores/couchbase) | ✅ | | ✅ | +| [DuckDB](/integrations/embedding-stores/duckdb) | ✅ | ✅ | ✅ | +| [Elasticsearch](/integrations/embedding-stores/elasticsearch) | ✅ | ✅ | ✅ | +| [Hazelcast](/integrations/embedding-stores/hazelcast) | ✅ | | ✅ | +| [Hibernate](/integrations/embedding-stores/hibernate) | ✅ | ✅ | ✅ | +| [Infinispan](/integrations/embedding-stores/infinispan) | ✅ | ✅ | ✅ | +| [JVector](/integrations/embedding-stores/jvector) | | | ✅ | +| [Mariadb](/integrations/embedding-stores/mariadb) | ✅ | ✅ | ✅ | +| [Milvus](/integrations/embedding-stores/milvus) | ✅ | ✅ | ✅ | +| [MongoDB Atlas](/integrations/embedding-stores/mongodb-atlas) | ✅ | ✅ | ✅ | +| [Neo4j](/integrations/embedding-stores/neo4j) | ✅ | | | +| [OceanBase](/integrations/embedding-stores/oceanbase) | ✅ | ✅ | ✅ | +| [OpenSearch](/integrations/embedding-stores/opensearch) | ✅ | ✅ | ✅ | +| [Oracle](/integrations/embedding-stores/oracle) | ✅ | ✅ | ✅ | +| [PGVector](/integrations/embedding-stores/pgvector) | ✅ | ✅ | ✅ | +| [Pinecone](/integrations/embedding-stores/pinecone) | ✅ | ✅ | ✅ | +| [Qdrant](/integrations/embedding-stores/qdrant) | ✅ | ✅ | ✅ | +| [Redis](/integrations/embedding-stores/redis) | ✅ | | ✅ | +| [SQL Server](/integrations/embedding-stores/sqlserver) | ✅ | ✅ | ✅ | +| [Tablestore](/integrations/embedding-stores/tablestore) | ✅ | ✅ | ✅ | +| [Vearch](/integrations/embedding-stores/vearch) | ✅ | | | +| [Vespa](/integrations/embedding-stores/vespa) | | | | +| [Weaviate](/integrations/embedding-stores/weaviate) | ✅ | | ✅ | +| [YugabyteDB](/integrations/embedding-stores/yugabytedb) | ✅ | ✅ | ✅ | diff --git a/docs/docs/integrations/embedding-stores/infinispan.md b/docs/docs/integrations/embedding-stores/infinispan.md new file mode 100644 index 0000000..82c0e08 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/infinispan.md @@ -0,0 +1,39 @@ +--- +sidebar_position: 13 +--- + +# Infinispan + +Infinispan is an open-source (Apache 2) in-memory key-value database and cache, can hold nearly any type of data, from plain-text +to structured objects, is designed for high performance, scalability, and low-latency data access. +It supports both embedded and client-server modes, allowing applications to +use it as a local cache or a remote server database. + +Infinispan is built in Java and provides features such as persistence, transactions, +querying (full-text and vector search included), clustering, and support for Protobuf-based data indexing—making it suitable +for use cases ranging from simple caching to complex real-time data processing in +microservices and AI applications. + +More in https://infinispan.org/ + +From 15.2 Infinispan Server and above, metadata filtering is supported. + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-infinispan + 1.17.2-beta27 + +``` + + +## APIs + +- `InfinispanEmbeddingStore` + + +## Examples + +- [InfinispanEmbeddingStoreExample](https://github.com/langchain4j/langchain4j-examples/blob/main/infinispan-example/src/main/java/InfinispanEmbeddingStoreExample.java) diff --git a/docs/docs/integrations/embedding-stores/jvector.md b/docs/docs/integrations/embedding-stores/jvector.md new file mode 100644 index 0000000..6ad2533 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/jvector.md @@ -0,0 +1,149 @@ +--- +sidebar_position: 32 +--- + +# JVector + +https://github.com/jbellis/jvector + +JVector is a pure Java embedded vector search engine that provides high-performance approximate nearest neighbor (ANN) search using graph-based indexing. It merges the DiskANN and HNSW algorithm families, offering fast similarity search with configurable accuracy/performance tradeoffs. + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-community-jvector + 1.17.2-beta27 + +``` + +Note: This is a community integration module. You may need to add the langchain4j-community repository to your project configuration. + +## APIs + +- `JVectorEmbeddingStore` + +## Features + +- **Pure Java Implementation**: No native dependencies required, runs anywhere Java runs +- **Graph-Based Indexing**: Uses HNSW hierarchy with Vamana algorithm for high-performance ANN search +- **In-Memory by Default**: Fast searches with optional disk persistence +- **Configurable Performance**: Tune accuracy/speed tradeoffs with parameters like `maxDegree` and `beamWidth` +- **Multiple Similarity Functions**: Supports DOT_PRODUCT (default), COSINE, and EUCLIDEAN distance metrics +- **Thread-Safe**: Nonblocking concurrency control allows safe concurrent access +- **Disk Persistence**: Optional save/load functionality for indexes +- **Dynamic Updates**: Add and remove embeddings after index creation + +## Basic Usage + +### In-Memory Store + +Create a simple in-memory embedding store: + +```java +EmbeddingStore store = JVectorEmbeddingStore.builder() + .dimension(384) // Must match your embedding model's dimension + .build(); +``` + +### Persistent Store + +Create an embedding store with disk persistence: + +```java +EmbeddingStore store = JVectorEmbeddingStore.builder() + .dimension(384) + .persistencePath("/path/to/index") // Base path for index files + .build(); + +// Add embeddings... +store.add(embedding, textSegment); + +// Save to disk +((JVectorEmbeddingStore) store).save(); +``` + +When you create a store with a `persistencePath`, the index will automatically load from disk if files exist at that location. + +## Configuration Options + +JVector provides several builder options to tune performance: + +```java +EmbeddingStore store = JVectorEmbeddingStore.builder() + .dimension(384) // Required: embedding dimension + .maxDegree(16) // Graph connectivity (default: 16) + .beamWidth(100) // Index construction quality (default: 100) + .neighborOverflow(1.2f) // Overflow during construction (default: 1.2) + .alpha(1.2f) // Diversity parameter (default: 1.2) + .similarityFunction(VectorSimilarityFunction.DOT_PRODUCT) // Default + .persistencePath("/path/to/index") // Optional: enable persistence + .build(); +``` + +### Parameter Guidelines + +- **dimension**: Must match your embedding model's output dimension (required) +- **maxDegree**: Controls graph connections per node. Higher values improve recall but use more memory. Recommended: 16 (default) +- **beamWidth**: Controls index construction quality. Higher values build better indexes but take longer. Recommended: 100 (default) +- **neighborOverflow**: Recommended 1.2 for in-memory indexes (default), 1.5 for disk-based indexes +- **alpha**: Controls edge distance vs diversity tradeoff. Recommended: 1.2 for high-dimensional vectors (default), 2.0 for low-dimensional (2D/3D) vectors +- **similarityFunction**: + - `DOT_PRODUCT` - Fastest for normalized vectors (default) + - `COSINE` - For cosine similarity + - `EUCLIDEAN` - For Euclidean distance + +## Persistence + +JVector supports saving and loading indexes from disk: + +```java +// Create store with persistence enabled +JVectorEmbeddingStore store = JVectorEmbeddingStore.builder() + .dimension(384) + .persistencePath("/path/to/index") + .build(); + +// Add embeddings +store.add(embeddings, textSegments); + +// Save to disk (creates .graph and .metadata files) +store.save(); + +// Later: Load automatically when creating with same path +JVectorEmbeddingStore loadedStore = JVectorEmbeddingStore.builder() + .dimension(384) + .persistencePath("/path/to/index") + .build(); +// All previous embeddings and index structure are restored +``` + +Persistence creates two files: +- `{path}.graph` - The graph index structure with vectors +- `{path}.metadata` - Embedding IDs, text segments, and metadata + +## Current Limitations + +- **No Metadata Filtering**: JVector does not support filtering search results by metadata during the search operation. All filtering must be done post-search. +- **Index Rebuild on Modification**: Adding or removing embeddings invalidates the index, which is rebuilt on the next search. For best performance, batch your additions when possible. +- **Dimension Must Match**: All embeddings must have the same dimension as specified during store creation. + +## Performance Characteristics + +JVector is optimized for: +- **Fast similarity search**: Logarithmic time complexity for searches +- **Linear scalability**: Index construction scales linearly with CPU cores +- **Memory efficiency**: In-memory indexes only, with optional disk persistence +- **High recall**: Graph-based approach typically achieves >98% recall with proper tuning + +Ideal use cases: +- Embedded applications requiring vector search without external dependencies +- Development and testing environments +- Production deployments where you want full control over the index +- Applications requiring disk persistence without a separate database + +## Examples + +- Example code can be found in the [JVector source repository](https://github.com/jbellis/jvector/tree/main/jvector-examples) +- For LangChain4j-specific integration examples, check the test files in the [langchain4j-community-jvector module](https://github.com/langchain4j/langchain4j-community/tree/main/embedding-stores/langchain4j-community-jvector/src/test/java) diff --git a/docs/docs/integrations/embedding-stores/mariadb.md b/docs/docs/integrations/embedding-stores/mariadb.md new file mode 100644 index 0000000..714f1c3 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/mariadb.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 18 +--- + +# Mariadb Vector + +https://mariadb.com/kb/en/vector-overview + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-mariadb + 1.17.2-beta27 + +``` + + +## APIs + +- `MariaDbEmbeddingStore` diff --git a/docs/docs/integrations/embedding-stores/milvus.md b/docs/docs/integrations/embedding-stores/milvus.md new file mode 100644 index 0000000..39f8be8 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/milvus.md @@ -0,0 +1,88 @@ +--- +sidebar_position: 14 +--- + +# Milvus + +https://milvus.io/ + + +## Maven Dependency + +```xml + + dev.langchain4j + langchain4j-milvus + 1.17.2-beta27 + +``` + + +## APIs + +- `MilvusEmbeddingStore` + +## Creation + +There are 2 ways to create `MilvusEmbeddingStore`: + + +1. Create MilvusEmbeddingStore with Automatic MilvusServiceClient Creation: Use this option to set up a new MilvusServiceClient internally with specified host, port, and authentication details for easy setup. + + +```java +MilvusEmbeddingStore store = MilvusEmbeddingStore.builder() + + .host("localhost") // Host for Milvus instance + .port(19530) // Port for Milvus instance + .collectionName("example_collection") // Name of the collection + .dimension(128) // Dimension of vectors + .indexType(IndexType.FLAT) // Index type + .metricType(MetricType.COSINE) // Metric type + .username("username") // Username for Milvus + .password("password") // Password for Milvus + .consistencyLevel(ConsistencyLevelEnum.EVENTUALLY) // Consistency level + .autoFlushOnInsert(true) // Auto flush after insert + .idFieldName("id") // ID field name + .textFieldName("text") // Text field name + .metadataFieldName("metadata") // Metadata field name + .vectorFieldName("vector") // Vector field name + .build(); // Build the MilvusEmbeddingStore instance +``` + +2. Create MilvusEmbeddingStore with an Existing MilvusServiceClient: If you already have a MilvusServiceClient, this option lets you directly use it in the builder, allowing customized configurations. + + +```java + +// Set up a custom MilvusServiceClient +MilvusServiceClient customMilvusClient = new MilvusServiceClient( + ConnectParam.newBuilder() + .withHost("localhost") + .withPort(19530) + .build() +); + +// Use the custom client in the builder +MilvusEmbeddingStore store = MilvusEmbeddingStore.builder() + + .milvusClient(customMilvusClient) // Use an existing Milvus client + .collectionName("example_collection") // Name of the collection + .dimension(128) // Dimension of vectors + .indexType(IndexType.FLAT) // Index type + .metricType(MetricType.COSINE) // Metric type + .consistencyLevel(ConsistencyLevelEnum.EVENTUALLY) // Consistency level + .autoFlushOnInsert(true) // Auto flush after insert + .idFieldName("id") // ID field name + .textFieldName("text") // Text field name + .metadataFieldName("metadata") // Metadata field name + .vectorFieldName("vector") // Vector field name + .build(); // Build the MilvusEmbeddingStore instance + + +``` + + +## Examples + +- [MilvusEmbeddingStoreExample](https://github.com/langchain4j/langchain4j-examples/blob/main/milvus-example/src/main/java/MilvusEmbeddingStoreExample.java) diff --git a/docs/docs/integrations/embedding-stores/mongodb-atlas.md b/docs/docs/integrations/embedding-stores/mongodb-atlas.md new file mode 100644 index 0000000..3e8e312 --- /dev/null +++ b/docs/docs/integrations/embedding-stores/mongodb-atlas.md @@ -0,0 +1,237 @@ +--- +sidebar_position: 15 +--- + +# MongoDB Atlas + +[MongoDB Atlas](https://www.mongodb.com/docs/atlas/) is a fully-managed +cloud database available in AWS, Azure, and GCP. It supports native +Vector Search and full-text search (BM25 algorithm) on your MongoDB +document data. + +The [Atlas Vector Search](https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-overview/) +feature allows you to store your embeddings in MongoDB documents, create +vector search indexes, and perform KNN search with an approximate +nearest neighbor algorithm called Hierarchical Navigable Small Worlds. +The MongoDB integration with LangChain4j implements Atlas Vector Search +internally by using the +[`$vectorSearch`](https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/#mongodb-pipeline-pipe.-vectorSearch) +aggregation stage. + +You can use Atlas Vector Search with LangChain4j to perform semantic +searches on your data and build a simple RAG implementation. To view a +full tutorial on performing these tasks, see the [Get Started with the +LangChain4j Integration](https://www.mongodb.com/docs/atlas/atlas-vector-search/ai-integrations/langchain4j/) +tutorial in the MongoDB Atlas documentation. + +## Prerequisites + +You must have a deployment running the following MongoDB Server versions +to use Atlas Vector Search: + +- 6.0.11 or later +- 7.0.2 or later + +MongoDB offers a free forever cluster. See the [Get Started with +Atlas](https://www.mongodb.com/docs/atlas/getting-started/) tutorial to +learn more about setting up an account and connecting to a deployment. + +You also must have an API key with credits for an LLM service that has +provides embedding models, such as [Voyage +AI](https://www.voyageai.com/), which offers a free tier. For RAG +applications, you must also an API key for a service that has chat model +functionality, such as [OpenAI](https://openai.com/api/) or models from +[HuggingFace](https://huggingface.co/). + +## Environment and Installation + +1. Create a new Java application in your preferred IDE. +2. Add the following dependencies to your application to install + LangChain4j and the MongoDB Java Sync Driver: + +```xml + + dev.langchain4j + langchain4j-mongodb-atlas + + + org.mongodb + mongodb-driver-sync + 5.4.0 + +``` + +You must also install a dependency for your embedding model, for example +Voyage AI: + +```xml + + dev.langchain4j + langchain4j-voyage-ai + +``` + +We also recommend adding the LangChain4j BOM: + +```xml + + + dev.langchain4j + langchain4j-bom + 1.17.2-beta27 + pom + + +``` + +## Use MongoDB Atlas as an Embedding Store + +1. Instantiate an [embedding model](https://docs.langchain4j.dev/category/embedding-models). +2. Instantiate MongoDB Atlas as the embedding store. + +You can enable automatic index creation by passing `true` to the +`createIndex()` method when building the `MongoDbEmbeddingStore` +instance. + +```java +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import dev.langchain4j.data.document.Metadata; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.voyageai.VoyageAiEmbeddingModel; +import dev.langchain4j.store.embedding.EmbeddingMatch; +import dev.langchain4j.store.embedding.EmbeddingSearchRequest; +import dev.langchain4j.store.embedding.EmbeddingSearchResult; +import dev.langchain4j.store.embedding.filter.comparison.*; +import dev.langchain4j.store.embedding.mongodb.IndexMapping; +import dev.langchain4j.store.embedding.mongodb.MongoDbEmbeddingStore; +import org.bson.Document; + +import java.io.*; +import java.util.*; + +String embeddingApiKey = System.getenv("VOYAGE_AI_KEY"); +String uri = System.getenv("MONGODB_URI"); + +EmbeddingModel embeddingModel = VoyageAiEmbeddingModel.builder() + .apiKey(embeddingApiKey) + .modelName("voyage-3") + .build(); + +MongoClient mongoClient = MongoClients.create(uri); + +System.out.println("Instantiating the embedding store..."); + +// Set to false if the vector index already exists +Boolean createIndex = true; + +IndexMapping indexMapping = IndexMapping.builder() + .dimension(embeddingModel.dimension()) + .metadataFieldNames(new HashSet<>()) + .build(); + +MongoDbEmbeddingStore embeddingStore = MongoDbEmbeddingStore.builder() + .databaseName("search") + .collectionName("langchaintest") + .createIndex(createIndex) + .indexName("vector_index") + .indexMapping(indexMapping) + .fromClient(mongoClient) + .build(); +``` + +## Store Data in MongoDB + +This code demonstrates how to persist your documents to the +embedding store. The `embed()` method generates embeddings for the `text` +field value in your documents. + +```java +ArrayList docs = new ArrayList<>(); + +docs.add(new Document() + .append("text", "Penguins are flightless seabirds that live almost exclusively below the equator. Some island-dwellers can be found in warmer climates.") + .append("metadata", new Metadata(Map.of("website", "Science Direct")))); + +docs.add(new Document() + .append("text", "Emperor penguins are amazing birds. They not only survive the Antarctic winter, but they breed during the worst weather conditions on earth.") + .append("metadata", new Metadata(Map.of("website", "Our Earth")))); + +docs.add(...); + +System.out.println("Persisting document embeddings..."); + +for (Document doc : docs) { + TextSegment segment = TextSegment.from( + doc.getString("text"), + doc.get("metadata", Metadata.class) + ); + Embedding embedding = embeddingModel.embed(segment).content(); + embeddingStore.add(embedding, segment); +} +``` + +## Perform Semantic/Similarity Searches + +This code demonstrates how to create a search request that converts your +query into a vector and returns semantically similar documents. The +resulting `EmbeddingMatch` instances contain the document contents as +well as a score that describes how well each result matches your query. + +```java +String query = "Where do penguins live?"; +Embedding queryEmbedding = embeddingModel.embed(query).content(); + +EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder() + .queryEmbedding(queryEmbedding) + .maxResults(3) + .build(); + +System.out.println("Performing the query..."); + +EmbeddingSearchResult searchResult = embeddingStore.search(searchRequest); +List> matches = searchResult.matches(); + +for (EmbeddingMatch embeddingMatch : matches) { + System.out.println("Response: " + embeddingMatch.embedded().text()); + System.out.println("Author: " + embeddingMatch.embedded().metadata().getString("author")); + System.out.println("Score: " + embeddingMatch.score()); +} +``` + +### Metadata Filtering + +You can implement metadata filtering by using the `filter()` method when +building a `EmbeddingSearchRequest`. The `filter()` method takes a +parameter that inherits from +[Filter](https://docs.langchain4j.dev/apidocs/dev/langchain4j/store/embedding/filter/Filter.html). + +This code implements metadata filtering for only documents in which the +value of `website` is one of the listed values. + +```java +EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder() + .queryEmbedding(queryEmbedding) + .filter(new IsIn("website", List.of("Our Earth", "Natural Habitats"))) + .maxResults(3) + .build(); +``` + +## RAG + +To view instructions on implementing RAG with MongoDB Atlas as your +vector store, see the [Use Your Data to Answer Questions](https://www.mongodb.com/docs/atlas/atlas-vector-search/ai-integrations/langchain4j/#use-your-data-to-answer-questions) +section of the LangChain4j tutorial in the Atlas documentation. + +## API Documentation + +- [MongoDB Atlas Embedding Store Integration](https://docs.langchain4j.dev/apidocs/dev/langchain4j/store/embedding/mongodb/package-summary.html) + +- [MongoDB Java Sync Driver](https://mongodb.github.io/mongo-java-driver/5.4/apidocs/mongodb-driver-sync/index.html) + +## Useful Links + +- [Get Started with the LangChain4j Integration](https://www.mongodb.com/docs/atlas/atlas-vector-search/ai-integrations/langchain4j/) +- [How to Make a RAG Application With LangChain4j](https://dev.to/mongodb/how-to-make-a-rag-application-with-langchain4j-1mad) diff --git a/docs/docs/integrations/embedding-stores/neo4j.md b/docs/docs/integrations/embedding-stores/neo4j.md new file mode 100644 index 0000000..51fcebb --- /dev/null +++ b/docs/docs/integrations/embedding-stores/neo4j.md @@ -0,0 +1,1087 @@ +--- +sidebar_position: 16 +--- + + + +# Neo4j + +[Neo4j](https://neo4j.com/) is a high-performance, open-source graph database designed for managing connected data. +Neo4j's native graph model is ideal for modelling complex and highly interconnected domains, like social graphs, recommendation systems, and knowledge networks. +With its integration in LangChain4j, the [Neo4j Vector](https://github.com/neo4j-documentation/labs-pages/blob/publish/modules/genai-ecosystem/pages/vector-search.adoc) capabilities can be used in the Langchain4j library. + +## Maven Dependency +```xml + + dev.langchain4j + langchain4j-community-neo4j + ${latest version here} + + + + dev.langchain4j + langchain4j-community-neo4j-retriever + ${latest version here} + + + + + dev.langchain4j + langchain4j-community-neo4j-spring-boot-starter + ${latest version here} + +``` +## APIs + +LangChain4j provides the following classes for Neo4j integration: +- `Neo4jEmbeddingStore`: Implements the EmbeddingStore interface, enabling storing and querying vector embeddings in a Neo4j database. +- `Neo4jText2CypherRetriever`: Implements the ContentRetriever interface for generating and executing Cypher queries from user questions, improving content retrieval from Neo4j databases. It translates natural language questions into Cypher queries, + leveraging the Neo4j schema calculated via [apoc.meta.data](https://neo4j.com/docs/apoc/current/overview/apoc.meta/apoc.meta.data) procedure. +- `KnowledgeGraphWriter`: A class that stores Neo4j nodes and relationships starting from structured data coming from `LLMGraphTransformer`, +that is a tool that transform one or more unstructured documents in a graph. It’s database-agnostic, which means that transforms texts into a set of Nodes and Edges that can also be used for other graph databases like RedisGraph. +- `Neo4jEmbeddingStoreIngestor`: Implements the `ParentChildEmbeddingStoreIngestor` interface, it performs a multi-stage transformation pipeline: it transforms documents, splits them into segments, optionally applies additional transformations to child segments, generates embeddings, and stores both the parent-child relationships and embeddings in Neo4j. +- `Neo4jChatMemoryStore`: Implements the `ChatMemoryStore` interface that stores and retrieves conversational messages in a Neo4j graph database. It supports managing chat history with efficient querying and persistence using Neo4j nodes and relationships. + +## Usage Examples + +### Neo4jEmbeddingStore + +Here is how to create a `Neo4jEmbeddingStore` instance: + +```java +Neo4jEmbeddingStore embeddingStore = Neo4jEmbeddingStore.builder()..build(); +``` + +Where `` must include `dimension` and either `driver` or `withBasicAuth` parameters, along with other optional ones. + +Here is the complete builder list: + +| Key | Default Value| Description | +| ------------------- |-----| --------------------- | +| `driver` | *Required if `withBasicAuth` is not set* | The [Java Driver instance](https://neo4j.com/docs/api/java-driver/current/org.neo4j.driver/org/neo4j/driver/Driver.html) | +| `withBasicAuth` | *Required if `driver` is not set* | Creates a [Java Driver instance](https://neo4j.com/docs/api/java-driver/current/org.neo4j.driver/org/neo4j/driver/Driver.html) from `uri`, `user`, and `password` | +| `dimension` | *Required* | The vector's dimension | +| `config` | `org.neo4j.driver.SessionConfig.forDatabase("")` | The [SessionConfig instance](https://neo4j.com/docs/api/java-driver/current/org.neo4j.driver/org/neo4j/driver/SessionConfig.html) | +| `label` | `"Document"`| The label name | +| `embeddingProperty` | `"embedding"` | The embedding property name | +| `idProperty` | `"id"` | The ID property name | +| `metadataPrefix` | `""` | The metadata prefix | +| `textProperty` | `"text"` | The text property name | +| `indexName` | `"vector"` | The vector index name | +| `databaseName` | `"neo4j"`| The database name | +| `retrievalQuery` | `"RETURN properties(node) AS metadata, node.idProperty AS idProperty, node.textProperty AS textProperty, node.embeddingProperty AS embeddingProperty, score"` | The retrieval query | + + + + +Therefore, to create `Neo4jEmbeddingStore` instance, you need to provide proper settings: +```java +// ---> MINIMAL EMBEDDING <--- +Neo4jEmbeddingStore minimalEmbedding = Neo4jEmbeddingStore.builder() + .withBasicAuth(NEO4J_CONNECTION_STRING, USERNAME, ADMIN_PASSWORD) + .dimension(384) + .build(); + +// ---> CUSTOM EMBEDDING <--- +Neo4jEmbeddingStore customEmbeddingStore = Neo4jEmbeddingStore.builder() + .withBasicAuth(NEO4J_CONNECTION_STRING, USERNAME, ADMIN_PASSWORD) + .dimension(384) + .indexName(CUSTOM_INDEX) + .metadataPrefix(CUSTOM_METADATA_PREF) + .label(CUSTOM_LABEL) + .embeddingProperty(CUSTOM_PROP) + .idProperty(CUSTOM_ID) + .textProperty(CUSTOM_TEXT) + .build(); +``` +Then you can add the embeddings in many different ways, and search them: +```java +// ---> ADD MINIMAL EMBEDDING <--- +Embedding embedding = embeddingModel.embed("embedText").content(); +String id = minimalEmbedding.add(embedding); // output: id of the embedding + +// ---> ADD MINIMAL EMBEDDING WITH ID <--- +String id = randomUUID(); +Embedding embedding = embeddingModel.embed("embedText").content(); +minimalEmbedding.add(id, embedding); + +// ---> ADD EMBEDDING WITH SEGMENT <--- +TextSegment segment = TextSegment.from(randomUUID()); +Embedding embedding = embeddingModel.embed(segment.text()).content(); +String id = minimalEmbedding.add(embedding, segment); + +// ---> ADD EMBEDDING WITH SEGMENT AND METADATA <--- +TextSegment segment = TextSegment.from(randomUUID(), Metadata.from(METADATA_KEY, "test-value")); +Embedding embedding = embeddingModel.embed(segment.text()).content(); +String id = minimalEmbedding.add(embedding, segment); + +// ---> ADD MULTIPLE EMBEDDINGS <--- +Embedding firstEmbedding = embeddingModel.embed("firstEmbedText").content(); +Embedding secondEmbedding = embeddingModel.embed("secondEmbedText").content(); +List ids = minimalEmbedding.addAll(asList(firstEmbedding, secondEmbedding)); + +// ---> ADD MULTIPLE EMBEDDINGS WITH SEGMENTS <--- +TextSegment firstSegment = TextSegment.from("firstText"); +Embedding firstEmbedding = embeddingModel.embed(firstSegment.text()).content(); +TextSegment secondSegment = TextSegment.from("secondText"); +Embedding secondEmbedding = embeddingModel.embed(secondSegment.text()).content(); +List ids = minimalEmbedding.addAll( + asList(firstEmbedding, secondEmbedding), + asList(firstSegment, secondSegment) +); +``` +Then you can search the stored embeddings: +```java +// ---> SEARCH EMBEDDING WITH MAX RESULTS <--- +String id = minimalEmbedding.add(embedding); +final EmbeddingSearchRequest request = EmbeddingSearchRequest.builder() + .queryEmbedding(embedding) + .maxResults(10) + .build(); +final List> relevant = embeddingStore.search(request).matches(); + +// ---> SEARCH EMBEDDING WITH MIN SCORE <--- +Embedding embedding = embeddingModel.embed("embedText").content(); +String id = minimalEmbedding.add(embedding); +final EmbeddingSearchRequest request = EmbeddingSearchRequest.builder() + .queryEmbedding(embedding) + .maxResults(10) + .minScore(0.15) + .build(); +final List> relevant = embeddingStore.search(request).matches(); + +// ---> SEARCH EMBEDDING WITH CUSTOM METADATA PREFIX <--- +String metadataCompleteKey = CUSTOM_METADATA_PREF + METADATA_KEY; +TextSegment segment = TextSegment.from(randomUUID(), Metadata.from(METADATA_KEY, "test-value")); +Embedding embedding = embeddingModel.embed(segment.text()).content(); +String id = customEmbeddingStore.add(embedding, segment); +final EmbeddingSearchRequest request = EmbeddingSearchRequest.builder() + .queryEmbedding(embedding) + .maxResults(10) + .build(); +final List> relevant = embeddingStore.search(request).matches(); + +// ---> SEARCH EMBEDDING WITH CUSTOM ID PROPERTY <--- +String metadataCompleteKey = CUSTOM_METADATA_PREF + METADATA_KEY; +TextSegment segment = TextSegment.from(randomUUID(), Metadata.from(METADATA_KEY, "test-value")); +Embedding embedding = embeddingModel.embed(segment.text()).content(); +String id = embeddingStore.add(embedding, segment); +final EmbeddingSearchRequest request = EmbeddingSearchRequest.builder() + .queryEmbedding(embedding) + .maxResults(10) + .build(); +final List> relevant = embeddingStore.search(request).matches(); + +// ---> SEARCH MULTIPLE EMBEDDING <--- +List ids = minimalEmbedding.addAll(asList(firstEmbedding, secondEmbedding)); +final EmbeddingSearchRequest request = EmbeddingSearchRequest.builder() + .queryEmbedding(firstEmbedding) + .maxResults(10) + .build(); +final List> relevant = embeddingStore.search(request).matches(); + +// ---> SEARCH MULTIPLE EMBEDDING WITH SEGMENTS <--- +List ids = minimalEmbedding.addAll( + asList(firstEmbedding, secondEmbedding), + asList(firstSegment, secondSegment) +); +final EmbeddingSearchRequest request = EmbeddingSearchRequest.builder() + .queryEmbedding(firstEmbedding) + .maxResults(10) + .build(); +final List> relevant = embeddingStore.search(request).matches(); +``` + +To get embeddings using a hybrid search leveraging both the vector and the full text index: +```java +// ---> ADDS EMBEDDING AND FULLTEXT WITH ID <--- +embeddingStore = Neo4jEmbeddingStore.builder() + .withBasicAuth("", "", "") + .dimension(384) + .fullTextIndexName("movie_text") + .fullTextQuery("Matrix") + .autoCreateFullText(true) + .label(LABEL_TO_SANITIZE) + .build(); + +List embeddings = + embeddingModel.embedAll(List.of(TextSegment.from("test"))).content(); + embeddingStore.addAll(embeddings); + +final Embedding queryEmbedding = embeddingModel.embed("Matrix").content(); + +final EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder() + .queryEmbedding(queryEmbedding) + .maxResults(1) + .build(); + +final List> matches = + embeddingStore.search(embeddingSearchRequest).matches(); + +// ---> SEARCH EMBEDDING WITH AUTOCREATED FULLTEXT <--- +final String fullTextIndexName = "movie_text"; +final String label = "Movie"; +final String fullTextSearch = "Matrix"; +embeddingStore = Neo4jEmbeddingStore.builder() + .withBasicAuth("", "", "") + .dimension(384) + .label(label) + .indexName("movie_vector_idx") + .fullTextIndexName(fullTextIndexName) + .fullTextQuery(fullTextSearch) + .build(); +``` + +If the FULLTEXT index is invalid, a descriptive exception will be thrown.: +```java +// ---> ERROR HANDLING WITH INVALID FULLTEXT <--- +Neo4jEmbeddingStore embeddingStore = Neo4jEmbeddingStore.builder() + .withBasicAuth("", "", "") + .dimension(384) + .fullTextIndexName("full_text_with_invalid_retrieval") + .fullTextQuery("Matrix") + .autoCreateFullText(true) + .fullTextRetrievalQuery("RETURN properties(invalid) AS metadata") + .label(LABEL_TO_SANITIZE) + .build(); + +List embeddings = embeddingModel.embedAll(List.of(TextSegment.from("test"))).content(); +embeddingStore.addAll(embeddings); + +final Embedding queryEmbedding = embeddingModel.embed("Matrix").content(); + +final EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder() + .queryEmbedding(queryEmbedding) + .maxResults(3) + .build(); +embeddingStore.search(embeddingSearchRequest).matches(); +// This search will throw a ClientException: ... Variable `invalid` not defined ... +``` + +To execute a search with a metadata filtering leveraging the `dev.langchain4j.store.embedding.filter.Filter` class: +```java +// ---> ADD EMBEDDING WITH ID AND RETRIEVE WITH OR WITHOUT PREFILTER <--- +final List segments = IntStream.range(0, 10) + .boxed() + .map(i -> { + if (i == 0) { + final Map metas = + Map.of("key1", "value1", "key2", 10, "key3", "3", "key4", "value4"); + final Metadata metadata = new Metadata(metas); + return TextSegment.from(randomUUID(), metadata); + } + return TextSegment.from(randomUUID()); + }) + .toList(); + +final List embeddings = embeddingModel.embedAll(segments).content(); +embeddingStore.addAll(embeddings, segments); + +final And filter = new And( + new And(new IsEqualTo("key1", "value1"), new IsEqualTo("key2", "10")), + new Not(new Or(new IsIn("key3", asList("1", "2")), new IsNotEqualTo("key4", "value4")))); + +TextSegment segmentToSearch = TextSegment.from(randomUUID()); +Embedding embeddingToSearch = + embeddingModel.embed(segmentToSearch.text()).content(); +final EmbeddingSearchRequest requestWithFilter = EmbeddingSearchRequest.builder() + .maxResults(5) + .minScore(0.0) + .filter(filter) + .queryEmbedding(embeddingToSearch) + .build(); +final EmbeddingSearchResult searchWithFilter = embeddingStore.search(requestWithFilter); +final List> matchesWithFilter = searchWithFilter.matches(); + +final EmbeddingSearchRequest requestWithoutFilter = EmbeddingSearchRequest.builder() + .maxResults(5) + .minScore(0.0) + .queryEmbedding(embeddingToSearch) + .build(); +final EmbeddingSearchResult searchWithoutFilter = embeddingStore.search(requestWithoutFilter); +final List> matchesWithoutFilter = searchWithoutFilter.matches(); +``` + +To execute a follow-up query for reading or writing data retrieved by the embedding search, we can leverage the nodes' `embeddingId`s. +For example: +```java +// ... Neo4jEmbeddingStore instance creation ... +// ... add embeddings.... + +final List> results = embeddingStore.search(/*dev.langchain4j.store.embedding.EmbeddingSearchRequest instance*/) + .matches(); + +// retrieve the ids to execute the follow-up +List nodeIds = results.stream().map(dev.langchain4j.store.embedding.EmbeddingMatch:embeddingId).toList(); + +String cypher = """ + MATCH (d:Document) + WHERE d.id IN $ids + // -- here the follow-up query, for example + WITH (d)-[:CONNECTED_TO]->(o:OtherLabel) + RETURN o.id + """; + +// run the follow-up query +Map params = Map.of("ids", nodeIds); +final List list = session.run(cypher, params).list(); +``` + +#### Spring Boot starter + +To create a **Spring Boot starter**, the Neo4j starter provides at the time being the following `application.properties`: +```properties + +# the builder.dimension(dimension) method +langchain4j.community.neo4j.dimension= +# the builder.withBasicAuth(uri, username, password) method +langchain4j.community.neo4j.auth.uri= +langchain4j.community.neo4j.auth.user= +langchain4j.community.neo4j.auth.password= +# the builder.label(label) method +langchain4j.community.neo4j.label=