From 132dd4d73360fd68ddbf83a819ecfb66b29371a4 Mon Sep 17 00:00:00 2001 From: Joe McElroy Date: Fri, 5 Apr 2024 10:12:57 +0100 Subject: [PATCH] [Search] [Playground] View code Flyout (#180083) Updates view code flyout to contain: - better working example of the python elasticsearch client and python langchain example - the template is updated based on the settings chosen by the developer - dependencies for pip install updated image --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../{server/utils => common}/prompt.ts | 0 .../search_playground/common/routes.ts | 8 ++ x-pack/plugins/search_playground/kibana.jsonc | 3 + .../py_lang_client.test.tsx.snap | 88 ++++++++++++ .../py_langchain_python.test.tsx.snap | 75 ++++++++++ .../examples/py_lang_client.test.tsx | 35 +++++ .../view_code/examples/py_lang_client.tsx | 87 ++++++++++++ .../examples/py_langchain_python.test.tsx | 35 +++++ .../examples/py_langchain_python.tsx | 80 +++++++++++ .../components/view_code/view_code_flyout.tsx | 128 ++++++++++-------- .../plugins/search_playground/public/types.ts | 4 +- .../search_playground/server/routes.ts | 2 +- .../plugins/search_playground/tsconfig.json | 3 +- 13 files changed, 492 insertions(+), 56 deletions(-) rename x-pack/plugins/search_playground/{server/utils => common}/prompt.ts (100%) create mode 100644 x-pack/plugins/search_playground/common/routes.ts create mode 100644 x-pack/plugins/search_playground/public/components/view_code/examples/__snapshots__/py_lang_client.test.tsx.snap create mode 100644 x-pack/plugins/search_playground/public/components/view_code/examples/__snapshots__/py_langchain_python.test.tsx.snap create mode 100644 x-pack/plugins/search_playground/public/components/view_code/examples/py_lang_client.test.tsx create mode 100644 x-pack/plugins/search_playground/public/components/view_code/examples/py_lang_client.tsx create mode 100644 x-pack/plugins/search_playground/public/components/view_code/examples/py_langchain_python.test.tsx create mode 100644 x-pack/plugins/search_playground/public/components/view_code/examples/py_langchain_python.tsx diff --git a/x-pack/plugins/search_playground/server/utils/prompt.ts b/x-pack/plugins/search_playground/common/prompt.ts similarity index 100% rename from x-pack/plugins/search_playground/server/utils/prompt.ts rename to x-pack/plugins/search_playground/common/prompt.ts diff --git a/x-pack/plugins/search_playground/common/routes.ts b/x-pack/plugins/search_playground/common/routes.ts new file mode 100644 index 00000000000000..68e1dffeec4f0b --- /dev/null +++ b/x-pack/plugins/search_playground/common/routes.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const MANAGEMENT_API_KEYS = '/app/management/security/api_keys'; diff --git a/x-pack/plugins/search_playground/kibana.jsonc b/x-pack/plugins/search_playground/kibana.jsonc index 61f4cd97c1507e..3f4837613fc9b2 100644 --- a/x-pack/plugins/search_playground/kibana.jsonc +++ b/x-pack/plugins/search_playground/kibana.jsonc @@ -14,6 +14,9 @@ "navigation", "security" ], + "optionalPlugins": [ + "cloud" + ], "requiredBundles": [ "kibanaReact" ] diff --git a/x-pack/plugins/search_playground/public/components/view_code/examples/__snapshots__/py_lang_client.test.tsx.snap b/x-pack/plugins/search_playground/public/components/view_code/examples/__snapshots__/py_lang_client.test.tsx.snap new file mode 100644 index 00000000000000..25d1f6cce4b610 --- /dev/null +++ b/x-pack/plugins/search_playground/public/components/view_code/examples/__snapshots__/py_lang_client.test.tsx.snap @@ -0,0 +1,88 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PY_LANG_CLIENT function renders with correct content 1`] = ` +"## Install the required packages +## pip install -qU elasticsearch openai + +import os +from elasticsearch import Elasticsearch +from openai import OpenAI + + +es_client = Elasticsearch( + http://my-local-cloud-instance, + api_key=os.environ[\\"ES_API_KEY\\"] +) + + +openai_client = OpenAI( + api_key=os.environ[\\"OPENAI_API_KEY\\"], +) + +def get_elasticsearch_results(query): + es_query = {} + + result = es.search(index=\\"index1,index2\\", query=es_query, size=10) + return result[\\"hits\\"][\\"hits\\"] + +def create_openai_prompt(question, results): + + context = \\"\\" + index_source_fields = { + \\"index1\\": [ + \\"field1\\" + ], + \\"index2\\": [ + \\"field2\\" + ] +} + for hit in results: + source_field = index_source_fields.get(hit[\\"_index\\"])[0] + hit_context = hit[\\"_source\\"][source_field] + context += f\\"{hit_context} +\\" + + prompt = f\\"\\"\\" + Instructions: + + - Your prompt + - Answer questions truthfully and factually using only the information presented. + - If you don't know the answer, just say that you don't know, don't make up an answer! + - You must always cite the document where the answer was extracted using inline academic citation style [], using the position. + - Use markdown format for code examples. + - You are correct, factual, precise, and reliable. + + + Context: + {context} + + Question: {question} + Answer: + \\"\\"\\" + + return prompt + +def generate_openai_completion(user_prompt): + response = openai_client.chat.completions.create( + model=\\"Your-new-model\\", + messages=[ + {\\"role\\": \\"system\\", \\"content\\": \\"You are an assistant for question-answering tasks.\\"}, + {\\"role\\": \\"user\\", \\"content\\": user_prompt}, + ] + ) + + return response.choices[0].message.content + +if __name__ == \\"__main__\\": + question = \\"my question\\" + + elasticsearch_results = get_elasticsearch_results(question) + + context_prompt = create_openai_prompt(question, elasticsearch_results) + + openai_completion = generate_openai_completion(context_prompt) + + print(openai_completion) + +" +`; diff --git a/x-pack/plugins/search_playground/public/components/view_code/examples/__snapshots__/py_langchain_python.test.tsx.snap b/x-pack/plugins/search_playground/public/components/view_code/examples/__snapshots__/py_langchain_python.test.tsx.snap new file mode 100644 index 00000000000000..7474cbe8b64d4d --- /dev/null +++ b/x-pack/plugins/search_playground/public/components/view_code/examples/__snapshots__/py_langchain_python.test.tsx.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PY_LANGCHAIN function renders with correct content 1`] = ` +"## Install the required packages +## pip install -qU elasticsearch langchain langchain-elasticsearch langchain-openai + +from langchain_elasticsearch import ElasticsearchRetriever +from langchain_openai import ChatOpenAI +from langchain_core.runnables import RunnablePassthrough +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.output_parsers import StrOutputParser +from langchain_core.prompts import format_document +from langchain.prompts.prompt import PromptTemplate +import os + + +es_client = Elasticsearch( + http://my-local-cloud-instance, + api_key=os.environ[\\"ES_API_KEY\\"] +) + + +def build_query(query): + return { + \\"query\\": {} +} + +retriever = ElasticsearchRetriever( + index_name=\\"{formValues.indices.join(',')}\\", + body_func=build_query, + content_field=\\"text\\", + es_client=es_client +) + +model = ChatOpenAI(openai_api_key=os.environ[\\"OPENAI_API_KEY\\"], model_name=\\"Your-new-model\\") + + +ANSWER_PROMPT = ChatPromptTemplate.from_template( + f\\"\\"\\" + Instructions: + + - Your prompt + - Answer questions truthfully and factually using only the information presented. + - If you don't know the answer, just say that you don't know, don't make up an answer! + - You must always cite the document where the answer was extracted using inline academic citation style [], using the position. + - Use markdown format for code examples. + - You are correct, factual, precise, and reliable. + + + Context: + {context} + + Question: {question} + Answer: + \\"\\"\\" +) + +DEFAULT_DOCUMENT_PROMPT = PromptTemplate.from_template(template=\\"{page_content}\\") + +def _combine_documents( + docs, document_prompt=DEFAULT_DOCUMENT_PROMPT, document_separator=\\"\\\\n\\\\n\\" +): + doc_strings = [format_document(doc, document_prompt) for doc in docs] + return document_separator.join(doc_strings) + +_context = { + \\"context\\": retriever | _combine_documents, + \\"question\\": RunnablePassthrough(), +} + +chain = _context | ANSWER_PROMPT | model | StrOutputParser() +ans = chain.invoke(\\"what is the nasa sales team?\\") +print(\\"---- Answer ----\\") +print(ans)" +`; diff --git a/x-pack/plugins/search_playground/public/components/view_code/examples/py_lang_client.test.tsx b/x-pack/plugins/search_playground/public/components/view_code/examples/py_lang_client.test.tsx new file mode 100644 index 00000000000000..13f666a78f14dc --- /dev/null +++ b/x-pack/plugins/search_playground/public/components/view_code/examples/py_lang_client.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '@testing-library/react'; +import { PY_LANG_CLIENT } from './py_lang_client'; // Adjust the import path according to your project structure +import { ES_CLIENT_DETAILS } from '../view_code_flyout'; +import { CloudSetup } from '@kbn/cloud-plugin/public'; +import { ChatForm } from '../../../types'; + +describe('PY_LANG_CLIENT function', () => { + test('renders with correct content', () => { + // Mocking necessary values for your function + const formValues = { + elasticsearch_query: { query: {} }, + indices: ['index1', 'index2'], + docSize: 10, + source_fields: { index1: ['field1'], index2: ['field2'] }, + prompt: 'Your prompt', + citations: true, + summarization_model: 'Your-new-model', + } as unknown as ChatForm; + + const clientDetails = ES_CLIENT_DETAILS({ + elasticsearchUrl: 'http://my-local-cloud-instance', + } as unknown as CloudSetup); + + const { container } = render(PY_LANG_CLIENT(formValues, clientDetails)); + + expect(container.firstChild?.textContent).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/search_playground/public/components/view_code/examples/py_lang_client.tsx b/x-pack/plugins/search_playground/public/components/view_code/examples/py_lang_client.tsx new file mode 100644 index 00000000000000..bb4543b7b8f0c5 --- /dev/null +++ b/x-pack/plugins/search_playground/public/components/view_code/examples/py_lang_client.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiCodeBlock } from '@elastic/eui'; +import React from 'react'; +import { ChatForm } from '../../../types'; +import { Prompt } from '../../../../common/prompt'; + +const getESQuery = (query: any) => { + try { + return JSON.stringify(query, null, 2).replace('"${query}"', 'f"${query}"'); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error parsing ES query', e); + return '{}'; + } +}; + +export const PY_LANG_CLIENT = (formValues: ChatForm, clientDetails: string) => ( + + {`## Install the required packages +## pip install -qU elasticsearch openai + +import os +from elasticsearch import Elasticsearch +from openai import OpenAI + +${clientDetails} + +openai_client = OpenAI( + api_key=os.environ["OPENAI_API_KEY"], +) + +def get_elasticsearch_results(query): + es_query = ${getESQuery(formValues.elasticsearch_query.query)} + + result = es.search(index="${formValues.indices.join(',')}", query=es_query, size=${ + formValues.docSize + }) + return result["hits"]["hits"] + +def create_openai_prompt(question, results): + + context = "" + index_source_fields = ${JSON.stringify(formValues.source_fields, null, 2)} + for hit in results: + source_field = index_source_fields.get(hit["_index"])[0] + hit_context = hit["_source"][source_field] + context += f"{hit_context}\n" + + prompt = f"""${Prompt(formValues.prompt, { + context: true, + citations: formValues.citations, + type: 'openai', + })}""" + + return prompt + +def generate_openai_completion(user_prompt): + response = openai_client.chat.completions.create( + model="${formValues.summarization_model}", + messages=[ + {"role": "system", "content": "You are an assistant for question-answering tasks."}, + {"role": "user", "content": user_prompt}, + ] + ) + + return response.choices[0].message.content + +if __name__ == "__main__": + question = "my question" + + elasticsearch_results = get_elasticsearch_results(question) + + context_prompt = create_openai_prompt(question, elasticsearch_results) + + openai_completion = generate_openai_completion(context_prompt) + + print(openai_completion) + +`} + +); diff --git a/x-pack/plugins/search_playground/public/components/view_code/examples/py_langchain_python.test.tsx b/x-pack/plugins/search_playground/public/components/view_code/examples/py_langchain_python.test.tsx new file mode 100644 index 00000000000000..b9ab32eddb7208 --- /dev/null +++ b/x-pack/plugins/search_playground/public/components/view_code/examples/py_langchain_python.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '@testing-library/react'; +import { ES_CLIENT_DETAILS } from '../view_code_flyout'; +import { CloudSetup } from '@kbn/cloud-plugin/public'; +import { ChatForm } from '../../../types'; +import { LANGCHAIN_PYTHON } from './py_langchain_python'; + +describe('PY_LANGCHAIN function', () => { + test('renders with correct content', () => { + // Mocking necessary values for your function + const formValues = { + elasticsearch_query: { query: {} }, + indices: ['index1', 'index2'], + docSize: 10, + source_fields: { index1: ['field1'], index2: ['field2'] }, + prompt: 'Your prompt', + citations: true, + summarization_model: 'Your-new-model', + } as unknown as ChatForm; + + const clientDetails = ES_CLIENT_DETAILS({ + elasticsearchUrl: 'http://my-local-cloud-instance', + } as unknown as CloudSetup); + + const { container } = render(LANGCHAIN_PYTHON(formValues, clientDetails)); + + expect(container.firstChild?.textContent).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/search_playground/public/components/view_code/examples/py_langchain_python.tsx b/x-pack/plugins/search_playground/public/components/view_code/examples/py_langchain_python.tsx new file mode 100644 index 00000000000000..cd02395c9c1658 --- /dev/null +++ b/x-pack/plugins/search_playground/public/components/view_code/examples/py_langchain_python.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiCodeBlock } from '@elastic/eui'; +import React from 'react'; +import { ChatForm } from '../../../types'; +import { Prompt } from '../../../../common/prompt'; + +const getESQuery = (query: any) => { + try { + return JSON.stringify(query, null, 2).replace('"${query}"', 'f"${query}"'); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error parsing ES query', e); + return '{}'; + } +}; + +export const LANGCHAIN_PYTHON = (formValues: ChatForm, clientDetails: string) => ( + + {`## Install the required packages +## pip install -qU elasticsearch langchain langchain-elasticsearch langchain-openai + +from langchain_elasticsearch import ElasticsearchRetriever +from langchain_openai import ChatOpenAI +from langchain_core.runnables import RunnablePassthrough +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.output_parsers import StrOutputParser +from langchain_core.prompts import format_document +from langchain.prompts.prompt import PromptTemplate +import os + +${clientDetails} + +def build_query(query): + return ${getESQuery(formValues.elasticsearch_query)} + +retriever = ElasticsearchRetriever( + index_name="{formValues.indices.join(',')}", + body_func=build_query, + content_field="text", + es_client=es_client +) + +model = ChatOpenAI(openai_api_key=os.environ["OPENAI_API_KEY"], model_name="${ + formValues.summarization_model + }") + + +ANSWER_PROMPT = ChatPromptTemplate.from_template( + f"""${Prompt(formValues.prompt, { + context: true, + citations: formValues.citations, + type: 'openai', + })}""" +) + +DEFAULT_DOCUMENT_PROMPT = PromptTemplate.from_template(template="{page_content}") + +def _combine_documents( + docs, document_prompt=DEFAULT_DOCUMENT_PROMPT, document_separator="\\n\\n" +): + doc_strings = [format_document(doc, document_prompt) for doc in docs] + return document_separator.join(doc_strings) + +_context = { + "context": retriever | _combine_documents, + "question": RunnablePassthrough(), +} + +chain = _context | ANSWER_PROMPT | model | StrOutputParser() +ans = chain.invoke("what is the nasa sales team?") +print("---- Answer ----") +print(ans)`} + +); diff --git a/x-pack/plugins/search_playground/public/components/view_code/view_code_flyout.tsx b/x-pack/plugins/search_playground/public/components/view_code/view_code_flyout.tsx index d11e89ee6fc9fe..a914da997d2766 100644 --- a/x-pack/plugins/search_playground/public/components/view_code/view_code_flyout.tsx +++ b/x-pack/plugins/search_playground/public/components/view_code/view_code_flyout.tsx @@ -6,69 +6,62 @@ */ import { - EuiFormLabel, - EuiCodeBlock, EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiSpacer, - EuiSteps, - EuiText, EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiSelect, + EuiText, + EuiButtonEmpty, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useMemo } from 'react'; -import { CreateApiKeyForm } from './create_api_key_form'; +import React, { useState } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { CloudSetup } from '@kbn/cloud-plugin/public'; +import { ChatForm } from '../../types'; +import { useKibana } from '../../hooks/use_kibana'; +import { MANAGEMENT_API_KEYS } from '../../../common/routes'; +import { LANGCHAIN_PYTHON } from './examples/py_langchain_python'; +import { PY_LANG_CLIENT } from './examples/py_lang_client'; interface ViewCodeFlyoutProps { onClose: () => void; } +export const ES_CLIENT_DETAILS = (cloud: CloudSetup | undefined) => { + if (cloud) { + return ` +es_client = Elasticsearch( + ${cloud.elasticsearchUrl}, + api_key=os.environ["ES_API_KEY"] +) + `; + } + + return ` +es_client = Elasticsearch( + "" +) + `; +}; + export const ViewCodeFlyout: React.FC = ({ onClose }) => { - const steps = useMemo( - () => [ - { - title: i18n.translate('xpack.searchPlayground.viewCode.flyout.step.apiKeyTitle', { - defaultMessage: 'Generate and copy an API key', - }), - children: ( - <> - -

- -

-
- - - - ), - }, - { - title: i18n.translate('xpack.searchPlayground.viewCode.flyout.step.createApplication', { - defaultMessage: 'Create application', - }), - children: ( - <> - - - - - - npm install - - - ), - }, - ], - [] - ); + const [selectedLanguage, setSelectedLanguage] = useState('py-es-client'); + const { getValues } = useFormContext(); + const formValues = getValues(); + const { + services: { cloud, http }, + } = useKibana(); + + const CLIENT_STEP = ES_CLIENT_DETAILS(cloud); + + const steps: Record = { + 'lc-py': LANGCHAIN_PYTHON(formValues, CLIENT_STEP), + 'py-es-client': PY_LANG_CLIENT(formValues, CLIENT_STEP), + }; return ( @@ -77,7 +70,7 @@ export const ViewCodeFlyout: React.FC = ({ onClose }) => {

@@ -86,13 +79,42 @@ export const ViewCodeFlyout: React.FC = ({ onClose }) => {

- + + + + + setSelectedLanguage(e.target.value)} + value={selectedLanguage} + /> + + + + + + + + + {steps[selectedLanguage]} +
); diff --git a/x-pack/plugins/search_playground/public/types.ts b/x-pack/plugins/search_playground/public/types.ts index 8536b9a19a9562..86c6c78ebaa643 100644 --- a/x-pack/plugins/search_playground/public/types.ts +++ b/x-pack/plugins/search_playground/public/types.ts @@ -17,6 +17,7 @@ import { SecurityPluginStart } from '@kbn/security-plugin/public'; import { HttpStart } from '@kbn/core-http-browser'; import React from 'react'; import { SharePluginStart } from '@kbn/share-plugin/public'; +import { CloudSetup } from '@kbn/cloud-plugin/public'; import type { App } from './components/app'; import type { PlaygroundProvider as PlaygroundProviderComponent } from './providers/playground_provider'; import type { Toolbar } from './components/toolbar'; @@ -39,6 +40,7 @@ export interface AppServicesContext { http: HttpStart; security: SecurityPluginStart; share: SharePluginStart; + cloud?: CloudSetup; } export enum ChatFormFields { @@ -60,7 +62,7 @@ export interface ChatForm { [ChatFormFields.openAIKey]: string; [ChatFormFields.indices]: string[]; [ChatFormFields.summarizationModel]: string; - [ChatFormFields.elasticsearchQuery]: QueryDslQueryContainer; + [ChatFormFields.elasticsearchQuery]: { query: QueryDslQueryContainer }; [ChatFormFields.sourceFields]: string[]; [ChatFormFields.docSize]: number; } diff --git a/x-pack/plugins/search_playground/server/routes.ts b/x-pack/plugins/search_playground/server/routes.ts index 15f514b4fd0e74..fe2631065c281c 100644 --- a/x-pack/plugins/search_playground/server/routes.ts +++ b/x-pack/plugins/search_playground/server/routes.ts @@ -13,7 +13,7 @@ import { IRouter } from '@kbn/core/server'; import { fetchFields } from './utils/fetch_query_source_fields'; import { AssistClientOptionsWithClient, createAssist as Assist } from './utils/assist'; import { ConversationalChain } from './utils/conversational_chain'; -import { Prompt } from './utils/prompt'; +import { Prompt } from '../common/prompt'; import { errorHandler } from './utils/error_handler'; import { APIRoutes } from './types'; diff --git a/x-pack/plugins/search_playground/tsconfig.json b/x-pack/plugins/search_playground/tsconfig.json index 4373613fc2d7b8..63f366614ffb59 100644 --- a/x-pack/plugins/search_playground/tsconfig.json +++ b/x-pack/plugins/search_playground/tsconfig.json @@ -26,7 +26,8 @@ "@kbn/shared-ux-page-kibana-template", "@kbn/navigation-plugin", "@kbn/core-http-server", - "@kbn/share-plugin" + "@kbn/share-plugin", + "@kbn/cloud-plugin" ], "exclude": [ "target/**/*",