From 4eb894789ee34f4afe50abdb1952914a8df44514 Mon Sep 17 00:00:00 2001 From: Jan Martiska Date: Wed, 28 Aug 2024 13:46:36 +0200 Subject: [PATCH] Integrate the Tavily search engine --- docs/modules/ROOT/nav.adoc | 1 + .../includes/quarkus-langchain4j-tavily.adoc | 220 ++++++++++++++++++ docs/modules/ROOT/pages/web-search.adoc | 67 ++++++ docs/pom.xml | 19 ++ .../src/main/resources/application.properties | 1 + pom.xml | 3 + samples/chatbot-web-search/README.md | 28 +++ samples/chatbot-web-search/pom.xml | 165 +++++++++++++ .../sample/chatbot/AdditionalTools.java | 25 ++ .../langchain4j/sample/chatbot/Bot.java | 14 ++ .../sample/chatbot/ChatBotWebSocket.java | 26 +++ .../sample/chatbot/ImportmapResource.java | 51 ++++ .../chatbot/WebSearchRetrievalAugmentor.java | 35 +++ .../resources/components/demo-chat.js | 31 +++ .../resources/components/demo-title.js | 74 ++++++ .../resources/fonts/red-hat-font.min.css | 1 + .../resources/META-INF/resources/index.html | 71 ++++++ .../src/main/resources/application.properties | 1 + web-search-engines/tavily/deployment/pom.xml | 54 +++++ .../tavily/deployment/TavilyProcessor.java | 47 ++++ .../langchain4j/tavily/test/TavilyTest.java | 142 +++++++++++ web-search-engines/tavily/pom.xml | 20 ++ web-search-engines/tavily/runtime/pom.xml | 71 ++++++ .../tavily/QuarkusTavilyWebSearchEngine.java | 187 +++++++++++++++ .../langchain4j/tavily/SearchDepth.java | 6 + .../tavily/runtime/TavilyClient.java | 17 ++ .../tavily/runtime/TavilyConfig.java | 89 +++++++ .../tavily/runtime/TavilyRecorder.java | 29 +++ .../tavily/runtime/TavilyResponse.java | 51 ++++ .../tavily/runtime/TavilySearchRequest.java | 77 ++++++ .../tavily/runtime/TavilySearchResult.java | 44 ++++ .../src/main/resources/META-INF/beans.xml | 0 .../resources/META-INF/quarkus-extension.yaml | 12 + 33 files changed, 1679 insertions(+) create mode 100644 docs/modules/ROOT/pages/includes/quarkus-langchain4j-tavily.adoc create mode 100644 docs/modules/ROOT/pages/web-search.adoc create mode 100644 samples/chatbot-web-search/README.md create mode 100644 samples/chatbot-web-search/pom.xml create mode 100644 samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/AdditionalTools.java create mode 100644 samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/Bot.java create mode 100644 samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ChatBotWebSocket.java create mode 100644 samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ImportmapResource.java create mode 100644 samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/WebSearchRetrievalAugmentor.java create mode 100644 samples/chatbot-web-search/src/main/resources/META-INF/resources/components/demo-chat.js create mode 100644 samples/chatbot-web-search/src/main/resources/META-INF/resources/components/demo-title.js create mode 100644 samples/chatbot-web-search/src/main/resources/META-INF/resources/fonts/red-hat-font.min.css create mode 100644 samples/chatbot-web-search/src/main/resources/META-INF/resources/index.html create mode 100644 samples/chatbot-web-search/src/main/resources/application.properties create mode 100644 web-search-engines/tavily/deployment/pom.xml create mode 100644 web-search-engines/tavily/deployment/src/main/java/io/quarkiverse/langchain4j/tavily/deployment/TavilyProcessor.java create mode 100644 web-search-engines/tavily/deployment/src/test/java/io/quarkiverse/langchain4j/tavily/test/TavilyTest.java create mode 100644 web-search-engines/tavily/pom.xml create mode 100644 web-search-engines/tavily/runtime/pom.xml create mode 100644 web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/QuarkusTavilyWebSearchEngine.java create mode 100644 web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/SearchDepth.java create mode 100644 web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilyClient.java create mode 100644 web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilyConfig.java create mode 100644 web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilyRecorder.java create mode 100644 web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilyResponse.java create mode 100644 web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilySearchRequest.java create mode 100644 web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilySearchResult.java create mode 100644 web-search-engines/tavily/runtime/src/main/resources/META-INF/beans.xml create mode 100644 web-search-engines/tavily/runtime/src/main/resources/META-INF/quarkus-extension.yaml diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index d5b3b3d1e..f74f1ea31 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -31,6 +31,7 @@ ** xref:easy-rag.adoc[Easy RAG] ** xref:dev-ui.adoc[Dev UI] ** xref:reranking.adoc[Reranking] +** xref:web-search.adoc[Web search] * Advanced topics ** xref:fault-tolerance.adoc[Fault Tolerance] diff --git a/docs/modules/ROOT/pages/includes/quarkus-langchain4j-tavily.adoc b/docs/modules/ROOT/pages/includes/quarkus-langchain4j-tavily.adoc new file mode 100644 index 000000000..6744067e9 --- /dev/null +++ b/docs/modules/ROOT/pages/includes/quarkus-langchain4j-tavily.adoc @@ -0,0 +1,220 @@ + +:summaryTableId: quarkus-langchain4j-tavily +[.configuration-legend] +icon:lock[title=Fixed at build time] Configuration property fixed at build time - All other configuration properties are overridable at runtime +[.configuration-reference.searchable, cols="80,.^10,.^10"] +|=== + +h|[[quarkus-langchain4j-tavily_configuration]]link:#quarkus-langchain4j-tavily_configuration[Configuration property] + +h|Type +h|Default + +a| [[quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-base-url]]`link:#quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-base-url[quarkus.langchain4j.tavily.base-url]` + + +[.description] +-- +Base URL of the Tavily API + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_TAVILY_BASE_URL+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_TAVILY_BASE_URL+++` +endif::add-copy-button-to-env-var[] +--|string +|`https://api.tavily.com` + + +a| [[quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-api-key]]`link:#quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-api-key[quarkus.langchain4j.tavily.api-key]` + + +[.description] +-- +API key for the Tavily API + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_TAVILY_API_KEY+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_TAVILY_API_KEY+++` +endif::add-copy-button-to-env-var[] +--|string +|required icon:exclamation-circle[title=Configuration property is required] + + +a| [[quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-max-results]]`link:#quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-max-results[quarkus.langchain4j.tavily.max-results]` + + +[.description] +-- +Maximum number of results to return + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_TAVILY_MAX_RESULTS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_TAVILY_MAX_RESULTS+++` +endif::add-copy-button-to-env-var[] +--|int +|`5` + + +a| [[quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-timeout]]`link:#quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-timeout[quarkus.langchain4j.tavily.timeout]` + + +[.description] +-- +The timeout duration for Tavily requests. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_TAVILY_TIMEOUT+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_TAVILY_TIMEOUT+++` +endif::add-copy-button-to-env-var[] +--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration] + link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]] +|`60S` + + +a| [[quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-log-requests]]`link:#quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-log-requests[quarkus.langchain4j.tavily.log-requests]` + + +[.description] +-- +Whether requests to Tavily should be logged + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_TAVILY_LOG_REQUESTS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_TAVILY_LOG_REQUESTS+++` +endif::add-copy-button-to-env-var[] +--|boolean +|`false` + + +a| [[quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-log-responses]]`link:#quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-log-responses[quarkus.langchain4j.tavily.log-responses]` + + +[.description] +-- +Whether responses from Tavily should be logged + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_TAVILY_LOG_RESPONSES+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_TAVILY_LOG_RESPONSES+++` +endif::add-copy-button-to-env-var[] +--|boolean +|`false` + + +a| [[quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-search-depth]]`link:#quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-search-depth[quarkus.langchain4j.tavily.search-depth]` + + +[.description] +-- +The search depth to use. This can be "basic" or "advanced". Basic is the default. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_TAVILY_SEARCH_DEPTH+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_TAVILY_SEARCH_DEPTH+++` +endif::add-copy-button-to-env-var[] +-- a| +`basic`, `advanced` +|`basic` + + +a| [[quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-include-answer]]`link:#quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-include-answer[quarkus.langchain4j.tavily.include-answer]` + + +[.description] +-- +Include a short answer to original query. Default is false. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_TAVILY_INCLUDE_ANSWER+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_TAVILY_INCLUDE_ANSWER+++` +endif::add-copy-button-to-env-var[] +--|boolean +|`false` + + +a| [[quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-include-raw-content]]`link:#quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-include-raw-content[quarkus.langchain4j.tavily.include-raw-content]` + + +[.description] +-- +Include the cleaned and parsed HTML content of each search result. Default is false. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_TAVILY_INCLUDE_RAW_CONTENT+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_TAVILY_INCLUDE_RAW_CONTENT+++` +endif::add-copy-button-to-env-var[] +--|boolean +|`false` + + +a| [[quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-include-domains]]`link:#quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-include-domains[quarkus.langchain4j.tavily.include-domains]` + + +[.description] +-- +A list of domains to specifically include in the search results. Default is ++[]++, which includes all domains. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_TAVILY_INCLUDE_DOMAINS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_TAVILY_INCLUDE_DOMAINS+++` +endif::add-copy-button-to-env-var[] +--|list of string +|`[]` + + +a| [[quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-exclude-domains]]`link:#quarkus-langchain4j-tavily_quarkus-langchain4j-tavily-exclude-domains[quarkus.langchain4j.tavily.exclude-domains]` + + +[.description] +-- +A list of domains to specifically exclude from the search results. Default is ++[]++, which doesn't exclude any domains. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_TAVILY_EXCLUDE_DOMAINS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_TAVILY_EXCLUDE_DOMAINS+++` +endif::add-copy-button-to-env-var[] +--|list of string +|`[]` + +|=== +ifndef::no-duration-note[] +[NOTE] +[id='duration-note-anchor-{summaryTableId}'] +.About the Duration format +==== +To write duration values, use the standard `java.time.Duration` format. +See the link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)[Duration#parse() Java API documentation] for more information. + +You can also use a simplified format, starting with a number: + +* If the value is only a number, it represents time in seconds. +* If the value is a number followed by `ms`, it represents time in milliseconds. + +In other cases, the simplified format is translated to the `java.time.Duration` format for parsing: + +* If the value is a number followed by `h`, `m`, or `s`, it is prefixed with `PT`. +* If the value is a number followed by `d`, it is prefixed with `P`. +==== +endif::no-duration-note[] diff --git a/docs/modules/ROOT/pages/web-search.adoc b/docs/modules/ROOT/pages/web-search.adoc new file mode 100644 index 000000000..9508cb44a --- /dev/null +++ b/docs/modules/ROOT/pages/web-search.adoc @@ -0,0 +1,67 @@ += Using web search + +Quarkus LangChain4j currently supports the https://tavily.com/[Tavily] search engine. +To use it, add the `quarkus-langchain4j-tavily` extension to your project. You'll need to specify the API key, this is done by the `quarkus.langchain4j.tavily.api-key` property. + +After this, you can inject the search engine into your application using + +[source,java] +---- +@Inject +WebSearchEngine engine; +---- + +and then use it by calling its `search` method. + +If you want to let an chat model use web search by itself, there are +generally two recommended ways to accomplish this: either by implementing a +tool that uses it, or as a content retriever inside a RAG pipeline. The +https://github.com/quarkiverse/quarkus-langchain4j/tree/main/samples/chatbot-web-search[chatbot-web-search] +example in the `quarkus-langchain4j` repository demonstrates using web +search as a tool. + +== Using Web search as a tool + +To use web search as a tool that the LLM can decide to execute (and the +relevant search results will be the return value of the tool execution), you +can either use the provided tool from the upstream LangChain4j project, +in class `dev.langchain4j.web.search.WebSearchTool`, or implement your own tool +if that one does not fit your requirements. The `samples/chatbot-web-search` +example demonstrates how to use the provided tool. + +== Using Web search in a RAG pipeline + +There is also a provided content retriever, `dev.langchain4j.rag.content.retriever.WebSearchContentRetriever` that uses +a web search engine to retrieve relevant documents. +For inspiration, the retrieval augmentor that wraps it may look like this: + +[source,java] +---- +@ApplicationScoped +public class WebSearchRetrievalAugmentor implements Supplier { + + @Inject + WebSearchEngine webSearchEngine; + + @Inject + ChatLanguageModel chatModel; + + @Override + public RetrievalAugmentor get() { + return DefaultRetrievalAugmentor.builder() + .queryTransformer((question) -> { + // before actually querying the engine, we need to transform the + // user's question into a suitable search query + String query = chatModel.generate("Transform the user's question into a suitable query for the " + + "Tavily search engine. The query should yield the results relevant to answering the user's question." + + "User's question: " + question.text()); + return Collections.singleton(Query.from(query)); + }).contentRetriever(new WebSearchContentRetriever(webSearchEngine, 10)) + .build(); + } +} +---- + +== Tavily configuration reference + +include::includes/quarkus-langchain4j-tavily.adoc[leveloffset=+1,opts=optional] diff --git a/docs/pom.xml b/docs/pom.xml index 650332afa..a69111f95 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -76,6 +76,11 @@ quarkus-langchain4j-watsonx ${project.version} + + io.quarkiverse.langchain4j + quarkus-langchain4j-tavily + ${project.version} + io.quarkiverse.langchain4j quarkus-langchain4j-easy-rag @@ -279,6 +284,19 @@ + + io.quarkiverse.langchain4j + quarkus-langchain4j-tavily-deployment + ${project.version} + pom + test + + + * + * + + + @@ -343,6 +361,7 @@ quarkus-langchain4j-watsonx.adoc quarkus-langchain4j-mistralai.adoc quarkus-langchain4j-neo4j.adoc + quarkus-langchain4j-tavily.adoc false diff --git a/docs/src/main/resources/application.properties b/docs/src/main/resources/application.properties index b3ebca027..df10162d1 100644 --- a/docs/src/main/resources/application.properties +++ b/docs/src/main/resources/application.properties @@ -9,6 +9,7 @@ quarkus.langchain4j.pinecone.environment=abc quarkus.langchain4j.pinecone.index-name=abc quarkus.langchain4j.pinecone.project-id=abc quarkus.langchain4j.pinecone.api-key=abc +quarkus.langchain4j.tavily.api-key=abc quarkus.langchain4j.redis.dimension=180 quarkus.langchain4j.easy-rag.path=abc quarkus.langchain4j.easy-rag.ingestion-strategy=off diff --git a/pom.xml b/pom.xml index a8e17c221..26647cc15 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,8 @@ quarkus-integrations/websockets-next + web-search-engines/tavily + rag/easy-rag rag/parsers-base @@ -205,6 +207,7 @@ samples/chatbot samples/chatbot-easy-rag samples/sql-chatbot + samples/chatbot-web-search diff --git a/samples/chatbot-web-search/README.md b/samples/chatbot-web-search/README.md new file mode 100644 index 000000000..67eb8c972 --- /dev/null +++ b/samples/chatbot-web-search/README.md @@ -0,0 +1,28 @@ +# Web search example + +This example demonstrates how to create a chatbot that can use the Tavily search +engine to look up information on the web. + +## Running the example + +A prerequisite to running this example is to provide your OpenAI and Tavily API keys. + +``` +export QUARKUS_LANGCHAIN4J_OPENAI_API_KEY= +export QUARKUS_LANGCHAIN4J_TAVILY_API_KEY= +``` + +Then, simply run the project in Dev mode: + +``` +mvn quarkus:dev +``` + +## Using the example + +Open your browser and navigate to http://localhost:8080. Click the red robot +in the bottom right corner to open the chat window. + +Read the description on the web page to learn about the implementation details. + + diff --git a/samples/chatbot-web-search/pom.xml b/samples/chatbot-web-search/pom.xml new file mode 100644 index 000000000..11bd6f9ab --- /dev/null +++ b/samples/chatbot-web-search/pom.xml @@ -0,0 +1,165 @@ + + + 4.0.0 + + io.quarkiverse.langchain4j + quarkus-langchain4j-sample-chatbot-web-search + Quarkus LangChain4j - Sample - Chatbot & Web search + 1.0-SNAPSHOT + + + 3.13.0 + true + 17 + UTF-8 + UTF-8 + quarkus-bom + io.quarkus + 3.12.1 + true + 3.2.5 + + 999-SNAPSHOT + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + io.quarkus + quarkus-websockets-next + + + io.quarkiverse.langchain4j + quarkus-langchain4j-openai + ${quarkus-langchain4j.version} + + + io.quarkiverse.langchain4j + quarkus-langchain4j-tavily + ${quarkus-langchain4j.version} + + + + + io.mvnpm + importmap + 1.0.11 + + + org.mvnpm + lit + 3.2.0 + runtime + + + org.mvnpm + wc-chatbot + 0.1.2 + runtime + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.platform.version} + + + + build + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + maven-surefire-plugin + 3.3.1 + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + native + + + native + + + + + + maven-failsafe-plugin + 3.4.0 + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + native + + + + + mvnpm + + + central + central + https://repo.maven.apache.org/maven2 + + + + false + + mvnpm.org + mvnpm + https://repo.mvnpm.org/maven2 + + + + + + diff --git a/samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/AdditionalTools.java b/samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/AdditionalTools.java new file mode 100644 index 000000000..834eb196a --- /dev/null +++ b/samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/AdditionalTools.java @@ -0,0 +1,25 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import dev.langchain4j.agent.tool.Tool; +import dev.langchain4j.web.search.WebSearchEngine; +import io.quarkus.logging.Log; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +@ApplicationScoped +public class AdditionalTools { + + @Inject + WebSearchEngine webSearchEngine; + + @Tool("Get today's date") + public String getTodaysDate() { + String date = DateTimeFormatter.ISO_DATE.format(LocalDate.now()); + Log.info("The model is asking for today's date, returning " + date); + return date; + } + +} diff --git a/samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/Bot.java b/samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/Bot.java new file mode 100644 index 000000000..c17feccf0 --- /dev/null +++ b/samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/Bot.java @@ -0,0 +1,14 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import dev.langchain4j.service.UserMessage; +import dev.langchain4j.web.search.WebSearchTool; +import io.quarkiverse.langchain4j.RegisterAiService; +import jakarta.enterprise.context.SessionScoped; + +@RegisterAiService(tools = {WebSearchTool.class, AdditionalTools.class}) +@SessionScoped +public interface Bot { + + String chat(@UserMessage String question); + +} diff --git a/samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ChatBotWebSocket.java b/samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ChatBotWebSocket.java new file mode 100644 index 000000000..1d09768aa --- /dev/null +++ b/samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ChatBotWebSocket.java @@ -0,0 +1,26 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/chatbot") +public class ChatBotWebSocket { + + private final Bot bot; + + public ChatBotWebSocket(Bot bot) { + this.bot = bot; + } + + @OnOpen + public String onOpen() { + return "Hello, I'm Bob, how can I help you?"; + } + + @OnTextMessage + public String onMessage(String message) { + return bot.chat(message); + } + +} diff --git a/samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ImportmapResource.java b/samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ImportmapResource.java new file mode 100644 index 000000000..88d0ee21d --- /dev/null +++ b/samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ImportmapResource.java @@ -0,0 +1,51 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; + +import io.mvnpm.importmap.Aggregator; + +/** + * Dynamically create the import map + */ +@ApplicationScoped +@Path("/_importmap") +public class ImportmapResource { + private String importmap; + + // See https://github.com/WICG/import-maps/issues/235 + // This does not seem to be supported by browsers yet... + @GET + @Path("/dynamic.importmap") + @Produces("application/importmap+json") + public String importMap() { + return this.importmap; + } + + @GET + @Path("/dynamic-importmap.js") + @Produces("application/javascript") + public String importMapJson() { + return JAVASCRIPT_CODE.formatted(this.importmap); + } + + @PostConstruct + void init() { + Aggregator aggregator = new Aggregator(); + // Add our own mappings + aggregator.addMapping("icons/", "/icons/"); + aggregator.addMapping("components/", "/components/"); + aggregator.addMapping("fonts/", "/fonts/"); + this.importmap = aggregator.aggregateAsJson(); + } + + private static final String JAVASCRIPT_CODE = """ + const im = document.createElement('script'); + im.type = 'importmap'; + im.textContent = JSON.stringify(%s); + document.currentScript.after(im); + """; +} diff --git a/samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/WebSearchRetrievalAugmentor.java b/samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/WebSearchRetrievalAugmentor.java new file mode 100644 index 000000000..dd738319d --- /dev/null +++ b/samples/chatbot-web-search/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/WebSearchRetrievalAugmentor.java @@ -0,0 +1,35 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.rag.DefaultRetrievalAugmentor; +import dev.langchain4j.rag.RetrievalAugmentor; +import dev.langchain4j.rag.content.retriever.WebSearchContentRetriever; +import dev.langchain4j.rag.query.Query; +import dev.langchain4j.web.search.WebSearchEngine; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.util.Collections; +import java.util.function.Supplier; + +@ApplicationScoped +public class WebSearchRetrievalAugmentor implements Supplier { + + @Inject + WebSearchEngine webSearchEngine; + + @Inject + ChatLanguageModel chatModel; + + @Override + public RetrievalAugmentor get() { + return DefaultRetrievalAugmentor.builder() + .queryTransformer((question) -> { + String query = chatModel.generate("Transform the user's question into a suitable query for the " + + "Tavily search engine. The query should yield the results relevant to answering the user's question." + + "User's question: " + question.text()); + return Collections.singleton(Query.from(query)); + }).contentRetriever(new WebSearchContentRetriever(webSearchEngine, 10)) + .build(); + } +} diff --git a/samples/chatbot-web-search/src/main/resources/META-INF/resources/components/demo-chat.js b/samples/chatbot-web-search/src/main/resources/META-INF/resources/components/demo-chat.js new file mode 100644 index 000000000..b1efa6b5b --- /dev/null +++ b/samples/chatbot-web-search/src/main/resources/META-INF/resources/components/demo-chat.js @@ -0,0 +1,31 @@ +import {LitElement} from 'lit'; + + +export class DemoChat extends LitElement { + + connectedCallback() { + const chatBot = document.getElementsByTagName("chat-bot")[0]; + + const socket = new WebSocket("ws://" + window.location.host + "/chatbot"); + socket.onmessage = function (event) { + chatBot.sendMessage(event.data, { + right: false, + sender: {name: 'Bob', id: '007'} + }); + } + + chatBot.addEventListener("sent", function (e) { + if (e.detail.message.right === true) { + // User message + socket.send(e.detail.message.message); + chatBot.sendMessage("", { + right: false, + sender: {name: 'Bob', id: '007'}, + loading: true + }); + } + }); + } +} + +customElements.define('demo-chat', DemoChat); \ No newline at end of file diff --git a/samples/chatbot-web-search/src/main/resources/META-INF/resources/components/demo-title.js b/samples/chatbot-web-search/src/main/resources/META-INF/resources/components/demo-title.js new file mode 100644 index 000000000..937b234de --- /dev/null +++ b/samples/chatbot-web-search/src/main/resources/META-INF/resources/components/demo-title.js @@ -0,0 +1,74 @@ +import {LitElement, html, css} from 'lit'; + +export class DemoTitle extends LitElement { + + static styles = css` + h1 { + font-family: "Red Hat Mono", monospace; + font-size: 60px; + font-style: normal; + font-variant: normal; + font-weight: 700; + line-height: 26.4px; + color: var(--main-highlight-text-color); + } + + .title { + text-align: center; + padding: 1em; + background: var(--main-bg-color); + } + + .explanation { + margin-left: auto; + margin-right: auto; + width: 50%; + text-align: justify; + font-size: 20px; + } + + .explanation img { + max-width: 60%; + display: block; + float:left; + margin-right: 2em; + margin-top: 1em; + } + ` + + render() { + return html` +
+

Web search example

+
+
+ This demo shows how to build a chatbot that can use the Tavily search + engine to look up data on the internet that may be relevant for answering + a user's question. Try opening the chatbot (the red robot button in the + bottom right) and ask a question like "Give me yesterday's news headlines". + As a follow-up question, we suggest for example, to ask for the + source URL of one of the returned headline articles. + + Observe the Quarkus log to see the interaction. + The interaction goes like this: +
+ +
+
    +
  1. The user send a question to the application.
  2. +
  3. If necessary, the model executes the getTodaysDate tool to obtain the current date + (this tool resides in the AdditionalTools class in this project).
  4. +
  5. The model transforms the user's question (and + optionally the obtained date) into a query for the Tavily engine and executes via + the tool that resides in the class dev.langchain4j.web.search.WebSearchTool.
  6. +
  7. Tavily returns the relevant articles.
  8. +
  9. The model extracts an appropriate answer from the articles.
  10. +
+
+ ` + } + + +} + +customElements.define('demo-title', DemoTitle); \ No newline at end of file diff --git a/samples/chatbot-web-search/src/main/resources/META-INF/resources/fonts/red-hat-font.min.css b/samples/chatbot-web-search/src/main/resources/META-INF/resources/fonts/red-hat-font.min.css new file mode 100644 index 000000000..f03010775 --- /dev/null +++ b/samples/chatbot-web-search/src/main/resources/META-INF/resources/fonts/red-hat-font.min.css @@ -0,0 +1 @@ +@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg8z6hR4jNCH5Z.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg_T6hR4jNCA.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg8z6hR4jNCH5Z.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg_T6hR4jNCA.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg8z6hR4jNCH5Z.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg_T6hR4jNCA.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Red Hat Text";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/redhattext/v6/RrQXbohi_ic6B3yVSzGBrMxQZqctMc-JPWCN.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Red Hat Text";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/redhattext/v6/RrQXbohi_ic6B3yVSzGBrMxQaKctMc-JPQ.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Red Hat Text";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/redhattext/v6/RrQXbohi_ic6B3yVSzGBrMxQZqctMc-JPWCN.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Red Hat Text";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/redhattext/v6/RrQXbohi_ic6B3yVSzGBrMxQaKctMc-JPQ.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}/*# sourceMappingURL=red-hat-font.css.map */ \ No newline at end of file diff --git a/samples/chatbot-web-search/src/main/resources/META-INF/resources/index.html b/samples/chatbot-web-search/src/main/resources/META-INF/resources/index.html new file mode 100644 index 000000000..87005b73e --- /dev/null +++ b/samples/chatbot-web-search/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + ChatBot + + + + + + + + +
+ + + +
+ + + + \ No newline at end of file diff --git a/samples/chatbot-web-search/src/main/resources/application.properties b/samples/chatbot-web-search/src/main/resources/application.properties new file mode 100644 index 000000000..f77f4a5d3 --- /dev/null +++ b/samples/chatbot-web-search/src/main/resources/application.properties @@ -0,0 +1 @@ +quarkus.langchain4j.tavily.max-results=5 \ No newline at end of file diff --git a/web-search-engines/tavily/deployment/pom.xml b/web-search-engines/tavily/deployment/pom.xml new file mode 100644 index 000000000..0b3f13e46 --- /dev/null +++ b/web-search-engines/tavily/deployment/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + io.quarkiverse.langchain4j + quarkus-langchain4j-tavily-parent + 999-SNAPSHOT + + quarkus-langchain4j-tavily-deployment + Quarkus LangChain4j - Tavily Web Search Engine - Deployment + + + io.quarkiverse.langchain4j + quarkus-langchain4j-core-deployment + ${project.version} + + + io.quarkus + quarkus-rest-client-reactive-jackson-deployment + + + io.quarkiverse.langchain4j + quarkus-langchain4j-tavily + ${project.version} + + + io.quarkus + quarkus-junit5-internal + test + + + io.quarkiverse.langchain4j + quarkus-langchain4j-testing-internal + ${project.version} + test + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + diff --git a/web-search-engines/tavily/deployment/src/main/java/io/quarkiverse/langchain4j/tavily/deployment/TavilyProcessor.java b/web-search-engines/tavily/deployment/src/main/java/io/quarkiverse/langchain4j/tavily/deployment/TavilyProcessor.java new file mode 100644 index 000000000..3aeca6515 --- /dev/null +++ b/web-search-engines/tavily/deployment/src/main/java/io/quarkiverse/langchain4j/tavily/deployment/TavilyProcessor.java @@ -0,0 +1,47 @@ +package io.quarkiverse.langchain4j.tavily.deployment; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.jandex.ClassType; +import org.jboss.jandex.DotName; + +import dev.langchain4j.web.search.WebSearchEngine; +import io.quarkiverse.langchain4j.tavily.QuarkusTavilyWebSearchEngine; +import io.quarkiverse.langchain4j.tavily.runtime.TavilyConfig; +import io.quarkiverse.langchain4j.tavily.runtime.TavilyRecorder; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.FeatureBuildItem; + +public class TavilyProcessor { + + public static final DotName TAVILY_WEB_SEARCH_ENGINE = DotName.createSimple(QuarkusTavilyWebSearchEngine.class); + + static final String FEATURE = "langchain4j-tavily"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public void createBean( + BuildProducer beanProducer, + TavilyRecorder recorder, + TavilyConfig config) { + beanProducer.produce(SyntheticBeanBuildItem + .configure(TAVILY_WEB_SEARCH_ENGINE) + .types(ClassType.create(WebSearchEngine.class), + ClassType.create(QuarkusTavilyWebSearchEngine.class)) + .defaultBean() + .setRuntimeInit() + .unremovable() + .scope(ApplicationScoped.class) + .supplier(recorder.tavilyEngineSupplier(config)) + .done()); + } +} diff --git a/web-search-engines/tavily/deployment/src/test/java/io/quarkiverse/langchain4j/tavily/test/TavilyTest.java b/web-search-engines/tavily/deployment/src/test/java/io/quarkiverse/langchain4j/tavily/test/TavilyTest.java new file mode 100644 index 000000000..f05efc882 --- /dev/null +++ b/web-search-engines/tavily/deployment/src/test/java/io/quarkiverse/langchain4j/tavily/test/TavilyTest.java @@ -0,0 +1,142 @@ +package io.quarkiverse.langchain4j.tavily.test; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.github.tomakehurst.wiremock.verification.LoggedRequest; + +import dev.langchain4j.web.search.WebSearchEngine; +import dev.langchain4j.web.search.WebSearchOrganicResult; +import dev.langchain4j.web.search.WebSearchRequest; +import dev.langchain4j.web.search.WebSearchResults; +import io.quarkiverse.langchain4j.testing.internal.WiremockAware; +import io.quarkus.test.QuarkusUnitTest; + +public class TavilyTest extends WiremockAware { + + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(new StringAsset( + """ + quarkus.langchain4j.tavily.api-key=test + quarkus.langchain4j.tavily.max-results=9 + quarkus.langchain4j.tavily.base-url=%s + quarkus.langchain4j.tavily.include-domains=yeah.com + quarkus.langchain4j.tavily.exclude-domains=nah.com + """.formatted(WiremockAware.wiremockUrlForConfig())), + "application.properties")); + + @Inject + WebSearchEngine webSearchEngine; + + @BeforeEach + public void setup() { + wiremock().register( + post(urlEqualTo("/search")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody( + """ + { + "query": "Who is Leo Messi?", + "follow_up_questions": null, + "answer": null, + "images": [], + "results": [ + { + "title": "Lionel Messi | Biography, Barcelona, PSG, Ballon d'Or, Inter Miami ...", + "url": "https://www.britannica.com/biography/Lionel-Messi", + "content": "In early 2009 Messi capped off a spectacular 2008–09 season by helping FC Barcelona capture the club’s first “treble” (winning three major European club titles in one season): the team won the La Liga championship, the Copa del Rey (Spain’s major domestic cup), and the Champions League title. Messi’s play continued to rapidly improve over the years, and by 2008 he was one of the most dominant players in the world, finishing second to Manchester United’s Cristiano Ronaldo in the voting for the 2008 Ballon d’Or. At the 2014 World Cup, Messi put on a dazzling display, scoring four goals and almost single-handedly propelling an offense-deficient Argentina team through the group stage and into the knockout rounds, where Argentina then advanced to the World Cup final for the first time in 24 years. After Argentina was defeated in the Copa final—the team’s third consecutive finals loss in a major tournament—Messi said that he was quitting the national team, but his short-lived “retirement” lasted less than two months before he announced his return to the Argentine team. Messi helped Barcelona capture another treble during the 2014–15 season, leading the team with 43 goals scored over the course of the campaign, which resulted in his fifth world player of the year honour.", + "score": 0.98564, + "raw_content": null + }, + { + "title": "Lionel Messi and the unmistakeable sense of an ending", + "url": "https://www.nytimes.com/athletic/5637953/2024/07/15/lionel-messi-argentina-ending-injury/", + "content": "First, he sank to the ground, grimacing. Play continued for a few seconds and then came the communal gasp. Lionel Messi was down. And Lionel Messi is not a player who goes down for nothing ...", + "score": 0.98369, + "raw_content": null + }, + { + "title": "Lionel Messi: Biography, Soccer Player, Inter Miami CF, Athlete", + "url": "https://www.biography.com/athletes/lionel-messi", + "content": "The following year, after Messi heavily criticized the referees in the wake of a 2-0 loss to Brazil in the Copa America semifinals, the Argentine captain was slapped with a three-game ban by the South American Football Confederation.\\n So, at the age of 13, when Messi was offered the chance to train at soccer powerhouse FC Barcelona’s youth academy, La Masia, and have his medical bills covered by the team, Messi’s family picked up and moved across the Atlantic to make a new home in Spain. Famous Athletes\\nDennis Rodman\\nBrett Favre\\nTiger Woods\\nJohn McEnroe\\nKurt Warner\\nSandy Koufax\\n10 Things You Might Not Know About Travis Kelce\\nPeyton Manning\\nJames Harden\\nKobe Bryant\\nStephen Curry\\nKyrie Irving\\nA Part of Hearst Digital Media\\n Their marriage, a civil ceremony dubbed by Argentina’s Clarín newspaper as the “wedding of the century,” was held at a luxury hotel in Rosario, with a number of fellow star soccer players and Colombian pop star Shakira on the 260-person guest list.\\n In 2013, the soccer great came back to earth somewhat due to the persistence of hamstring injuries, but he regained his record-breaking form by becoming the all-time leading scorer in La Liga and Champions League play in late 2014.\\n", + "score": 0.97953, + "raw_content": null + }, + { + "title": "Lionel Messi - Wikipedia", + "url": "https://en.wikipedia.org/wiki/Lionel_Messi", + "content": "He scored twice in the last group match, a 3–2 victory over Nigeria, his second goal coming from a free kick, as they finished first in their group.[423] Messi assisted a late goal in extra time to ensure a 1–0 win against Switzerland in the round of 16, and played in the 1–0 quarter-final win against Belgium as Argentina progressed to the semi-final of the World Cup for the first time since 1990.[424][425] Following a 0–0 draw in extra time, they eliminated the Netherlands 4–2 in a penalty shootout to reach the final, with Messi scoring his team's first penalty.[426]\\nBilled as Messi versus Germany, the world's best player against the best team, the final was a repeat of the 1990 final featuring Diego Maradona.[427] Within the first half-hour, Messi had started the play that led to a goal, but it was ruled offside. \\"[582] Moreover, several pundits and footballing figures, including Maradona, questioned Messi's leadership with Argentina at times, despite his playing ability.[583][584][585] Vickery states the perception of Messi among Argentines changed in 2019, with Messi making a conscious effort to become \\"more one of the group, more Argentine\\", with Vickery adding that following the World Cup victory in 2022 Messi would now be held in the same esteem by his compatriots as Maradona.[581]\\nComparisons with Cristiano Ronaldo\\nAmong his contemporary peers, Messi is most often compared and contrasted with Portuguese forward Cristiano Ronaldo, as part of an ongoing rivalry that has been compared to past sports rivalries like the Muhammad Ali–Joe Frazier rivalry in boxing, the Roger Federer–Rafael Nadal rivalry in tennis, and the Prost–Senna rivalry from Formula One motor racing.[586][587]\\nAlthough Messi has at times denied any rivalry,[588][589] they are widely believed to push one another in their aim to be the best player in the world.[160] Since 2008, Messi has won eight Ballons d'Or to Ronaldo's five,[590] seven FIFA World's Best Player awards to Ronaldo's five, and six European Golden Shoes to Ronaldo's four.[591] Pundits and fans regularly argue the individual merits of both players.[160][592] On 11 July, Messi provided his 20th assist of the league season for Arturo Vidal in a 1–0 away win over Real Valladolid, equalling Xavi's record of 20 assists in a single La Liga season from 2008 to 2009;[281][282] with 22 goals, he also became only the second player ever, after Thierry Henry in the 2002–03 FA Premier League season with Arsenal (24 goals and 20 assists), to record at least 20 goals and 20 assists in a single league season in one of Europe's top-five leagues.[282][283] Following his brace in a 5–0 away win against Alavés in the final match of the season on 20 May, Messi finished the season as both the top scorer and top assist provider in La Liga, with 25 goals and 21 assists respectively, which saw him win his record seventh Pichichi trophy, overtaking Zarra; however, Barcelona missed out on the league title to Real Madrid.[284] On 7 March, two weeks after scoring four goals in a league fixture against Valencia, he scored five times in a Champions League last 16-round match against Bayer Leverkusen, an unprecedented achievement in the history of the competition.[126][127] In addition to being the joint top assist provider with five assists, this feat made him top scorer with 14 goals, tying José Altafini's record from the 1962–63 season, as well as becoming only the second player after Gerd Müller to be top scorer in four campaigns.[128][129] Two weeks later, on 20 March, Messi became the top goalscorer in Barcelona's history at 24 years old, overtaking the 57-year record of César Rodríguez's 232 goals with a hat-trick against Granada.[130]\\nDespite Messi's individual form, Barcelona's four-year cycle of success under Guardiola – one of the greatest eras in the club's history – drew to an end.[131] He still managed to break two longstanding records in a span of seven days: a hat-trick on 16 March against Osasuna saw him overtake Paulino Alcántara's 369 goals to become Barcelona's top goalscorer in all competitions including friendlies, while another hat-trick against Real Madrid on 23 March made him the all-time top scorer in El Clásico, ahead of the 18 goals scored by former Real Madrid player Alfredo Di Stéfano.[160][162] Messi finished the campaign with his worst output in five seasons, though he still managed to score 41 goals in all competitions.[161][163] For the first time in five years, Barcelona ended the season without a major trophy; they were defeated in the Copa del Rey final by Real Madrid and lost the league in the last game to Atlético Madrid, causing Messi to be booed by sections of fans at the Camp Nou.[164]", + "score": 0.97579, + "raw_content": null + }, + { + "title": "The life and times of Lionel Messi", + "url": "https://www.nytimes.com/athletic/4783674/2023/08/18/lionel-messi-profile-soccer/", + "content": "For Messi, it is major trophy number 44.. Despite turning 36 in June, he is as influential as ever. Here is the complete story of Lionel Andres Messi, widely regarded as one of the greatest ...", + "score": 0.96961, + "raw_content": null + } + ], + "response_time": 0.88 + } + """))); + } + + @Test + public void testSearch() { + WebSearchRequest searchRequest = WebSearchRequest.builder() + .searchTerms("testing-query") + .build(); + WebSearchResults result = webSearchEngine.search(searchRequest); + LoggedRequest actualRequest = singleLoggedRequest(); + System.out.println(actualRequest); + + // verify the request + assertEquals(actualRequest.getHeader("Accept"), "application/json"); + assertEquals(actualRequest.getHeader("Content-Type"), "application/json"); + assertEquals(actualRequest.getUrl(), "/search"); + assertEquals(actualRequest.getMethod().toString(), "POST"); + String body = actualRequest.getBodyAsString(); + assertThat(body).contains("\"api_key\":\"test\""); + assertThat(body).contains("\"max_results\":9"); + assertThat(body).contains("\"query\":\"testing-query\""); + assertThat(body).contains("\"include_domains\":[\"yeah.com\"]"); + assertThat(body).contains("\"exclude_domains\":[\"nah.com\"]"); + + // verify the parsed response + assertThat(result.searchInformation().totalResults()).isEqualTo(5); + WebSearchOrganicResult firstResult = result.results().get(0); + assertThat(firstResult.title().equals("Lionel Messi | Biography, Barcelona, PSG, Ballon d'Or, Inter Miami ...")); + assertThat(firstResult.url().equals("https://www.britannica.com/biography/Lionel-Messi")); + assertThat(firstResult.snippet() + .equals(""" + In early 2009 Messi capped off a spectacular 2008–09 season by helping \ + FC Barcelona capture the club’s first “treble” (winning three major European club titles in one season): \ + the team won the La Liga championship, the Copa del Rey (Spain’s major domestic cup), and the Champions League title. \ + Messi’s play continued to rapidly improve over the years, and by 2008 he was one of the most dominant players in the world, \ + finishing second to Manchester United’s Cristiano Ronaldo in the voting for the 2008 Ballon d’Or. At the 2014 World Cup, Messi \ + put on a dazzling display, scoring four goals and almost single-handedly propelling an offense-deficient Argentina team through \ + the group stage and into the knockout rounds, where Argentina then advanced to the World Cup final for the first time in 24 years. \ + After Argentina was defeated in the Copa final—the team’s third consecutive finals loss in a major tournament—Messi said that \ + he was quitting the national team, but his short-lived “retirement” lasted less than two months before he announced his return \ + to the Argentine team. Messi helped Barcelona capture another treble during the 2014–15 season, leading the team with 43 goals \ + scored over the course of the campaign, which resulted in his fifth world player of the year honour.""")); + assertThat(firstResult.metadata().get("score").equals("0.98564")); + } + +} diff --git a/web-search-engines/tavily/pom.xml b/web-search-engines/tavily/pom.xml new file mode 100644 index 000000000..b769e7a62 --- /dev/null +++ b/web-search-engines/tavily/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + io.quarkiverse.langchain4j + quarkus-langchain4j-parent + 999-SNAPSHOT + ../../pom.xml + + quarkus-langchain4j-tavily-parent + Quarkus LangChain4j - Tavily Web Search Engine - Parent + pom + + + deployment + runtime + + + + diff --git a/web-search-engines/tavily/runtime/pom.xml b/web-search-engines/tavily/runtime/pom.xml new file mode 100644 index 000000000..26f767d6b --- /dev/null +++ b/web-search-engines/tavily/runtime/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + io.quarkiverse.langchain4j + quarkus-langchain4j-tavily-parent + 999-SNAPSHOT + + quarkus-langchain4j-tavily + Quarkus LangChain4j - Tavily Web Search Engine - Runtime + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-rest-client-reactive-jackson + + + io.quarkiverse.langchain4j + quarkus-langchain4j-core + ${project.version} + + + io.quarkiverse.langchain4j + quarkus-langchain4j-testing-internal + ${project.version} + test + + + io.quarkus + quarkus-junit5-internal + test + + + + + + + io.quarkus + quarkus-extension-maven-plugin + ${quarkus.version} + + + compile + + extension-descriptor + + + ${project.groupId}:${project.artifactId}-deployment:${project.version} + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + + diff --git a/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/QuarkusTavilyWebSearchEngine.java b/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/QuarkusTavilyWebSearchEngine.java new file mode 100644 index 000000000..8fdb72bf6 --- /dev/null +++ b/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/QuarkusTavilyWebSearchEngine.java @@ -0,0 +1,187 @@ +package io.quarkiverse.langchain4j.tavily; + +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; +import static java.util.stream.StreamSupport.stream; + +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.client.api.ClientLogger; +import org.jboss.resteasy.reactive.client.api.LoggingScope; + +import dev.langchain4j.web.search.WebSearchEngine; +import dev.langchain4j.web.search.WebSearchInformationResult; +import dev.langchain4j.web.search.WebSearchOrganicResult; +import dev.langchain4j.web.search.WebSearchRequest; +import dev.langchain4j.web.search.WebSearchResults; +import io.quarkiverse.langchain4j.tavily.runtime.TavilyClient; +import io.quarkiverse.langchain4j.tavily.runtime.TavilyResponse; +import io.quarkiverse.langchain4j.tavily.runtime.TavilySearchRequest; +import io.quarkiverse.langchain4j.tavily.runtime.TavilySearchResult; +import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; + +// TODO: use the upstream implementation once it doesn't depend on OkHttp etc. +public class QuarkusTavilyWebSearchEngine implements WebSearchEngine { + + private final TavilyClient tavilyClient; + private final String apiKey; + private final Integer maxResults; + private final SearchDepth searchDepth; + private final boolean includeAnswer; + private final boolean includeRawContent; + private final List includeDomains; + private final List excludeDomains; + + public QuarkusTavilyWebSearchEngine(String baseUrl, + String apiKey, + Integer maxResults, + Duration timeout, + boolean logRequests, + boolean logResponses, + SearchDepth searchDepth, + boolean includeAnswer, + boolean includeRawContent, + List includeDomains, + List excludeDomains) { + this.apiKey = apiKey; + this.maxResults = maxResults; + this.searchDepth = searchDepth; + this.includeAnswer = includeAnswer; + this.includeRawContent = includeRawContent; + this.includeDomains = includeDomains; + this.excludeDomains = excludeDomains; + try { + QuarkusRestClientBuilder builder = QuarkusRestClientBuilder.newBuilder() + .baseUri(new URI(baseUrl)) + .connectTimeout(timeout.toSeconds(), TimeUnit.SECONDS) + .readTimeout(timeout.toSeconds(), TimeUnit.SECONDS); + if (logRequests || logResponses) { + builder = builder + .loggingScope(LoggingScope.REQUEST_RESPONSE) + .clientLogger(new TavilyClientLogger(logRequests, logResponses)); + } + tavilyClient = builder.build(TavilyClient.class); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + @Override + public WebSearchResults search(WebSearchRequest webSearchRequest) { + TavilySearchRequest tavilySearchRequest = new TavilySearchRequest( + apiKey, + webSearchRequest.searchTerms(), + searchDepth.toString().toLowerCase(), + includeAnswer, + includeRawContent, + webSearchRequest.maxResults() != null ? webSearchRequest.maxResults() : maxResults, + includeDomains, + excludeDomains); + TavilyResponse tavilyResponse = tavilyClient.search(tavilySearchRequest); + + final List results = tavilyResponse.getResults().stream() + .map(QuarkusTavilyWebSearchEngine::toWebSearchOrganicResult) + .collect(toList()); + + if (tavilyResponse.getAnswer() != null) { + WebSearchOrganicResult answerResult = WebSearchOrganicResult.from( + "Tavily Search API", + URI.create("https://tavily.com/"), + tavilyResponse.getAnswer(), + null); + results.add(0, answerResult); + } + + return WebSearchResults.from(WebSearchInformationResult.from((long) results.size()), results); + } + + private static WebSearchOrganicResult toWebSearchOrganicResult(TavilySearchResult tavilySearchResult) { + return WebSearchOrganicResult.from(tavilySearchResult.getTitle(), + URI.create(tavilySearchResult.getUrl().replaceAll(" ", "%20")), + tavilySearchResult.getContent(), + tavilySearchResult.getRawContent(), + Collections.singletonMap("score", String.valueOf(tavilySearchResult.getScore()))); + } + + static class TavilyClientLogger implements ClientLogger { + private static final Logger log = Logger.getLogger(TavilyClientLogger.class); + + private final boolean logRequests; + private final boolean logResponses; + + public TavilyClientLogger(boolean logRequests, boolean logResponses) { + this.logRequests = logRequests; + this.logResponses = logResponses; + } + + @Override + public void setBodySize(int bodySize) { + // ignore + } + + @Override + public void logRequest(HttpClientRequest request, Buffer body, boolean omitBody) { + if (!logRequests || !log.isInfoEnabled()) { + return; + } + try { + log.infof("Request:\n- method: %s\n- url: %s\n- headers: %s\n- body: %s", + request.getMethod(), + request.absoluteURI(), + inOneLine(request.headers()), + bodyToString(body)); + } catch (Exception e) { + log.warn("Failed to log request", e); + } + } + + @Override + public void logResponse(HttpClientResponse response, boolean redirect) { + if (!logResponses || !log.isInfoEnabled()) { + return; + } + response.bodyHandler(new io.vertx.core.Handler<>() { + @Override + public void handle(Buffer body) { + try { + log.infof( + "Response:\n- status code: %s\n- headers: %s\n- body: %s", + response.statusCode(), + inOneLine(response.headers()), + bodyToString(body)); + } catch (Exception e) { + log.warn("Failed to log response", e); + } + } + }); + } + + private String bodyToString(Buffer body) { + if (body == null) { + return ""; + } + return body.toString(); + } + + private String inOneLine(io.vertx.core.MultiMap headers) { + + return stream(headers.spliterator(), false) + .map(header -> { + String headerKey = header.getKey(); + String headerValue = header.getValue(); + return String.format("[%s: %s]", headerKey, headerValue); + }) + .collect(joining(", ")); + } + + } +} diff --git a/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/SearchDepth.java b/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/SearchDepth.java new file mode 100644 index 000000000..2afdd3272 --- /dev/null +++ b/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/SearchDepth.java @@ -0,0 +1,6 @@ +package io.quarkiverse.langchain4j.tavily; + +public enum SearchDepth { + BASIC, + ADVANCED +} diff --git a/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilyClient.java b/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilyClient.java new file mode 100644 index 000000000..4d4ffc67d --- /dev/null +++ b/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilyClient.java @@ -0,0 +1,17 @@ +package io.quarkiverse.langchain4j.tavily.runtime; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public interface TavilyClient { + + @POST + @Path("/search") + TavilyResponse search(TavilySearchRequest request); +} diff --git a/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilyConfig.java b/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilyConfig.java new file mode 100644 index 000000000..fe303da22 --- /dev/null +++ b/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilyConfig.java @@ -0,0 +1,89 @@ +package io.quarkiverse.langchain4j.tavily.runtime; + +import static io.quarkus.runtime.annotations.ConfigPhase.RUN_TIME; + +import java.time.Duration; +import java.util.List; +import java.util.Optional; + +import io.quarkiverse.langchain4j.tavily.SearchDepth; +import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@ConfigRoot(phase = RUN_TIME) +@ConfigMapping(prefix = "quarkus.langchain4j.tavily") +public interface TavilyConfig { + + /** + * Base URL of the Tavily API + */ + @WithDefault("https://api.tavily.com") + String baseUrl(); + + /** + * API key for the Tavily API + */ + String apiKey(); + + /** + * Maximum number of results to return + */ + @WithDefault("5") + Integer maxResults(); + + /** + * The timeout duration for Tavily requests. + */ + // Note: probably should not default to the value of the global quarkus.langchain4j.timeout, + // because 10 seconds is too short for Tavily + @WithDefault("60s") + Duration timeout(); + + /** + * Whether requests to Tavily should be logged + */ + @ConfigDocDefault("false") + @WithDefault("${quarkus.langchain4j.log-requests}") + Optional logRequests(); + + /** + * Whether responses from Tavily should be logged + */ + @ConfigDocDefault("false") + @WithDefault("${quarkus.langchain4j.log-requests}") + Optional logResponses(); + + /** + * The search depth to use. This can be "basic" or "advanced". + * Basic is the default. + */ + @WithDefault("BASIC") + SearchDepth searchDepth(); + + /** + * Include a short answer to original query. Default is false. + */ + @WithDefault("false") + boolean includeAnswer(); + + /** + * Include the cleaned and parsed HTML content of each search result. Default is false. + */ + @WithDefault("false") + boolean includeRawContent(); + + /** + * A list of domains to specifically include in the search results. Default is [], which includes all domains. + */ + @WithDefault("[]") + List includeDomains(); + + /** + * A list of domains to specifically exclude from the search results. Default is [], which doesn't exclude any domains. + */ + @WithDefault("[]") + List excludeDomains(); + +} diff --git a/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilyRecorder.java b/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilyRecorder.java new file mode 100644 index 000000000..649f37b43 --- /dev/null +++ b/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilyRecorder.java @@ -0,0 +1,29 @@ +package io.quarkiverse.langchain4j.tavily.runtime; + +import java.util.function.Supplier; + +import io.quarkiverse.langchain4j.tavily.QuarkusTavilyWebSearchEngine; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class TavilyRecorder { + + public Supplier tavilyEngineSupplier(TavilyConfig config) { + return new Supplier<>() { + @Override + public QuarkusTavilyWebSearchEngine get() { + return new QuarkusTavilyWebSearchEngine(config.baseUrl(), + config.apiKey(), + config.maxResults(), + config.timeout(), + config.logRequests().orElse(false), + config.logResponses().orElse(false), + config.searchDepth(), + config.includeAnswer(), + config.includeRawContent(), + config.includeDomains(), + config.excludeDomains()); + } + }; + } +} diff --git a/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilyResponse.java b/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilyResponse.java new file mode 100644 index 000000000..760330a32 --- /dev/null +++ b/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilyResponse.java @@ -0,0 +1,51 @@ +package io.quarkiverse.langchain4j.tavily.runtime; + +import java.util.List; + +public class TavilyResponse { + + private final String answer; + private final String query; + private final Double responseTime; + private final List images; + private final List followUpQuestions; + private final List results; + + public TavilyResponse(String answer, + String query, + Double responseTime, + List images, + List followUpQuestions, + List results) { + this.answer = answer; + this.query = query; + this.responseTime = responseTime; + this.images = images; + this.followUpQuestions = followUpQuestions; + this.results = results; + } + + public String getAnswer() { + return answer; + } + + public String getQuery() { + return query; + } + + public Double getResponseTime() { + return responseTime; + } + + public List getImages() { + return images; + } + + public List getFollowUpQuestions() { + return followUpQuestions; + } + + public List getResults() { + return results; + } +} diff --git a/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilySearchRequest.java b/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilySearchRequest.java new file mode 100644 index 000000000..327ca0321 --- /dev/null +++ b/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilySearchRequest.java @@ -0,0 +1,77 @@ +package io.quarkiverse.langchain4j.tavily.runtime; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TavilySearchRequest { + + @JsonProperty("api_key") + private final String apiKey; + + private final String query; + + @JsonProperty("search_depth") + private final String searchDepth; + + @JsonProperty("include_answer") + private final Boolean includeAnswer; + + @JsonProperty("include_raw_content") + private final Boolean includeRawContent; + + @JsonProperty("max_results") + private final Integer maxResults; + + @JsonProperty("include_domains") + private final List includeDomains; + + @JsonProperty("exclude_domains") + private final List excludeDomains; + + public TavilySearchRequest(String apiKey, String query, + String searchDepth, Boolean includeAnswer, + Boolean includeRawContent, Integer maxResults, + List includeDomains, List excludeDomains) { + this.apiKey = apiKey; + this.query = query; + this.searchDepth = searchDepth; + this.includeAnswer = includeAnswer; + this.includeRawContent = includeRawContent; + this.maxResults = maxResults; + this.includeDomains = includeDomains; + this.excludeDomains = excludeDomains; + } + + public String getApiKey() { + return apiKey; + } + + public String getQuery() { + return query; + } + + public String getSearchDepth() { + return searchDepth; + } + + public Boolean getIncludeAnswer() { + return includeAnswer; + } + + public Boolean getIncludeRawContent() { + return includeRawContent; + } + + public Integer getMaxResults() { + return maxResults; + } + + public List getIncludeDomains() { + return includeDomains; + } + + public List getExcludeDomains() { + return excludeDomains; + } +} diff --git a/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilySearchResult.java b/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilySearchResult.java new file mode 100644 index 000000000..4d42a9892 --- /dev/null +++ b/web-search-engines/tavily/runtime/src/main/java/io/quarkiverse/langchain4j/tavily/runtime/TavilySearchResult.java @@ -0,0 +1,44 @@ +package io.quarkiverse.langchain4j.tavily.runtime; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TavilySearchResult { + + private final String title; + private final String url; + private final String content; + + @JsonProperty("raw_content") + private final String rawContent; + private final Double score; + + public TavilySearchResult(String title, String url, + String content, String rawContent, + Double score) { + this.title = title; + this.url = url; + this.content = content; + this.rawContent = rawContent; + this.score = score; + } + + public String getTitle() { + return title; + } + + public String getUrl() { + return url; + } + + public String getContent() { + return content; + } + + public String getRawContent() { + return rawContent; + } + + public Double getScore() { + return score; + } +} diff --git a/web-search-engines/tavily/runtime/src/main/resources/META-INF/beans.xml b/web-search-engines/tavily/runtime/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..e69de29bb diff --git a/web-search-engines/tavily/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/web-search-engines/tavily/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 000000000..8022165a5 --- /dev/null +++ b/web-search-engines/tavily/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,12 @@ +name: LangChain4j Tavily Web Search Engine +artifact: ${project.groupId}:${project.artifactId}:${project.version} +description: Provides the Tavily Web search engine for LangChain4j +metadata: + keywords: + - ai + - langchain4j + guide: "https://docs.quarkiverse.io/quarkus-langchain4j/dev/index.html" + categories: + - "ai" + status: "experimental" +