{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# LLM" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [], "source": [ "### LLM\n", "from langchain_community.chat_models import ChatOllama\n", "# local_llm = \"llama3.1:8b-instruct-fp16\"\n", "local_llm = \"llama3-groq-tool-use:latest\"\n", "\n", "llm_json = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", "llm = ChatOllama(model=local_llm, temperature=0)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# RAG" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Retriever" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "FAISS index loaded from faiss_index.bin\n", "Metadata loaded from faiss_metadata.pkl\n", "Using existing FAISS index and metadata.\n", "Creating FAISS retriever...\n" ] } ], "source": [ "from faiss_index import create_faiss_retriever, faiss_query\n", "retriever = create_faiss_retriever()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Generation" ] }, { "cell_type": "code", "execution_count": 59, "metadata": {}, "outputs": [], "source": [ "from langchain.prompts import ChatPromptTemplate\n", "from langchain_core.output_parsers import StrOutputParser\n", "def faiss_query(question: str, docs, llm, multi_query: bool = False) -> str:\n", " context = docs\n", " # try:\n", " # context = \"\\n\".join(doc.page_content for doc in docs)\n", " # except:\n", " # context = \"\\n\".join(doc for doc in docs)\n", " \n", " system_prompt: str = \"你是一個來自台灣的AI助理,樂於以台灣人的立場幫助使用者,會用繁體中文回答問題。\"\n", " template = \"\"\"\n", " <|begin_of_text|>\n", " \n", " <|start_header_id|>system<|end_header_id|>\n", " 你是一個來自台灣的ESG的AI助理,請用繁體中文回答問題 \\n\n", " You should not mention anything about \"根據提供的文件內容\" or other similar terms.\n", " Use five sentences maximum and keep the answer concise.\n", " 如果你不知道答案請回答:\"很抱歉,目前我無法回答您的問題,請將您的詢問發送至 test@systex.com 以便獲得更進一步的幫助,謝謝。\"\n", " 勿回答無關資訊\n", " <|eot_id|>\n", " \n", " <|start_header_id|>user<|end_header_id|>\n", " Answer the following question based on this context:\n", "\n", " {context}\n", "\n", " Question: {question}\n", " 用繁體中文回答問題\n", " <|eot_id|>\n", " \n", " <|start_header_id|>assistant<|end_header_id|>\n", " \"\"\"\n", " prompt = ChatPromptTemplate.from_template(\n", " system_prompt + \"\\n\\n\" +\n", " template\n", " )\n", " \n", " # prompt = ChatPromptTemplate.from_template(\n", " # system_prompt + \"\\n\\n\" +\n", " # \"Answer the following question based on this context:\\n\\n\"\n", " # \"{context}\\n\\n\"\n", " # \"Question: {question}\\n\"\n", " # \"Answer in the same language as the question. If you don't know the answer, \"\n", " # \"say 'I'm sorry, I don't have enough information to answer that question.'\"\n", " # )\n", "\n", " \n", " # chain = prompt | taide_llm | StrOutputParser()\n", " chain = prompt | llm | StrOutputParser()\n", " return chain.invoke({\"context\": context, \"question\": question})" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "# docs = retriever.get_relevant_documents(question, k=10)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "question = \"誰需要繳交碳費?\"\n", "docs = retriever.get_relevant_documents(question, k=50)\n", "docs" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "### Generate\n", "# llm = ChatOllama(model=local_llm, temperature=0)\n", "\n", "# docs_documents = \"\\n\\n\".join(doc.page_content for doc in docs)\n", "# generation = faiss_query(question, docs_documents, llm)\n", "# generation" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Retrieval Grader" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "### Retrieval Grader\n", "\n", "from langchain_community.chat_models import ChatOllama\n", "from langchain_core.output_parsers import JsonOutputParser\n", "from langchain_core.prompts import PromptTemplate\n", "\n", "# LLM\n", "# llm_json = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", "\n", "prompt = PromptTemplate(\n", " template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are a grader assessing relevance \n", " of a retrieved document to a user question. If the document contains keywords related to the user question, \n", " grade it as relevant. It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n", " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n", " Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\n", " <|eot_id|><|start_header_id|>user<|end_header_id|>\n", " Here is the retrieved document: \\n\\n {document} \\n\\n\n", " Here is the user question: {question} \\n <|eot_id|><|start_header_id|>assistant<|end_header_id|>\n", " \"\"\",\n", " input_variables=[\"question\", \"document\"],\n", ")\n", "\n", "retrieval_grader = prompt | llm_json | JsonOutputParser()\n", "# question = \"溫室氣體是什麼\"\n", "# # docs = retriever.invoke(question)\n", "# docs = retriever.get_relevant_documents(question, k=10)\n", "# doc_txt = docs[1].page_content\n", "# print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "# for doc in docs:\n", "# doc_txt = doc.page_content\n", "# print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Hallucination Grader" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'score': 'yes'}" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "### Hallucination Grader\n", "\n", "from langchain_community.chat_models import ChatOllama\n", "from langchain_core.output_parsers import JsonOutputParser\n", "from langchain_core.prompts import PromptTemplate\n", "\n", "# LLM\n", "# llm_json = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", "\n", "# Prompt\n", "prompt = PromptTemplate(\n", " template=\"\"\" <|begin_of_text|><|start_header_id|>system<|end_header_id|> \n", " You are a grader assessing whether an answer is grounded in / supported by a set of facts. \n", " Give 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. \n", " Provide 'yes' or 'no' score as a JSON with a single key 'score' and no preamble or explanation. \n", " Return the a JSON with a single key 'score' and no premable or explanation. \n", " <|eot_id|><|start_header_id|>user<|end_header_id|>\n", " Here are the facts:\n", " \\n ------- \\n\n", " {documents} \n", " \\n ------- \\n\n", " Here is the answer: {generation} \n", " Provide 'yes' or 'no' score as a JSON with a single key 'score' and no premable or explanation.\n", " <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n", " input_variables=[\"generation\", \"documents\"],\n", ")\n", "\n", "hallucination_grader = prompt | llm_json | JsonOutputParser()\n", "\n", "question = \"誰需要繳交碳費?\"\n", "docs = retriever.get_relevant_documents(question, k=10)\n", "\n", "generation = faiss_query(question, docs, llm)\n", "# docs_documents = \"\\n\\n\".join(doc.page_content for doc in docs)\n", "\n", "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "docs_documents = \"\\n\\n\".join(doc.page_content for doc in docs)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Answer Grader" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [], "source": [ "### Answer Grader\n", "\n", "# LLM\n", "llm_json = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", "\n", "# Prompt\n", "prompt = PromptTemplate(\n", " template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are a grader assessing whether an \n", " answer is useful to resolve a question. Give a binary score 'yes' or 'no' to indicate whether the answer is \n", " useful to resolve a question. Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\n", " <|eot_id|><|start_header_id|>user<|end_header_id|> Here is the answer:\n", " \\n ------- \\n\n", " {generation} \n", " \\n ------- \\n\n", " Here is the question: {question} <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n", " input_variables=[\"generation\", \"question\"],\n", ")\n", "\n", "answer_grader = prompt | llm_json | JsonOutputParser()\n", "# answer_grader.invoke({\"question\": question, \"generation\": generation})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# SQL" ] }, { "cell_type": "code", "execution_count": 104, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/usr/local/anaconda3/envs/llama3/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Did not recognize type 'vector' of column 'embedding'\n", " self._metadata.reflect(\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 104, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from importlib import reload # Python 3.4+\n", "import text_to_sql2\n", "reload(text_to_sql2)" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/usr/local/anaconda3/envs/llama3/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Did not recognize type 'vector' of column 'embedding'\n", " self._metadata.reflect(\n", "/usr/local/anaconda3/envs/llama3/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Did not recognize type 'vector' of column 'embedding'\n", " self._metadata.reflect(\n" ] } ], "source": [ "\n", "from text_to_sql import run, get_query, query_to_nl\n", "from langchain_community.utilities import SQLDatabase\n", "import os\n", "URI: str = os.environ.get('SUPABASE_URI')\n", "db = SQLDatabase.from_uri(URI)\n", "\n", "def run_text_to_sql(question: str):\n", " selected_table = ['2022 清冊數據(GHG)', '2022 清冊數據(ISO)', '2023 清冊數據(GHG)', '2023 清冊數據(ISO)', '水電使用量(GHG)', '水電使用量(ISO)']\n", " # question = \"建準去年的固定燃燒總排放量是多少?\"\n", " query, result, answer = run(db, question, selected_table, llm)\n", " \n", " return answer, query\n", "\n", "def _get_query(question: str):\n", " selected_table = ['2022 清冊數據(GHG)', '2022 清冊數據(ISO)', '2023 清冊數據(GHG)', '2023 清冊數據(ISO)', '水電使用量(ISO)']\n", " query = get_query(db, question, selected_table, llm)\n", " return query\n", "\n", "def _query_to_nl(question: str, query: str):\n", " answer = query_to_nl(db, question, query, llm)\n", " return answer" ] }, { "cell_type": "code", "execution_count": 187, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/usr/local/anaconda3/envs/llama3/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Did not recognize type 'vector' of column 'embedding'\n", " self._metadata.reflect(\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 187, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from importlib import reload # Python 3.4+\n", "import text_to_sql2\n", "reload(text_to_sql2)" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/usr/local/anaconda3/envs/llama3/lib/python3.12/site-packages/langchain_community/utilities/sql_database.py:123: SAWarning: Did not recognize type 'vector' of column 'embedding'\n", " self._metadata.reflect(\n" ] } ], "source": [ "from text_to_sql2 import run, get_query, query_to_nl\n", "from langchain_community.utilities import SQLDatabase\n", "import os\n", "URI: str = os.environ.get('SUPABASE_URI')\n", "db = SQLDatabase.from_uri(URI)\n", "\n", "def run_text_to_sql(question: str):\n", " selected_table = ['104_112碳排放公開及建準資料', '水電使用量(GHG)', '水電使用量(ISO)']\n", " # question = \"建準去年的固定燃燒總排放量是多少?\"\n", " query, result, answer = run(db, question, selected_table, llm)\n", " \n", " return answer, query\n", "\n", "def _get_query(question: str):\n", " selected_table = ['104_112碳排放公開及建準資料', '水電使用量(GHG)', '水電使用量(ISO)']\n", " query = get_query(db, question, selected_table, llm)\n", " return query\n", "\n", "def _query_to_nl(question: str, query: str):\n", " answer = query_to_nl(db, question, query, llm)\n", " return answer" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "SELECT SUM(\"昆山廣興廠\") AS \"建準廣興廠綠電使用量\"\n", "FROM \"2023 清冊數據(GHG)\"\n", "WHERE \"類別\" = '類別5-使用來自組織產品間接排放'\n" ] }, { "data": { "text/plain": [ "'SELECT SUM(\"昆山廣興廠\") AS \"建準廣興廠綠電使用量\"\\nFROM \"2023 清冊數據(GHG)\"\\nWHERE \"類別\" = \\'類別5-使用來自組織產品間接排放\\''" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "question = \"建準廣興廠去年的綠電使用量是多少?\"\n", "query = _get_query(question)\n", "query" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[(494312.5775,)]\n" ] }, { "data": { "text/plain": [ "'建準廣興廠去年的綠電使用量是494312.5775。'" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer = _query_to_nl(question, query)\n", "answer" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## SQL Grader" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [], "source": [ "\n", "### SQL Grader\n", "\n", "from langchain_community.chat_models import ChatOllama\n", "from langchain_core.output_parsers import JsonOutputParser\n", "from langchain_core.prompts import PromptTemplate\n", "\n", "# LLM\n", "# llm_json = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", "\n", "prompt = PromptTemplate(\n", " template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> \n", " You are a SQL query grader assessing correctness of PostgreSQL query to a user question. \n", " Based on following database description, you need to grade whether the PostgreSQL query exactly matches the user question.\n", " \n", " Here is database description:\n", " {table_info}\n", " \n", " You need to check that each where statement is correctly filtered out what user question need.\n", " \n", " For example, if user question is \"建準去年的固定燃燒總排放量是多少?\", and the PostgreSQL query is \n", " \"SELECT SUM(\"排放量(公噸CO2e)\") AS \"下游租賃總排放量\"\n", " FROM \"104_112碳排放公開及建準資料\"\n", " WHERE \"事業名稱\" like '%建準%'\n", " AND \"排放源\" = '下游租賃'\n", " AND \"盤查標準\" = 'GHG'\n", " AND \"年度\" = EXTRACT(YEAR FROM CURRENT_DATE)-1;\"\n", " For the above example, we can find that user asked for \"固定燃燒\", but the PostgreSQL query gives \"排放源\" = '下游租賃' in WHERE statement, which means the PostgreSQL query is incorrect for the user question.\n", " \n", " Another example like \"建準去年的固定燃燒總排放量是多少?\", and the PostgreSQL query is \n", " \"SELECT SUM(\"排放量(公噸CO2e)\") AS \"固定燃燒總排放量\"\n", " FROM \"104_112碳排放公開及建準資料\"\n", " WHERE \"事業名稱\" like '%台積電%'\n", " AND \"排放源\" = '固定燃燒'\n", " AND \"盤查標準\" = 'GHG'\n", " AND \"年度\" = EXTRACT(YEAR FROM CURRENT_DATE)-1;\"\n", " For the above example, we can find that user asked for \"建準\", but the PostgreSQL query gives \"事業名稱\" like '%台積電%' in WHERE statement, which means the PostgreSQL query is incorrect for the user question.\n", " \n", " and so on. You need to strictly examine whether the sql PostgreSQL query matches the user question.\n", " \n", " If the PostgreSQL query do not exactly matches the user question, grade it as incorrect. \n", " You need to strictly examine whether the sql PostgreSQL query matches the user question.\n", " Give a binary score 'yes' or 'no' score to indicate whether the PostgreSQL query is correct to the question. \\n\n", " Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\n", " <|eot_id|>\n", " \n", " <|start_header_id|>user<|end_header_id|>\n", " Here is the PostgreSQL query: \\n\\n {sql_query} \\n\\n\n", " Here is the user question: {question} \\n <|eot_id|><|start_header_id|>assistant<|end_header_id|>\n", " \"\"\",\n", " input_variables=[\"table_info\", \"question\", \"sql_query\"],\n", ")\n", "\n", "sql_query_grader = prompt | llm_json | JsonOutputParser()" ] }, { "cell_type": "code", "execution_count": 180, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'score': 'no'}\n" ] } ], "source": [ "from text_to_sql2 import table_description\n", "question = \"建準去年的類別一排放量\"\n", "# sql_query = \"\"\"\n", "# SELECT SUM(\"高雄總部及運通廠\" + \"台北辦事處\" + \"昆山廣興廠\" + \"北海建準廠\" + \"北海立準廠\" + \"菲律賓建準廠\" + \"Inc\" + \"SAS\" + \"India\") AS \"類別一排放量\"\n", "# FROM \"2023 清冊數據(GHG)\"\n", "# WHERE \"類別\" = '類別一-直接排放'\n", "# \"\"\"\n", "question = \"台積電去年的固定燃燒總排放量是多少?\"\n", "sql_query = \"\"\"\n", "SELECT SUM(\"排放量(公噸CO2e)\") AS \"固定燃燒總排放量\"\n", "FROM \"104_112碳排放公開及建準資料\"\n", "WHERE \"事業名稱\" like '%建準%'\n", "AND \"排放源\" = '固定燃燒'\n", "AND \"盤查標準\" = 'GHG'\n", "AND \"年度\" = EXTRACT(YEAR FROM CURRENT_DATE)-1;\n", "\"\"\"\n", "print(sql_query_grader.invoke({\"table_info\": table_description(), \"question\": question, \"sql_query\": sql_query}))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Additional details" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from importlib import reload # Python 3.4+\n", "import post_processing_sqlparse\n", "reload(post_processing_sqlparse)" ] }, { "cell_type": "code", "execution_count": 60, "metadata": {}, "outputs": [], "source": [ "sql_query = \"\"\"\n", "SELECT SUM(\"排放量(公噸CO2e)\") AS \"固定燃燒總排放量\"\n", "FROM \"104_112碳排放公開及建準資料\"\n", "WHERE \"事業名稱\" like '%建準%'\n", "AND \"類別\" = '類別1-直接排放'\n", "AND \"盤查標準\" = 'GHG'\n", "AND \"年度\" = EXTRACT(YEAR FROM CURRENT_DATE)-1;\n", "\"\"\"" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "from post_processing_sqlparse import get_query_columns, parse_sql_for_stock_info, get_table_name" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['SUM']\n", "('固定燃燒', None)\n", "\"104_112碳排放公開及建準資料\"\n" ] } ], "source": [ "print(get_query_columns(sql_query, get_real_name=True))\n", "print(parse_sql_for_stock_info(sql_query))\n", "print(get_table_name(sql_query))" ] }, { "cell_type": "code", "execution_count": 68, "metadata": {}, "outputs": [], "source": [ "def generate_additional_detail(sql_query):\n", " terms = parse_sql_for_stock_info(sql_query)\n", " answer = \"\"\n", " for term in terms:\n", " if term is None: continue\n", " question_format = [f\"什麼是{term}?\", f\"{term}的用途是什麼\", f\"如何計算{term}?\"]\n", " for question in question_format:\n", " # question = f\"什麼是{term}?\"\n", " documents = retriever.get_relevant_documents(question, k=30)\n", " generation = faiss_query(question, documents, llm)\n", " answer += generation\n", " answer += \"\\n\"\n", " # print(question)\n", " # print(generation)\n", " return answer" ] }, { "cell_type": "code", "execution_count": 66, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'直接排放(Category 1)指的是固定燃燒排放源中使用天然氣的設備所產生的溫室氣體排放量。\\n直接排放的用途主要包括固定燃燒和製程中使用含氟氣體及 N2O所產生之排放源。\\n直接排放的計算可以根據溫室氣體排放量盤查作業指引113年版進行。主要步驟如下:\\n\\n1. 依照活動數據,計算低位熱值和燃料用量。\\n2. 使用質量平衡法或直接監測法計算二氧化碳排放量。\\n3. 將排放係數乘以燃料用量和低位熱值,以取得單位產品用量。\\n\\n這些步驟可以幫助您計算類別1-直接排放的數據。\\n'" ] }, "execution_count": 66, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer = generate_additional_detail(sql_query)\n", "answer" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Router" ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'datasource': '自有數據'}\n" ] } ], "source": [ "### Router\n", "\n", "from langchain_community.chat_models import ChatOllama\n", "from langchain_core.output_parsers import JsonOutputParser\n", "from langchain_core.prompts import PromptTemplate\n", "\n", "# LLM\n", "# llm_json = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", "\n", "prompt = PromptTemplate(\n", " template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> \n", " You are an expert at routing a user question to a 專業知識 or 自有數據. \n", " Use company private data for questions about the informations about a company's greenhouse gas emissions data.\n", " Otherwise, use the 專業知識 for questions on ESG field knowledge or news about ESG. \n", " You do not need to be stringent with the keywords in the question related to these topics. \n", " Give a binary choice '自有數據' or '專業知識' based on the question. \n", " Return the a JSON with a single key 'datasource' and no premable or explanation. \n", " \n", " Question to route: {question} \n", " <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n", " input_variables=[\"question\"],\n", ")\n", "\n", "question_router = prompt | llm_json | JsonOutputParser()\n", "question = \"建準去年的類別1排放量是多少?\"\n", "question = \"建準去年的綠電使用量是多少?\"\n", "# docs = retriever.get_relevant_documents(question)\n", "# doc_txt = docs[1].page_content\n", "print(question_router.invoke({\"question\": question}))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Node" ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [], "source": [ "# RAG + text-to-sql\n", "\n", "from pprint import pprint\n", "from typing import List\n", "\n", "from langchain_core.documents import Document\n", "from typing_extensions import TypedDict\n", "\n", "from langgraph.graph import END, StateGraph, START\n", "\n", "### State\n", "\n", "\n", "class GraphState(TypedDict):\n", " \"\"\"\n", " Represents the state of our graph.\n", "\n", " Attributes:\n", " question: question\n", " generation: LLM generation\n", " company_private_data: whether to search company private data\n", " documents: list of documents\n", " \"\"\"\n", "\n", " question: str\n", " generation: str\n", " documents: List[str]\n", " retry: int\n", " sql_query: str" ] }, { "cell_type": "code", "execution_count": 70, "metadata": {}, "outputs": [], "source": [ "def retrieve_and_generation(state):\n", " \"\"\"\n", " Retrieve documents from vectorstore\n", "\n", " Args:\n", " state (dict): The current graph state\n", "\n", " Returns:\n", " state (dict): New key added to state, documents, that contains retrieved documents, and generation, genrating by LLM\n", " \"\"\"\n", " print(\"---RETRIEVE---\")\n", " question = state[\"question\"]\n", "\n", " # Retrieval\n", " # documents = retriever.invoke(question)\n", " # TODO: correct Retrieval function\n", " documents = retriever.get_relevant_documents(question, k=30)\n", " # docs_documents = \"\\n\\n\".join(doc.page_content for doc in documents)\n", " print(documents)\n", " generation = faiss_query(question, documents, llm)\n", " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n", "\n", "def company_private_data_get_sql_query(state):\n", " \"\"\"\n", " Get PostgreSQL query according to question\n", "\n", " Args:\n", " state (dict): The current graph state\n", "\n", " Returns:\n", " state (dict): return generated PostgreSQL query and record retry times\n", " \"\"\"\n", " print(\"---SQL QUERY---\")\n", " question = state[\"question\"]\n", " \n", " if state[\"retry\"]:\n", " retry = state[\"retry\"]\n", " retry += 1\n", " else: \n", " retry = 0\n", " print(\"RETRY: \", retry)\n", " \n", " sql_query = _get_query(question)\n", " \n", " return {\"sql_query\": sql_query, \"question\": question, \"retry\": retry}\n", " \n", "def company_private_data_search(state):\n", " \"\"\"\n", " Execute PostgreSQL query and convert to nature language.\n", "\n", " Args:\n", " state (dict): The current graph state\n", "\n", " Returns:\n", " state (dict): Appended sql results to state\n", " \"\"\"\n", "\n", " print(\"---SQL TO NL---\")\n", " print(state)\n", " question = state[\"question\"]\n", " sql_query = state[\"sql_query\"]\n", " generation = _query_to_nl(question, sql_query)\n", " \n", " # generation = [company_private_data_result]\n", " \n", " return {\"sql_query\": sql_query, \"question\": question, \"generation\": generation}\n", "\n", "def company_private_data_search(state):\n", " \"\"\"\n", " Execute PostgreSQL query and convert to nature language.\n", "\n", " Args:\n", " state (dict): The current graph state\n", "\n", " Returns:\n", " state (dict): Appended sql results to state\n", " \"\"\"\n", "\n", " print(\"---SQL TO NL---\")\n", " print(state)\n", " question = state[\"question\"]\n", " sql_query = state[\"sql_query\"]\n", " generation = _query_to_nl(question, sql_query)\n", " \n", " # generation = [company_private_data_result]\n", " \n", " return {\"sql_query\": sql_query, \"question\": question, \"generation\": generation}\n", "\n", "def additional_explanation(state):\n", " \"\"\"_summary_\n", "\n", " Args:\n", " state (_type_): _description_\n", " \n", " Returns:\n", " state (dict): Appended additional explanation to state\n", " \"\"\"\n", " \n", " print(\"---ADDITIONAL EXPLANATION---\")\n", " print(state)\n", " question = state[\"question\"]\n", " sql_query = state[\"sql_query\"]\n", " generation = generate_additional_detail(sql_query)\n", " \n", " # generation = [company_private_data_result]\n", " \n", " return {\"sql_query\": sql_query, \"question\": question, \"generation\": generation}\n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Conditional edge" ] }, { "cell_type": "code", "execution_count": 71, "metadata": {}, "outputs": [], "source": [ "### Conditional edge\n", "\n", "\n", "def route_question(state):\n", " \"\"\"\n", " Route question to web search or RAG.\n", "\n", " Args:\n", " state (dict): The current graph state\n", "\n", " Returns:\n", " str: Next node to call\n", " \"\"\"\n", "\n", " print(\"---ROUTE QUESTION---\")\n", " question = state[\"question\"]\n", " # print(question)\n", " question_router = Router()\n", " source = question_router.invoke({\"question\": question})\n", " # print(source)\n", " print(source[\"datasource\"])\n", " if source[\"datasource\"] == \"自有數據\":\n", " print(\"---ROUTE QUESTION TO TEXT-TO-SQL---\")\n", " return \"自有數據\"\n", " elif source[\"datasource\"] == \"專業知識\":\n", " print(\"---ROUTE QUESTION TO RAG---\")\n", " return \"專業知識\"\n", " \n", "def grade_generation_v_documents_and_question(state):\n", " \"\"\"\n", " Determines whether the generation is grounded in the document and answers question.\n", "\n", " Args:\n", " state (dict): The current graph state\n", "\n", " Returns:\n", " str: Decision for next node to call\n", " \"\"\"\n", "\n", " print(\"---CHECK HALLUCINATIONS---\")\n", " question = state[\"question\"]\n", " documents = state[\"documents\"]\n", " generation = state[\"generation\"]\n", "\n", " \n", " # print(docs_documents)\n", " # print(generation)\n", " score = hallucination_grader.invoke(\n", " {\"documents\": documents, \"generation\": generation}\n", " )\n", " print(score)\n", " grade = score[\"score\"]\n", "\n", " # Check hallucination\n", " if grade in [\"yes\", \"true\", 1, \"1\"]:\n", " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n", " # Check question-answering\n", " print(\"---GRADE GENERATION vs QUESTION---\")\n", " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n", " grade = score[\"score\"]\n", " if grade in [\"yes\", \"true\", 1, \"1\"]:\n", " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n", " return \"useful\"\n", " else:\n", " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n", " return \"not useful\"\n", " else:\n", " pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n", " return \"not supported\"\n", " \n", "def grade_sql_query(state):\n", " \"\"\"\n", " Determines whether the Postgresql query are correct to the question\n", "\n", " Args:\n", " state (dict): The current graph state\n", "\n", " Returns:\n", " state (dict): Decision for retry or continue\n", " \"\"\"\n", "\n", " print(\"---CHECK SQL CORRECTNESS TO QUESTION---\")\n", " question = state[\"question\"]\n", " sql_query = state[\"sql_query\"]\n", " retry = state[\"retry\"]\n", "\n", " # Score each doc\n", " \n", " score = sql_query_grader.invoke({\"table_info\": table_description(), \"question\": question, \"sql_query\": sql_query})\n", " grade = score[\"score\"]\n", " # Document relevant\n", " if grade in [\"yes\", \"true\", 1, \"1\"]:\n", " print(\"---GRADE: CORRECT SQL QUERY---\")\n", " return \"correct\"\n", " elif retry >= 5:\n", " print(\"---GRADE: INCORRECT SQL QUERY AND REACH RETRY LIMIT---\")\n", " return \"failed\"\n", " else:\n", " print(\"---GRADE: INCORRECT SQL QUERY---\")\n", " return \"incorrect\"\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Graph" ] }, { "cell_type": "code", "execution_count": 72, "metadata": {}, "outputs": [], "source": [ "\n", "from langgraph.pregel import RetryPolicy\n", "\n", "workflow = StateGraph(GraphState)\n", "\n", "# Define the nodes\n", "workflow.add_node(\"Text-to-SQL\", company_private_data_get_sql_query, retry=RetryPolicy(max_attempts=5)) # web search\n", "workflow.add_node(\"SQL Answer\", company_private_data_search, retry=RetryPolicy(max_attempts=5)) # web search\n", "workflow.add_node(\"Additoinal Explanation\", additional_explanation, retry=RetryPolicy(max_attempts=5)) # retrieve\n", "workflow.add_node(\"RAG\", retrieve_and_generation, retry=RetryPolicy(max_attempts=5)) # retrieve\n", "# workflow.add_node(\"grade_generation\", grade_documents) # grade documents\n", "# workflow.add_node(\"generate\", generate) # generatae\n", "\n", "workflow.add_conditional_edges(\n", " START,\n", " route_question,\n", " {\n", " \"自有數據\": \"Text-to-SQL\",\n", " \"專業知識\": \"RAG\",\n", " },\n", ")\n", "\n", "workflow.add_conditional_edges(\n", " \"RAG\",\n", " grade_generation_v_documents_and_question,\n", " {\n", " \"not supported\": \"RAG\",\n", " \"useful\": END,\n", " \"not useful\": \"RAG\",\n", " },\n", ")\n", "workflow.add_conditional_edges(\n", " \"Text-to-SQL\",\n", " grade_sql_query,\n", " {\n", " \"correct\": \"SQL Answer\",\n", " \"incorrect\": \"Text-to-SQL\",\n", " \"failed\": \"RAG\"\n", " \n", " },\n", ")\n", "workflow.add_edge(\"SQL Answer\", \"Additoinal Explanation\")\n", "workflow.add_edge(\"Additoinal Explanation\", END)\n", "\n", "\n", "\n", "# workflow.add_edge(\"company_private_data_search\", END)\n", "\n", "app = workflow.compile()" ] }, { "cell_type": "code", "execution_count": 224, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "%%{init: {'flowchart': {'curve': 'linear'}}}%%\n", "graph TD;\n", "\t__start__([__start__]):::first\n", "\tcompany_private_data_query(company_private_data_query)\n", "\tcompany_private_data_search(company_private_data_search)\n", "\tretrieve_and_generation(retrieve_and_generation)\n", "\t__end__([__end__]):::last\n", "\tcompany_private_data_query --> company_private_data_search;\n", "\t__start__ -.  company_private_data  .-> company_private_data_query;\n", "\t__start__ -.  vectorstore  .-> retrieve_and_generation;\n", "\tretrieve_and_generation -.  not supported  .-> retrieve_and_generation;\n", "\tretrieve_and_generation -.  useful  .-> __end__;\n", "\tretrieve_and_generation -.  not useful  .-> retrieve_and_generation;\n", "\tcompany_private_data_search -.  not supported  .-> company_private_data_query;\n", "\tcompany_private_data_search -.  useful  .-> __end__;\n", "\tcompany_private_data_search -.  not useful  .-> retrieve_and_generation;\n", "\tclassDef default fill:#f2f0ff,line-height:1.2\n", "\tclassDef first fill-opacity:0\n", "\tclassDef last fill:#bfb6fc\n", "\n" ] } ], "source": [ "print(app.get_graph().draw_mermaid())" ] }, { "cell_type": "code", "execution_count": 73, "metadata": {}, "outputs": [ { "data": { "image/jpeg": "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7FsLZG0IntY0YTu8Tg1yPPlve3ju+9tQc75tul9Mn4ZmzZz5rkzFxlChHIC508znBoaA1pPALhy4+QB5JC8mFo7fkOomxUchgqmK02pAxmMyvpwms3pyGuc4xBoDI29zmkOIPczy5DuRJNE6fU9G1XD4V16/sL8X3uiyedmFq657y4ue6UgHuPe4cjjyPHuUpQYZmr1wxodLK53HmQQOT+LhfXqxV++Tfyh+pZdEGI9WKv3yb+UP1J6sVfvk38ofqWXRBiPVir98m/lD9SerFX75N/KH6ll0QYj1Yq/fJv5Q/UsjUrNp12QsJLW88F3v9/K7kQEREBERAREQEREBERAREQFHeov+T7Z/4Ltf9pykSjvUX/J9s/8ABdr/ALTkFdfI0/zXunP8Ft/puVzqmPkaf5r3Tn+C2/03K50BERAREQEREBERAREQEREBERAREQEREBERAREQEREBVl1exWEyOz9OJcttc2uWaudZLQqROIGUm7HcV3fSCOT+RWaqy6vZXCY7Z+nEWW1SbY7NrOsioW4mkjFzdjuLDvoAHI/Kgs1ERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQFHeov+T7Z/wCC7X/acpEo71F/yfbP/Bdr/tOQV18jT/Ne6c/wW3+m5XOqY+Rp/mvdOf4Lb/TcrnQEREBERAREQEREBERAREQEREBERAREQEREBERAREQFC9/9dvnrUPVP0L5t+dG/P3pXHd6F2nnw+f33dx7lNFWXV7FYTI7P04ly21za5Zq51ktCpE4gZSbsdxXd9II5P5EFmoiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIuCeByfIKGy7tlcg7xcHhK9ygf3O1evOreMP9JjWxSEtPwJ4594HBBO23aru+7+I+6xGUzRQr1o236uYf8ATUv7KnrRtv1cw/6al/ZVu7Ld8PNTzXCaooV60bb9XMP+mpf2VPWjbfq5h/01L+yp2W74eanmYTVFCvWjbfq5h/01L+yp60bb9XMP+mpf2VOy3fDzU8zCaooV60bb9XMP+mpf2VPWjbfq5h/01L+yp2W74eanmYTVfnd/dV+kdyexrHUqr3TVIYBg7zAOfB9uSWF/u9xL5WknyBDB++W7nrRt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", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from IPython.display import Image, display\n", "from langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeStyles\n", "\n", "display(\n", " Image(\n", " app.get_graph().draw_mermaid_png(\n", " draw_method=MermaidDrawMethod.API,\n", " output_file_path=\"agent_workflow.png\",\n", " )\n", " )\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Test" ] }, { "cell_type": "code", "execution_count": 288, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "---ROUTE QUESTION---\n", "建準去年的類別八排放量\n", "{'datasource': 'company_private_data'}\n", "company_private_data\n", "---ROUTE QUESTION TO TEXT-TO-SQL---\n", "---SQL QUERY---\n", "RETRY: 0\n", "SELECT SUM(\"排放量(公噸CO2e)\") AS \"類別8總排放量\"\n", "FROM \"104_112碳排放公開及建準資料\"\n", "WHERE \"事業名稱\" like '%建準%'\n", "AND \"年度\" = EXTRACT(YEAR FROM CURRENT_DATE)-1\n", "AND \"類別\" = '類別8-';\n", "---CHECK SQL CORRECTNESS TO QUESTION---\n", "---GRADE: CORRECT SQL QUERY---\n", "'Finished running: company_private_data_query:'\n", "---SQL TO NL---\n", "{'question': '建準去年的類別八排放量', 'generation': None, 'documents': None, 'retry': 0, 'sql_query': 'SELECT SUM(\"排放量(公噸CO2e)\") AS \"類別8總排放量\"\\nFROM \"104_112碳排放公開及建準資料\"\\nWHERE \"事業名稱\" like \\'%建準%\\'\\nAND \"年度\" = EXTRACT(YEAR FROM CURRENT_DATE)-1\\nAND \"類別\" = \\'類別8-\\';'}\n", "[(None,)]\n", "'Finished running: company_private_data_search:'\n", "('SELECT SUM(\"排放量(公噸CO2e)\") AS \"類別8總排放量\"\\n'\n", " 'FROM \"104_112碳排放公開及建準資料\"\\n'\n", " 'WHERE \"事業名稱\" like \\'%建準%\\'\\n'\n", " 'AND \"年度\" = EXTRACT(YEAR FROM CURRENT_DATE)-1\\n'\n", " 'AND \"類別\" = \\'類別8-\\';',\n", " '[(None,)]',\n", " '根據 SQL '\n", " '查詢和結果,去年建準的類別八排放量為空,可能是資料庫中沒有符合條件的資料。建議檢查資料庫中是否有符合條件的資料,或調整查詢條件以確保能夠正確查詢到相應的數據。')\n" ] } ], "source": [ "# Test\n", "\n", "inputs = {\"question\": \"建準去年的類別八排放量\"}\n", "for output in app.stream(inputs, {\"recursion_limit\": 10}):\n", " for key, value in output.items():\n", " pprint(f\"Finished running: {key}:\")\n", "pprint(value[\"generation\"])" ] }, { "cell_type": "code", "execution_count": 181, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'建準去年的類別一排放量是13.5953。'" ] }, "execution_count": 181, "metadata": {}, "output_type": "execute_result" } ], "source": [ "value[\"generation\"]" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "ename": "NameError", "evalue": "name 'stop' is not defined", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[27], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mstop\u001b[49m\n", "\u001b[0;31mNameError\u001b[0m: name 'stop' is not defined" ] } ], "source": [ "stop" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# from RAG_strategy import multi_query_chain\n", "llm = ChatOllama(model=local_llm, temperature=0)\n", "question = \"溫室氣體是什麼\"\n", "generate_queries = multi_query_chain(llm)\n", "\n", "questions = generate_queries.invoke(question)\n", "questions" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def ask_more_detail_chain(llm):\n", " # Multi Query: Different Perspectives\n", " template = \"\"\"\n", " <|begin_of_text|>\n", " \n", " <|start_header_id|>system<|end_header_id|>\n", " 你是一個來自台灣的AI助理,你的專長是根據使用者提供的文本來進一步詢問當中的細節,例如名詞解釋,請用繁體中文。 \\n\n", " You are an AI language model assistant. \n", " Your task is to generate three questions about the given user context as additional explanation.\n", " By generating three in-depth questions about the user context, your goal is to help the user realize more details. \n", " Provide these questions separated by newlines.\n", " For example:\n", " context: 建準廣興廠去年2023年一共自產發電了684,508度綠電\n", " in-depth question:什麼是綠電?\\n為何要使用綠電? \n", "\n", " output must in user's language and no preamble or explanation.\n", " <|eot_id|>\n", " \n", " <|start_header_id|>user<|end_header_id|>\n", " \n", " \n", " \n", " Original context: {question}\n", " three questions:\n", " <|eot_id|>\n", " \n", " <|start_header_id|>assistant<|end_header_id|>\"\"\"\n", " prompt_perspectives = ChatPromptTemplate.from_template(template)\n", "\n", " \n", " # llm = ChatOpenAI(temperature=0, model=\"gpt-4-1106-preview\")\n", " # llm = ChatOllama(model=\"llama3\", num_gpu=1, temperature=0)\n", "\n", " generate_queries = (\n", " prompt_perspectives \n", " | llm\n", " | StrOutputParser() \n", " | (lambda x: x.split(\"\\n\"))\n", " )\n", "\n", " return generate_queries" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "llm = ChatOllama(model=local_llm, temperature=0)\n", "question = \"建準廣興廠去年的固定燃燒排放量是多少?\"\n", "generate_queries = ask_more_detail_chain(llm)\n", "\n", "questions = generate_queries.invoke(question)\n", "questions" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "llm = ChatOllama(model=local_llm, temperature=0)\n", "question = \"固定燃燒是什麼?\"\n", "docs = retriever.get_relevant_documents(question, k=10)\n", "generation = faiss_query(question, docs, llm)\n", "generation" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import datetime\n", "from typing import Literal, Optional, Tuple\n", "\n", "from langchain_core.pydantic_v1 import BaseModel, Field\n", "\n", "\n", "class SubQuery(BaseModel):\n", " \"\"\"Search over a database of tutorial videos about a software library.\"\"\"\n", "\n", " sub_query: str = Field(\n", " ...,\n", " description=\"A very specific query against the database.\",\n", " )" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[SubQuery(sub_query='建準去年的類別一排放量是多少?'), SubQuery(sub_query='溫室氣體是什麼?')]" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from langchain.output_parsers import PydanticToolsParser\n", "from langchain_core.prompts import ChatPromptTemplate\n", "from langchain_openai import ChatOpenAI\n", "from dotenv import load_dotenv\n", "\n", "load_dotenv()\n", "\n", "system = \"\"\"You are an expert at converting user questions into database queries. \\\n", "\n", "Perform query decomposition. Given a user question, break it down into distinct sub questions that \\\n", "you need to answer in order to answer the original question.\n", "\n", "If there are acronyms or words you are not familiar with, do not try to rephrase them.\n", "用繁體中文.\n", "\"\"\"\n", "prompt = ChatPromptTemplate.from_messages(\n", " [\n", " (\"system\", system),\n", " (\"human\", \"{question}用繁體中文\"),\n", " ]\n", ")\n", "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0.5)\n", "llm_with_tools = llm.bind_tools([SubQuery])\n", "parser = PydanticToolsParser(tools=[SubQuery])\n", "query_analyzer = prompt | llm_with_tools | parser\n", "\n", "query_analyzer.invoke({\"question\": \"建準去年的類別一排放量?溫室氣體是什麼?\"})" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[SubQuery(sub_query='什麼是溫室氣體'), SubQuery(sub_query='去年的類別一排放量是多少')]" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "query_analyzer.invoke({\"question\": \"溫室氣體是什麼?建準去年的類別一排放量?\"})" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[]" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "query_analyzer.invoke({\"question\": \"建準去年的類別一排放量?\"})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "You have access to a database of tutorial videos about a software library for building LLM-powered applications. \\" ] } ], "metadata": { "kernelspec": { "display_name": "llama3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.4" } }, "nbformat": 4, "nbformat_minor": 2 }