From d2cce54bf1930312be04405750aad40882d2353e Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Mon, 18 Dec 2023 14:00:18 -0800 Subject: [PATCH] WIP: sql research assistant (#14240) --- .../how_to/generators.ipynb | 2 +- templates/sql-research-assistant/.gitignore | 1 + templates/sql-research-assistant/LICENSE | 21 +++ templates/sql-research-assistant/README.md | 62 +++++++ .../sql-research-assistant/pyproject.toml | 24 +++ .../sql_research_assistant/__init__.py | 3 + .../sql_research_assistant/chain.py | 22 +++ .../sql_research_assistant/search/__init__.py | 0 .../search/nba_roster.db | Bin 0 -> 57344 bytes .../sql_research_assistant/search/sql.py | 93 +++++++++++ .../sql_research_assistant/search/web.py | 151 ++++++++++++++++++ .../sql_research_assistant/writer.py | 75 +++++++++ .../sql-research-assistant/tests/__init__.py | 0 13 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 templates/sql-research-assistant/.gitignore create mode 100644 templates/sql-research-assistant/LICENSE create mode 100644 templates/sql-research-assistant/README.md create mode 100644 templates/sql-research-assistant/pyproject.toml create mode 100644 templates/sql-research-assistant/sql_research_assistant/__init__.py create mode 100644 templates/sql-research-assistant/sql_research_assistant/chain.py create mode 100644 templates/sql-research-assistant/sql_research_assistant/search/__init__.py create mode 100644 templates/sql-research-assistant/sql_research_assistant/search/nba_roster.db create mode 100644 templates/sql-research-assistant/sql_research_assistant/search/sql.py create mode 100644 templates/sql-research-assistant/sql_research_assistant/search/web.py create mode 100644 templates/sql-research-assistant/sql_research_assistant/writer.py create mode 100644 templates/sql-research-assistant/tests/__init__.py diff --git a/docs/docs/expression_language/how_to/generators.ipynb b/docs/docs/expression_language/how_to/generators.ipynb index 627be2248c2..c9635c8aacd 100644 --- a/docs/docs/expression_language/how_to/generators.ipynb +++ b/docs/docs/expression_language/how_to/generators.ipynb @@ -176,7 +176,7 @@ "\n", "\n", "async def asplit_into_list(\n", - " input: AsyncIterator[str]\n", + " input: AsyncIterator[str],\n", ") -> AsyncIterator[List[str]]: # async def\n", " buffer = \"\"\n", " async for (\n", diff --git a/templates/sql-research-assistant/.gitignore b/templates/sql-research-assistant/.gitignore new file mode 100644 index 00000000000..bee8a64b79a --- /dev/null +++ b/templates/sql-research-assistant/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/templates/sql-research-assistant/LICENSE b/templates/sql-research-assistant/LICENSE new file mode 100644 index 00000000000..426b6509034 --- /dev/null +++ b/templates/sql-research-assistant/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 LangChain, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/templates/sql-research-assistant/README.md b/templates/sql-research-assistant/README.md new file mode 100644 index 00000000000..362c546e8b2 --- /dev/null +++ b/templates/sql-research-assistant/README.md @@ -0,0 +1,62 @@ +# sql-research-assistant + +This package does research over a SQL database + +## Usage + +To use this package, you should first have the LangChain CLI installed: + +```shell +pip install -U langchain-cli +``` + +To create a new LangChain project and install this as the only package, you can do: + +```shell +langchain app new my-app --package sql-research-assistant +``` + +If you want to add this to an existing project, you can just run: + +```shell +langchain app add sql-research-assistant +``` + +And add the following code to your `server.py` file: +```python +from sql_research_assistant import chain as sql_research_assistant_chain + +add_routes(app, sql_research_assistant_chain, path="/sql-research-assistant") +``` + +(Optional) Let's now configure LangSmith. +LangSmith will help us trace, monitor and debug LangChain applications. +LangSmith is currently in private beta, you can sign up [here](https://smith.langchain.com/). +If you don't have access, you can skip this section + + +```shell +export LANGCHAIN_TRACING_V2=true +export LANGCHAIN_API_KEY= +export LANGCHAIN_PROJECT= # if not specified, defaults to "default" +``` + +If you are inside this directory, then you can spin up a LangServe instance directly by: + +```shell +langchain serve +``` + +This will start the FastAPI app with a server is running locally at +[http://localhost:8000](http://localhost:8000) + +We can see all templates at [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) +We can access the playground at [http://127.0.0.1:8000/sql-research-assistant/playground](http://127.0.0.1:8000/sql-research-assistant/playground) + +We can access the template from code with: + +```python +from langserve.client import RemoteRunnable + +runnable = RemoteRunnable("http://localhost:8000/sql-research-assistant") +``` \ No newline at end of file diff --git a/templates/sql-research-assistant/pyproject.toml b/templates/sql-research-assistant/pyproject.toml new file mode 100644 index 00000000000..fd36f615cce --- /dev/null +++ b/templates/sql-research-assistant/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "sql-research-assistant" +version = "0.0.1" +description = "" +authors = [] +readme = "README.md" + +[tool.poetry.dependencies] +python = ">=3.8.1,<4.0" +langchain = ">=0.0.313, <0.1" +openai = "^0.28.1" + +[tool.poetry.group.dev.dependencies] +langchain-cli = ">=0.0.4" +fastapi = "^0.104.0" +sse-starlette = "^1.6.5" + +[tool.langserve] +export_module = "sql_research_assistant" +export_attr = "chain" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/templates/sql-research-assistant/sql_research_assistant/__init__.py b/templates/sql-research-assistant/sql_research_assistant/__init__.py new file mode 100644 index 00000000000..2f4d63e1fd3 --- /dev/null +++ b/templates/sql-research-assistant/sql_research_assistant/__init__.py @@ -0,0 +1,3 @@ +from sql_research_assistant.chain import chain + +__all__ = ["chain"] diff --git a/templates/sql-research-assistant/sql_research_assistant/chain.py b/templates/sql-research-assistant/sql_research_assistant/chain.py new file mode 100644 index 00000000000..43984719a3d --- /dev/null +++ b/templates/sql-research-assistant/sql_research_assistant/chain.py @@ -0,0 +1,22 @@ +from langchain.pydantic_v1 import BaseModel +from langchain.schema.runnable import RunnablePassthrough + +from sql_research_assistant.search.web import chain as search_chain +from sql_research_assistant.writer import chain as writer_chain + +chain_notypes = ( + RunnablePassthrough().assign(research_summary=search_chain) | writer_chain +) + + +class InputType(BaseModel): + question: str + + +chain = chain_notypes.with_types(input_type=InputType) + + +if __name__ == "__main__": + print( + chain.invoke({"question": "who is typically older: point guards or centers?"}) + ) diff --git a/templates/sql-research-assistant/sql_research_assistant/search/__init__.py b/templates/sql-research-assistant/sql_research_assistant/search/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/templates/sql-research-assistant/sql_research_assistant/search/nba_roster.db b/templates/sql-research-assistant/sql_research_assistant/search/nba_roster.db new file mode 100644 index 0000000000000000000000000000000000000000..9ea4e20c367f04791176cf30a1ad7913e2b9594c GIT binary patch literal 57344 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 LS3qq?)!_dFB=6Y0 literal 0 HcmV?d00001 diff --git a/templates/sql-research-assistant/sql_research_assistant/search/sql.py b/templates/sql-research-assistant/sql_research_assistant/search/sql.py new file mode 100644 index 00000000000..cb976e2d211 --- /dev/null +++ b/templates/sql-research-assistant/sql_research_assistant/search/sql.py @@ -0,0 +1,93 @@ +from pathlib import Path + +from langchain.chat_models import ChatOllama, ChatOpenAI +from langchain.memory import ConversationBufferMemory +from langchain.prompts import ChatPromptTemplate +from langchain.pydantic_v1 import BaseModel +from langchain.schema.output_parser import StrOutputParser +from langchain.schema.runnable import RunnablePassthrough +from langchain.utilities import SQLDatabase + +# Add the LLM downloaded from Ollama +ollama_llm = "llama2" +llm = ChatOllama(model=ollama_llm) + + +db_path = Path(__file__).parent / "nba_roster.db" +rel = db_path.relative_to(Path.cwd()) +db_string = f"sqlite:///{rel}" +db = SQLDatabase.from_uri(db_string, sample_rows_in_table_info=2) + + +def get_schema(_): + return db.get_table_info() + + +def run_query(query): + return db.run(query) + + +# Prompt + +template = """Based on the table schema below, write a SQL query that would answer the user's question: +{schema} + +Question: {question} +SQL Query:""" # noqa: E501 +prompt = ChatPromptTemplate.from_messages( + [ + ("system", "Given an input question, convert it to a SQL query. No pre-amble."), + ("human", template), + ] +) + +memory = ConversationBufferMemory(return_messages=True) + +# Chain to query with memory + +sql_chain = ( + RunnablePassthrough.assign( + schema=get_schema, + ) + | prompt + | llm.bind(stop=["\nSQLResult:"]) + | StrOutputParser() + | (lambda x: x.split("\n\n")[0]) +) + + +# Chain to answer +template = """Based on the table schema below, question, sql query, and sql response, write a natural language response: +{schema} + +Question: {question} +SQL Query: {query} +SQL Response: {response}""" # noqa: E501 +prompt_response = ChatPromptTemplate.from_messages( + [ + ( + "system", + "Given an input question and SQL response, convert it to a natural " + "language answer. No pre-amble.", + ), + ("human", template), + ] +) + + +# Supply the input types to the prompt +class InputType(BaseModel): + question: str + + +sql_answer_chain = ( + RunnablePassthrough.assign(query=sql_chain).with_types(input_type=InputType) + | RunnablePassthrough.assign( + schema=get_schema, + response=lambda x: db.run(x["query"]), + ) + | RunnablePassthrough.assign( + answer=prompt_response | ChatOpenAI() | StrOutputParser() + ) + | (lambda x: f"Question: {x['question']}\n\nAnswer: {x['answer']}") +) diff --git a/templates/sql-research-assistant/sql_research_assistant/search/web.py b/templates/sql-research-assistant/sql_research_assistant/search/web.py new file mode 100644 index 00000000000..eb84202febd --- /dev/null +++ b/templates/sql-research-assistant/sql_research_assistant/search/web.py @@ -0,0 +1,151 @@ +import json +from typing import Any + +import requests +from bs4 import BeautifulSoup +from langchain.chat_models import ChatOpenAI +from langchain.prompts import ChatPromptTemplate +from langchain.schema.messages import SystemMessage +from langchain.schema.output_parser import StrOutputParser +from langchain.schema.runnable import ( + Runnable, + RunnableLambda, + RunnableParallel, + RunnablePassthrough, +) +from langchain.utilities import DuckDuckGoSearchAPIWrapper + +from sql_research_assistant.search.sql import sql_answer_chain + +RESULTS_PER_QUESTION = 3 + +ddg_search = DuckDuckGoSearchAPIWrapper() + + +def scrape_text(url: str): + # Send a GET request to the webpage + try: + response = requests.get(url) + + # Check if the request was successful + if response.status_code == 200: + # Parse the content of the request with BeautifulSoup + soup = BeautifulSoup(response.text, "html.parser") + + # Extract all text from the webpage + page_text = soup.get_text(separator=" ", strip=True) + + # Print the extracted text + return page_text + else: + return f"Failed to retrieve the webpage: Status code {response.status_code}" + except Exception as e: + print(e) + return f"Failed to retrieve the webpage: {e}" + + +def web_search(query: str, num_results: int): + results = ddg_search.results(query, num_results) + return [r["link"] for r in results] + + +SEARCH_PROMPT = ChatPromptTemplate.from_messages( + [ + ("system", "{agent_prompt}"), + ( + "user", + "Write 3 google search queries to search online that form an " + "objective opinion from the following: {question}\n" + "You must respond with a list of strings in the following format: " + '["query 1", "query 2", "query 3"].', + ), + ] +) + +AUTO_AGENT_INSTRUCTIONS = """ +This task involves researching a given topic, regardless of its complexity or the availability of a definitive answer. The research is conducted by a specific agent, defined by its type and role, with each agent requiring distinct instructions. +Agent +The agent is determined by the field of the topic and the specific name of the agent that could be utilized to research the topic provided. Agents are categorized by their area of expertise, and each agent type is associated with a corresponding emoji. + +examples: +task: "should I invest in apple stocks?" +response: +{ + "agent": "💰 Finance Agent", + "agent_role_prompt: "You are a seasoned finance analyst AI assistant. Your primary goal is to compose comprehensive, astute, impartial, and methodically arranged financial reports based on provided data and trends." +} +task: "could reselling sneakers become profitable?" +response: +{ + "agent": "📈 Business Analyst Agent", + "agent_role_prompt": "You are an experienced AI business analyst assistant. Your main objective is to produce comprehensive, insightful, impartial, and systematically structured business reports based on provided business data, market trends, and strategic analysis." +} +task: "what are the most interesting sites in Tel Aviv?" +response: +{ + "agent: "🌍 Travel Agent", + "agent_role_prompt": "You are a world-travelled AI tour guide assistant. Your main purpose is to draft engaging, insightful, unbiased, and well-structured travel reports on given locations, including history, attractions, and cultural insights." +} +""" # noqa: E501 +CHOOSE_AGENT_PROMPT = ChatPromptTemplate.from_messages( + [SystemMessage(content=AUTO_AGENT_INSTRUCTIONS), ("user", "task: {task}")] +) + +SUMMARY_TEMPLATE = """{text} + +----------- + +Using the above text, answer in short the following question: + +> {question} + +----------- +if the question cannot be answered using the text, imply summarize the text. Include all factual information, numbers, stats etc if available.""" # noqa: E501 +SUMMARY_PROMPT = ChatPromptTemplate.from_template(SUMMARY_TEMPLATE) + +scrape_and_summarize: Runnable[Any, Any] = ( + RunnableParallel( + { + "question": lambda x: x["question"], + "text": lambda x: scrape_text(x["url"])[:10000], + "url": lambda x: x["url"], + } + ) + | RunnableParallel( + { + "summary": SUMMARY_PROMPT | ChatOpenAI(temperature=0) | StrOutputParser(), + "url": lambda x: x["url"], + } + ) + | RunnableLambda(lambda x: f"Source Url: {x['url']}\nSummary: {x['summary']}") +) + + +def load_json(s): + try: + return json.loads(s) + except Exception: + return {} + + +search_query = SEARCH_PROMPT | ChatOpenAI(temperature=0) | StrOutputParser() | load_json +choose_agent = ( + CHOOSE_AGENT_PROMPT | ChatOpenAI(temperature=0) | StrOutputParser() | load_json +) + +get_search_queries = ( + RunnablePassthrough().assign( + agent_prompt=RunnableParallel({"task": lambda x: x}) + | choose_agent + | (lambda x: x.get("agent_role_prompt")) + ) + | search_query +) + + +chain = ( + get_search_queries + | (lambda x: [{"question": q} for q in x]) + | sql_answer_chain.map() + | (lambda x: "\n\n".join(x)) +) diff --git a/templates/sql-research-assistant/sql_research_assistant/writer.py b/templates/sql-research-assistant/sql_research_assistant/writer.py new file mode 100644 index 00000000000..9af15f4b0d1 --- /dev/null +++ b/templates/sql-research-assistant/sql_research_assistant/writer.py @@ -0,0 +1,75 @@ +from langchain.chat_models import ChatOpenAI +from langchain.prompts import ChatPromptTemplate +from langchain.schema.output_parser import StrOutputParser +from langchain.schema.runnable import ConfigurableField + +WRITER_SYSTEM_PROMPT = "You are an AI critical thinker research assistant. Your sole purpose is to write well written, critically acclaimed, objective and structured reports on given text." # noqa: E501 + + +# Report prompts from https://github.com/assafelovic/gpt-researcher/blob/master/gpt_researcher/master/prompts.py +RESEARCH_REPORT_TEMPLATE = """Information: +-------- +{research_summary} +-------- + +Using the above information, answer the following question or topic: "{question}" in a detailed report -- \ +The report should focus on the answer to the question, should be well structured, informative, \ +in depth, with facts and numbers if available and a minimum of 1,200 words. + +You should strive to write the report as long as you can using all relevant and necessary information provided. +You must write the report with markdown syntax. +You MUST determine your own concrete and valid opinion based on the given information. Do NOT deter to general and meaningless conclusions. +Write all used source urls at the end of the report, and make sure to not add duplicated sources, but only one reference for each. +You must write the report in apa format. +Please do your best, this is very important to my career.""" # noqa: E501 + + +RESOURCE_REPORT_TEMPLATE = """Information: +-------- +{research_summary} +-------- + +Based on the above information, generate a bibliography recommendation report for the following question or topic: "{question}". \ +The report should provide a detailed analysis of each recommended resource, explaining how each source can contribute to finding answers to the research question. \ +Focus on the relevance, reliability, and significance of each source. \ +Ensure that the report is well-structured, informative, in-depth, and follows Markdown syntax. \ +Include relevant facts, figures, and numbers whenever available. \ +The report should have a minimum length of 1,200 words. + +Please do your best, this is very important to my career.""" # noqa: E501 + +OUTLINE_REPORT_TEMPLATE = """Information: +-------- +{research_summary} +-------- + +Using the above information, generate an outline for a research report in Markdown syntax for the following question or topic: "{question}". \ +The outline should provide a well-structured framework for the research report, including the main sections, subsections, and key points to be covered. \ +The research report should be detailed, informative, in-depth, and a minimum of 1,200 words. \ +Use appropriate Markdown syntax to format the outline and ensure readability. + +Please do your best, this is very important to my career.""" # noqa: E501 + +model = ChatOpenAI(temperature=0) +prompt = ChatPromptTemplate.from_messages( + [ + ("system", WRITER_SYSTEM_PROMPT), + ("user", RESEARCH_REPORT_TEMPLATE), + ] +).configurable_alternatives( + ConfigurableField("report_type"), + default_key="research_report", + resource_report=ChatPromptTemplate.from_messages( + [ + ("system", WRITER_SYSTEM_PROMPT), + ("user", RESOURCE_REPORT_TEMPLATE), + ] + ), + outline_report=ChatPromptTemplate.from_messages( + [ + ("system", WRITER_SYSTEM_PROMPT), + ("user", OUTLINE_REPORT_TEMPLATE), + ] + ), +) +chain = prompt | model | StrOutputParser() diff --git a/templates/sql-research-assistant/tests/__init__.py b/templates/sql-research-assistant/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d