Update notebooks to use bind_tools (#394)

This commit is contained in:
William FH
2024-05-04 01:30:52 -07:00
committed by GitHub
parent 21b8cbfd33
commit e0770d68b3
29 changed files with 1466 additions and 949 deletions
+78 -120
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -83,6 +83,7 @@
"\n",
"class PromptInstructions(BaseModel):\n",
" \"\"\"Instructions on how to prompt the LLM.\"\"\"\n",
"\n",
" objective: str\n",
" variables: List[str]\n",
" constraints: List[str]\n",
@@ -121,7 +122,7 @@
"source": [
"# Helper function for determining if tool was called\n",
"def _is_tool_call(msg):\n",
" return hasattr(msg, \"additional_kwargs\") and 'tool_calls' in msg.additional_kwargs"
" return hasattr(msg, \"additional_kwargs\") and \"tool_calls\" in msg.additional_kwargs"
]
},
{
@@ -136,6 +137,7 @@
"\n",
"{reqs}\"\"\"\n",
"\n",
"\n",
"# Function to get the messages for the prompt\n",
"# Will only get messages AFTER the tool call\n",
"def get_prompt_messages(messages):\n",
@@ -143,11 +145,10 @@
" other_msgs = []\n",
" for m in messages:\n",
" if _is_tool_call(m):\n",
" tool_call = m.additional_kwargs['tool_calls'][0]['function']['arguments']\n",
" tool_call = m.additional_kwargs[\"tool_calls\"][0][\"function\"][\"arguments\"]\n",
" elif tool_call is not None:\n",
" other_msgs.append(m)\n",
" return [SystemMessage(content=prompt_system.format(reqs=tool_call))] + other_msgs\n",
" "
" return [SystemMessage(content=prompt_system.format(reqs=tool_call))] + other_msgs"
]
},
{
@@ -215,7 +216,7 @@
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
"\n",
"nodes = {k:k for k in ['info', 'prompt', END]}\n",
"nodes = {k: k for k in [\"info\", \"prompt\", END]}\n",
"workflow = MessageGraph()\n",
"workflow.add_node(\"info\", chain)\n",
"workflow.add_node(\"prompt\", prompt_gen_chain)\n",
@@ -401,9 +402,9 @@
"\n",
"config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n",
"while True:\n",
" user = input('User (q/Q to quit): ')\n",
" if user in {'q', 'Q'}:\n",
" print('AI: Byebye')\n",
" user = input(\"User (q/Q to quit): \")\n",
" if user in {\"q\", \"Q\"}:\n",
" print(\"AI: Byebye\")\n",
" break\n",
" for output in graph.stream([HumanMessage(content=user)], config=config):\n",
" if \"__end__\" in output:\n",
@@ -34,7 +34,7 @@
"metadata": {},
"outputs": [],
"source": [
" ! pip install -U langchain_community langchain-openai langchain-anthropic langchain langgraph bs4"
"! pip install -U langchain_community langchain-openai langchain-anthropic langchain langgraph bs4"
]
},
{
@@ -97,16 +97,22 @@
"\n",
"### OpenAI\n",
"\n",
"# Grader prompt \n",
"# Grader prompt\n",
"code_gen_prompt = ChatPromptTemplate.from_messages(\n",
" [(\"system\",\"\"\"You are a coding assistant with expertise in LCEL, LangChain expression language. \\n \n",
" [\n",
" (\n",
" \"system\",\n",
" \"\"\"You are a coding assistant with expertise in LCEL, LangChain expression language. \\n \n",
" Here is a full set of LCEL documentation: \\n ------- \\n {context} \\n ------- \\n Answer the user \n",
" question based on the above provided documentation. Ensure any code you provide can be executed \\n \n",
" with all required imports and variables defined. Structure your answer with a description of the code solution. \\n\n",
" Then list the imports. And finally list the functioning code block. Here is the user question:\"\"\"),\n",
" (\"placeholder\", \"{messages}\")]\n",
" Then list the imports. And finally list the functioning code block. Here is the user question:\"\"\",\n",
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
")\n",
"\n",
"\n",
"# Data model\n",
"class code(BaseModel):\n",
" \"\"\"Code output\"\"\"\n",
@@ -116,6 +122,7 @@
" code: str = Field(description=\"Code block not including import statements\")\n",
" description = \"Schema for code solutions to questions about LCEL.\"\n",
"\n",
"\n",
"expt_llm = \"gpt-4-0125-preview\"\n",
"llm = ChatOpenAI(temperature=0, model=expt_llm)\n",
"code_gen_chain = code_gen_prompt | llm.with_structured_output(code)\n",
@@ -138,12 +145,19 @@
"\n",
"# Prompt to enforce tool use\n",
"code_gen_prompt_claude = ChatPromptTemplate.from_messages(\n",
" [(\"system\",\"\"\"<instructions> You are a coding assistant with expertise in LCEL, LangChain expression language. \\n \n",
" [\n",
" (\n",
" \"system\",\n",
" \"\"\"<instructions> You are a coding assistant with expertise in LCEL, LangChain expression language. \\n \n",
" Here is the LCEL documentation: \\n ------- \\n {context} \\n ------- \\n Answer the user question based on the \\n \n",
" above provided documentation. Ensure any code you provide can be executed with all required imports and variables \\n\n",
" defined. Structure your answer: 1) a prefix describing the code solution, 2) the imports, 3) the functioning code block. \\n\n",
" Invoke the code tool to structure the output correctly. </instructions> \\n Here is the user question:\"\"\",),\n",
" (\"placeholder\", \"{messages}\"),])\n",
" Invoke the code tool to structure the output correctly. </instructions> \\n Here is the user question:\"\"\",\n",
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
")\n",
"\n",
"\n",
"# Data model\n",
"class code(BaseModel):\n",
@@ -156,8 +170,8 @@
"\n",
"\n",
"# LLM\n",
"# expt_llm = \"claude-3-haiku-20240307\" \n",
"expt_llm = \"claude-3-opus-20240229\" \n",
"# expt_llm = \"claude-3-haiku-20240307\"\n",
"expt_llm = \"claude-3-opus-20240229\"\n",
"llm = ChatAnthropic(\n",
" model=expt_llm,\n",
" default_headers={\"anthropic-beta\": \"tools-2024-04-04\"},\n",
@@ -165,6 +179,7 @@
"\n",
"structured_llm_claude = llm.with_structured_output(code, include_raw=True)\n",
"\n",
"\n",
"# Optional: Check for errors in case tool use is flaky\n",
"def check_claude_output(tool_output):\n",
" \"\"\"Check for parse error or failure to call the tool\"\"\"\n",
@@ -179,7 +194,7 @@
" f\"Error parsing your output! Be sure to invoke the tool. Output: {raw_output}. \\n Parse error: {error}\"\n",
" )\n",
"\n",
" # Tool was not invoked \n",
" # Tool was not invoked\n",
" elif not tool_output[\"parsed\"]:\n",
" print(\"Failed to invoke tool!\")\n",
" raise ValueError(\n",
@@ -187,12 +202,16 @@
" )\n",
" return tool_output\n",
"\n",
"\n",
"# Chain with output check\n",
"code_chain_claude_raw = code_gen_prompt_claude | structured_llm_claude | check_claude_output\n",
"code_chain_claude_raw = (\n",
" code_gen_prompt_claude | structured_llm_claude | check_claude_output\n",
")\n",
"\n",
"\n",
"def insert_errors(inputs):\n",
" \"\"\"Insert errors for tool parsing in the messages\"\"\"\n",
" \n",
"\n",
" # Get errors\n",
" error = inputs[\"error\"]\n",
" messages = inputs[\"messages\"]\n",
@@ -207,19 +226,24 @@
" \"context\": inputs[\"context\"],\n",
" }\n",
"\n",
"\n",
"# This will be run as a fallback chain\n",
"fallback_chain = insert_errors | code_chain_claude_raw\n",
"N = 3 # Max re-tries\n",
"code_gen_chain_re_try = code_chain_claude_raw.with_fallbacks(fallbacks=[fallback_chain] * N, exception_key=\"error\")\n",
"code_gen_chain_re_try = code_chain_claude_raw.with_fallbacks(\n",
" fallbacks=[fallback_chain] * N, exception_key=\"error\"\n",
")\n",
"\n",
"\n",
"def parse_output(solution):\n",
" \"\"\"When we add 'include_raw=True' to structured output, \n",
" it will return a dict w 'raw', 'parsed', 'parsing_error'. \"\"\"\n",
" \n",
" return solution['parsed']\n",
" \"\"\"When we add 'include_raw=True' to structured output,\n",
" it will return a dict w 'raw', 'parsed', 'parsing_error'.\"\"\"\n",
"\n",
" return solution[\"parsed\"]\n",
"\n",
"\n",
"# Wtih re-try to correct for failure to invoke tool\n",
"# TODO: Annoying errors w/ \"user\" vs \"assistant\" \n",
"# TODO: Annoying errors w/ \"user\" vs \"assistant\"\n",
"# Roles must alternate between \"user\" and \"assistant\", but found multiple \"user\" roles in a row\n",
"code_gen_chain = code_gen_chain_re_try | parse_output\n",
"\n",
@@ -238,7 +262,9 @@
"source": [
"# Test\n",
"question = \"How do I build a RAG chain in LCEL?\"\n",
"solution = code_gen_chain.invoke({\"context\":concatenated_content,\"messages\":[(\"user\",question)]})\n",
"solution = code_gen_chain.invoke(\n",
" {\"context\": concatenated_content, \"messages\": [(\"user\", question)]}\n",
")\n",
"solution"
]
},
@@ -261,6 +287,7 @@
"source": [
"from typing import Dict, TypedDict, List\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
@@ -269,13 +296,13 @@
" error : Binary flag for control flow to indicate whether test error was tripped\n",
" messages : With user question, error messages, reasoning\n",
" generation : Code solution\n",
" iterations : Number of tries \n",
" iterations : Number of tries\n",
" \"\"\"\n",
"\n",
" error : str\n",
" messages : List\n",
" generation : str\n",
" iterations : int"
" error: str\n",
" messages: List\n",
" generation: str\n",
" iterations: int"
]
},
{
@@ -306,10 +333,11 @@
"max_iterations = 3\n",
"# Reflect\n",
"# flag = 'reflect'\n",
"flag = 'do not reflect'\n",
" \n",
"flag = \"do not reflect\"\n",
"\n",
"### Nodes\n",
"\n",
"\n",
"def generate(state: GraphState):\n",
" \"\"\"\n",
" Generate a code solution\n",
@@ -322,7 +350,7 @@
" \"\"\"\n",
"\n",
" print(\"---GENERATING CODE SOLUTION---\")\n",
" \n",
"\n",
" # State\n",
" messages = state[\"messages\"]\n",
" iterations = state[\"iterations\"]\n",
@@ -330,16 +358,29 @@
"\n",
" # We have been routed back to generation with an error\n",
" if error == \"yes\":\n",
" messages += [(\"user\",\"Now, try again. Invoke the code tool to structure the output with a prefix, imports, and code block:\")]\n",
" \n",
" messages += [\n",
" (\n",
" \"user\",\n",
" \"Now, try again. Invoke the code tool to structure the output with a prefix, imports, and code block:\",\n",
" )\n",
" ]\n",
"\n",
" # Solution\n",
" code_solution = code_gen_chain.invoke({\"context\": concatenated_content, \"messages\" : messages})\n",
" messages += [(\"assistant\",f\"{code_solution.prefix} \\n Imports: {code_solution.imports} \\n Code: {code_solution.code}\")]\n",
" \n",
" code_solution = code_gen_chain.invoke(\n",
" {\"context\": concatenated_content, \"messages\": messages}\n",
" )\n",
" messages += [\n",
" (\n",
" \"assistant\",\n",
" f\"{code_solution.prefix} \\n Imports: {code_solution.imports} \\n Code: {code_solution.code}\",\n",
" )\n",
" ]\n",
"\n",
" # Increment\n",
" iterations = iterations + 1\n",
" return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n",
"\n",
"\n",
"def code_check(state: GraphState):\n",
" \"\"\"\n",
" Check code\n",
@@ -352,7 +393,7 @@
" \"\"\"\n",
"\n",
" print(\"---CHECKING CODE---\")\n",
" \n",
"\n",
" # State\n",
" messages = state[\"messages\"]\n",
" code_solution = state[\"generation\"]\n",
@@ -370,8 +411,13 @@
" print(\"---CODE IMPORT CHECK: FAILED---\")\n",
" error_message = [(\"user\", f\"Your solution failed the import test: {e}\")]\n",
" messages += error_message\n",
" return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations, \"error\": \"yes\"}\n",
" \n",
" return {\n",
" \"generation\": code_solution,\n",
" \"messages\": messages,\n",
" \"iterations\": iterations,\n",
" \"error\": \"yes\",\n",
" }\n",
"\n",
" # Check execution\n",
" try:\n",
" exec(imports + \"\\n\" + code)\n",
@@ -379,11 +425,22 @@
" print(\"---CODE BLOCK CHECK: FAILED---\")\n",
" error_message = [(\"user\", f\"Your solution failed the code execution test: {e}\")]\n",
" messages += error_message\n",
" return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations, \"error\": \"yes\"}\n",
" \n",
" return {\n",
" \"generation\": code_solution,\n",
" \"messages\": messages,\n",
" \"iterations\": iterations,\n",
" \"error\": \"yes\",\n",
" }\n",
"\n",
" # No errors\n",
" print(\"---NO CODE TEST FAILURES---\")\n",
" return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations, \"error\": \"no\"}\n",
" return {\n",
" \"generation\": code_solution,\n",
" \"messages\": messages,\n",
" \"iterations\": iterations,\n",
" \"error\": \"no\",\n",
" }\n",
"\n",
"\n",
"def reflect(state: GraphState):\n",
" \"\"\"\n",
@@ -397,24 +454,33 @@
" \"\"\"\n",
"\n",
" print(\"---GENERATING CODE SOLUTION---\")\n",
" \n",
"\n",
" # State\n",
" messages = state[\"messages\"]\n",
" iterations = state[\"iterations\"]\n",
" code_solution = state[\"generation\"]\n",
"\n",
" # Prompt reflection\n",
" reflection_message = [(\"user\", \"\"\"You tried to solve this problem and failed a unit test. Reflect on this failure\n",
" reflection_message = [\n",
" (\n",
" \"user\",\n",
" \"\"\"You tried to solve this problem and failed a unit test. Reflect on this failure\n",
" given the provided documentation. Write a few key suggestions based on the \n",
" documentation to avoid making this mistake again.\"\"\")]\n",
" \n",
" documentation to avoid making this mistake again.\"\"\",\n",
" )\n",
" ]\n",
"\n",
" # Add reflection\n",
" reflections = code_gen_chain.invoke({\"context\" : concatenated_content, \"messages\" : messages})\n",
" messages += [(\"assistant\" , f\"Here are reflections on the error: {reflections}\")]\n",
" reflections = code_gen_chain.invoke(\n",
" {\"context\": concatenated_content, \"messages\": messages}\n",
" )\n",
" messages += [(\"assistant\", f\"Here are reflections on the error: {reflections}\")]\n",
" return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n",
"\n",
"\n",
"### Edges\n",
"\n",
"\n",
"def decide_to_finish(state: GraphState):\n",
" \"\"\"\n",
" Determines whether to finish.\n",
@@ -433,7 +499,7 @@
" return \"end\"\n",
" else:\n",
" print(\"---DECISION: RE-TRY SOLUTION---\")\n",
" if flag == 'reflect':\n",
" if flag == \"reflect\":\n",
" return \"reflect\"\n",
" else:\n",
" return \"generate\""
@@ -479,7 +545,7 @@
"outputs": [],
"source": [
"question = \"How can I directly pass a string to a runnable and use it to construct the input needed for my prompt?\"\n",
"app.invoke({\"messages\":[(\"user\",question)],\"iterations\":0})"
"app.invoke({\"messages\": [(\"user\", question)], \"iterations\": 0})"
]
},
{
@@ -510,6 +576,7 @@
"outputs": [],
"source": [
"import langsmith\n",
"\n",
"client = langsmith.Client()"
]
},
@@ -521,7 +588,9 @@
"outputs": [],
"source": [
"# Clone the dataset to your tenant to use it\n",
"public_dataset = (\"https://smith.langchain.com/public/326674a6-62bd-462d-88ae-eea49d503f9d/d\")\n",
"public_dataset = (\n",
" \"https://smith.langchain.com/public/326674a6-62bd-462d-88ae-eea49d503f9d/d\"\n",
")\n",
"client.clone_public_dataset(public_dataset)"
]
},
@@ -542,22 +611,24 @@
"source": [
"from langsmith.schemas import Example, Run\n",
"\n",
"def check_import(run: Run, example: Example) -> dict: \n",
"\n",
"def check_import(run: Run, example: Example) -> dict:\n",
" imports = run.outputs.get(\"imports\")\n",
" try:\n",
" exec(imports)\n",
" return {\"key\": \"import_check\" , \"score\": 1} \n",
" return {\"key\": \"import_check\", \"score\": 1}\n",
" except:\n",
" return {\"key\": \"import_check\" , \"score\": 0} \n",
" return {\"key\": \"import_check\", \"score\": 0}\n",
"\n",
"def check_execution(run: Run, example: Example) -> dict: \n",
"\n",
"def check_execution(run: Run, example: Example) -> dict:\n",
" imports = run.outputs.get(\"imports\")\n",
" code = run.outputs.get(\"code\")\n",
" try:\n",
" exec(imports + \"\\n\" + code)\n",
" return {\"key\": \"code_execution_check\" , \"score\": 1} \n",
" return {\"key\": \"code_execution_check\", \"score\": 1}\n",
" except:\n",
" return {\"key\": \"code_execution_check\" , \"score\": 0} "
" return {\"key\": \"code_execution_check\", \"score\": 0}"
]
},
{
@@ -576,14 +647,17 @@
"outputs": [],
"source": [
"def predict_base_case(example: dict):\n",
" \"\"\" Context stuffing \"\"\"\n",
" solution = code_gen_chain.invoke({\"context\" : concatenated_content, \"messages\" : [(\"user\",example[\"question\"])]})\n",
" solution_structured = structured_code_formatter.invoke([(\"code\",solution)])\n",
" \"\"\"Context stuffing\"\"\"\n",
" solution = code_gen_chain.invoke(\n",
" {\"context\": concatenated_content, \"messages\": [(\"user\", example[\"question\"])]}\n",
" )\n",
" solution_structured = structured_code_formatter.invoke([(\"code\", solution)])\n",
" return {\"imports\": solution_structured.imports, \"code\": solution_structured.code}\n",
"\n",
"\n",
"def predict_langgraph(example: dict):\n",
" \"\"\" LangGraph \"\"\"\n",
" graph = app.invoke({\"messages\":[(\"user\",example[\"question\"])],\"iterations\":0})\n",
" \"\"\"LangGraph\"\"\"\n",
" graph = app.invoke({\"messages\": [(\"user\", example[\"question\"])], \"iterations\": 0})\n",
" solution = graph[\"generation\"]\n",
" return {\"imports\": solution.imports, \"code\": solution.code}"
]
@@ -598,7 +672,7 @@
"from langsmith.evaluation import evaluate\n",
"\n",
"# Evaluator\n",
"code_evalulator = [check_import,check_execution]\n",
"code_evalulator = [check_import, check_execution]\n",
"\n",
"# Dataset\n",
"dataset_name = \"test-LCEL-code-gen\""
@@ -616,10 +690,10 @@
" predict_base_case,\n",
" data=dataset_name,\n",
" evaluators=code_evalulator,\n",
" experiment_prefix=f\"test-without-langgraph-{expt_llm}\", \n",
" experiment_prefix=f\"test-without-langgraph-{expt_llm}\",\n",
" max_concurrency=2,\n",
" metadata={\n",
" \"llm\": expt_llm,\n",
" \"llm\": expt_llm,\n",
" },\n",
")"
]
@@ -639,8 +713,8 @@
" experiment_prefix=f\"test-with-langgraph-{expt_llm}-{flag}\",\n",
" max_concurrency=2,\n",
" metadata={\n",
" \"llm\": expt_llm,\n",
" \"feedback\": flag,\n",
" \"llm\": expt_llm,\n",
" \"feedback\": flag,\n",
" },\n",
")"
]
+14 -7
View File
@@ -44,9 +44,10 @@
"\n",
"\n",
"def _call_model(state):\n",
" response = model.invoke(state['messages'])\n",
" response = model.invoke(state[\"messages\"])\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"workflow.add_node(\"model\", _call_model)\n",
@@ -106,11 +107,13 @@
" \"openai\": openai_model,\n",
"}\n",
"\n",
"\n",
"def _call_model(state, config):\n",
" m = models[config['configurable'].get('model', 'anthropic')]\n",
" response = m.invoke(state['messages'])\n",
" m = models[config[\"configurable\"].get(\"model\", \"anthropic\")]\n",
" response = m.invoke(state[\"messages\"])\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"workflow.add_node(\"model\", _call_model)\n",
@@ -198,14 +201,18 @@
"source": [
"from langchain_core.messages import SystemMessage\n",
"\n",
"\n",
"def _call_model(state, config):\n",
" m = models[config['configurable'].get('model', 'anthropic')]\n",
" messages = state['messages']\n",
" if 'system_message' in config['configurable']:\n",
" messages = [SystemMessage(content=config['configurable']['system_message'])] + messages\n",
" m = models[config[\"configurable\"].get(\"model\", \"anthropic\")]\n",
" messages = state[\"messages\"]\n",
" if \"system_message\" in config[\"configurable\"]:\n",
" messages = [\n",
" SystemMessage(content=config[\"configurable\"][\"system_message\"])\n",
" ] + messages\n",
" response = m.invoke(messages)\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"workflow.add_node(\"model\", _call_model)\n",
+11 -5
View File
@@ -222,11 +222,15 @@
" llm: BaseChatModel, tools: Sequence[BaseTool], base_prompt: ChatPromptTemplate\n",
"):\n",
" tool_descriptions = \"\\n\".join(\n",
" f\"{i+1}. {tool.description}\\n\" for i, tool in enumerate(tools) # +1 to offset the 0 starting index, we want it count normally from 1.\n",
" f\"{i+1}. {tool.description}\\n\"\n",
" for i, tool in enumerate(\n",
" tools\n",
" ) # +1 to offset the 0 starting index, we want it count normally from 1.\n",
" )\n",
" planner_prompt = base_prompt.partial(\n",
" replan=\"\",\n",
" num_tools=len(tools)+1, # Add one because we're adding the join() tool at the end.\n",
" num_tools=len(tools)\n",
" + 1, # Add one because we're adding the join() tool at the end.\n",
" tool_descriptions=tool_descriptions,\n",
" )\n",
" replanner_prompt = base_prompt.partial(\n",
@@ -236,7 +240,7 @@
" ' - When starting the Current Plan, you should start with \"Thought\" that outlines the strategy for the next plan.\\n'\n",
" \" - In the Current Plan, you should NEVER repeat the actions that are already executed in the Previous Plan.\\n\"\n",
" \" - You must continue the task index from the end of the previous one. Do not repeat task indices.\",\n",
" num_tools=len(tools)+1,\n",
" num_tools=len(tools) + 1,\n",
" tool_descriptions=tool_descriptions,\n",
" )\n",
"\n",
@@ -465,7 +469,7 @@
" task_names[task[\"idx\"]] = (\n",
" task[\"tool\"] if isinstance(task[\"tool\"], str) else task[\"tool\"].name\n",
" )\n",
" args_for_tasks[task[\"idx\"]] = (task[\"args\"])\n",
" args_for_tasks[task[\"idx\"]] = task[\"args\"]\n",
" if (\n",
" # Depends on other tasks\n",
" deps\n",
@@ -491,7 +495,9 @@
" for k in sorted(observations.keys() - originals)\n",
" }\n",
" tool_messages = [\n",
" FunctionMessage(name=name, content=str(obs), additional_kwargs={\"idx\": k, 'args':task_args})\n",
" FunctionMessage(\n",
" name=name, content=str(obs), additional_kwargs={\"idx\": k, \"args\": task_args}\n",
" )\n",
" for k, (name, task_args, obs) in new_observations.items()\n",
" ]\n",
" return tool_messages"
File diff suppressed because one or more lines are too long
+1 -3
View File
@@ -352,9 +352,7 @@
"outputs": [],
"source": [
"from psycopg_pool import ConnectionPool\n",
"from langchain_postgres import (\n",
" PostgresSaver, PickleCheckpointSerializer\n",
")\n",
"from langchain_postgres import PostgresSaver, PickleCheckpointSerializer\n",
"\n",
"pool = ConnectionPool(\n",
" # Example configuration\n",
+84 -40
View File
@@ -109,6 +109,7 @@
"from langchain_community.document_loaders import WebBaseLoader\n",
"from langchain_community.vectorstores import Chroma\n",
"from langchain_openai import OpenAIEmbeddings\n",
"\n",
"### from langchain_cohere import CohereEmbeddings\n",
"\n",
"# Set embeddings\n",
@@ -172,6 +173,7 @@
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"\n",
"# Data model\n",
"class RouteQuery(BaseModel):\n",
" \"\"\"Route a user query to the most relevant datasource.\"\"\"\n",
@@ -181,11 +183,12 @@
" description=\"Given a user question choose to route it to web search or a vectorstore.\",\n",
" )\n",
"\n",
"# LLM with function call \n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_router = llm.with_structured_output(RouteQuery)\n",
"\n",
"# Prompt \n",
"# Prompt\n",
"system = \"\"\"You are an expert at routing a user question to a vectorstore or web search.\n",
"The vectorstore contains documents related to agents, prompt engineering, and adversarial attacks.\n",
"Use the vectorstore for questions on these topics. Otherwise, use web-search.\"\"\"\n",
@@ -197,7 +200,11 @@
")\n",
"\n",
"question_router = route_prompt | structured_llm_router\n",
"print(question_router.invoke({\"question\": \"Who will the Bears draft first in the NFL draft?\"}))\n",
"print(\n",
" question_router.invoke(\n",
" {\"question\": \"Who will the Bears draft first in the NFL draft?\"}\n",
" )\n",
")\n",
"print(question_router.invoke({\"question\": \"What are the types of agent memory?\"}))"
]
},
@@ -216,19 +223,23 @@
}
],
"source": [
"### Retrieval Grader \n",
"### Retrieval Grader\n",
"\n",
"\n",
"# Data model\n",
"class GradeDocuments(BaseModel):\n",
" \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n",
"\n",
" binary_score: str = Field(description=\"Documents are relevant to the question, 'yes' or 'no'\")\n",
" binary_score: str = Field(\n",
" description=\"Documents are relevant to the question, 'yes' or 'no'\"\n",
" )\n",
"\n",
"# LLM with function call \n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
"\n",
"# Prompt \n",
"# Prompt\n",
"system = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
" If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n",
" It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n",
@@ -273,10 +284,12 @@
"# LLM\n",
"llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n",
"\n",
"\n",
"# Post-processing\n",
"def format_docs(docs):\n",
" return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
"\n",
"\n",
"# Chain\n",
"rag_chain = prompt | llm | StrOutputParser()\n",
"\n",
@@ -303,19 +316,23 @@
}
],
"source": [
"### Hallucination Grader \n",
"### Hallucination Grader\n",
"\n",
"\n",
"# Data model\n",
"class GradeHallucinations(BaseModel):\n",
" \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n",
"\n",
" binary_score: str = Field(description=\"Answer is grounded in the facts, 'yes' or 'no'\")\n",
" binary_score: str = Field(\n",
" description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n",
" )\n",
"\n",
"# LLM with function call \n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n",
"\n",
"# Prompt \n",
"# Prompt\n",
"system = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n \n",
" Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\n",
"hallucination_prompt = ChatPromptTemplate.from_messages(\n",
@@ -347,19 +364,23 @@
}
],
"source": [
"### Answer Grader \n",
"### Answer Grader\n",
"\n",
"\n",
"# Data model\n",
"class GradeAnswer(BaseModel):\n",
" \"\"\"Binary score to assess answer addresses question.\"\"\"\n",
"\n",
" binary_score: str = Field(description=\"Answer addresses the question, 'yes' or 'no'\")\n",
" binary_score: str = Field(\n",
" description=\"Answer addresses the question, 'yes' or 'no'\"\n",
" )\n",
"\n",
"# LLM with function call \n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeAnswer)\n",
"\n",
"# Prompt \n",
"# Prompt\n",
"system = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n \n",
" Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\n",
"answer_prompt = ChatPromptTemplate.from_messages(\n",
@@ -370,7 +391,7 @@
")\n",
"\n",
"answer_grader = answer_prompt | structured_llm_grader\n",
"answer_grader.invoke({\"question\": question,\"generation\": generation})"
"answer_grader.invoke({\"question\": question, \"generation\": generation})"
]
},
{
@@ -393,16 +414,19 @@
"source": [
"### Question Re-writer\n",
"\n",
"# LLM \n",
"# LLM\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"\n",
"# Prompt \n",
"# Prompt\n",
"system = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n",
" for vectorstore retrieval. Look at the input and try to reason about the underlying sematic intent / meaning.\"\"\"\n",
"re_write_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", system),\n",
" (\"human\", \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\"),\n",
" (\n",
" \"human\",\n",
" \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n",
" ),\n",
" ]\n",
")\n",
"\n",
@@ -428,6 +452,7 @@
"### Search\n",
"\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"\n",
"web_search_tool = TavilySearchResults(k=3)"
]
},
@@ -453,6 +478,7 @@
"from typing_extensions import TypedDict\n",
"from typing import List\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
@@ -460,11 +486,12 @@
" Attributes:\n",
" question: question\n",
" generation: LLM generation\n",
" documents: list of documents \n",
" documents: list of documents\n",
" \"\"\"\n",
" question : str\n",
" generation : str\n",
" documents : List[str]"
"\n",
" question: str\n",
" generation: str\n",
" documents: List[str]"
]
},
{
@@ -484,6 +511,7 @@
"source": [
"from langchain.schema import Document\n",
"\n",
"\n",
"def retrieve(state):\n",
" \"\"\"\n",
" Retrieve documents\n",
@@ -501,6 +529,7 @@
" documents = retriever.invoke(question)\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer\n",
@@ -514,11 +543,12 @@
" print(\"---GENERATE---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" \n",
"\n",
" # RAG generation\n",
" generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
" return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
"\n",
"\n",
"def grade_documents(state):\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question.\n",
@@ -533,11 +563,13 @@
" print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" \n",
"\n",
" # Score each doc\n",
" filtered_docs = []\n",
" for d in documents:\n",
" score = retrieval_grader.invoke({\"question\": question, \"document\": d.page_content})\n",
" score = retrieval_grader.invoke(\n",
" {\"question\": question, \"document\": d.page_content}\n",
" )\n",
" grade = score.binary_score\n",
" if grade == \"yes\":\n",
" print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
@@ -547,6 +579,7 @@
" continue\n",
" return {\"documents\": filtered_docs, \"question\": question}\n",
"\n",
"\n",
"def transform_query(state):\n",
" \"\"\"\n",
" Transform the query to produce a better question.\n",
@@ -566,6 +599,7 @@
" better_question = question_rewriter.invoke({\"question\": question})\n",
" return {\"documents\": documents, \"question\": better_question}\n",
"\n",
"\n",
"def web_search(state):\n",
" \"\"\"\n",
" Web search based on the re-phrased question.\n",
@@ -587,8 +621,10 @@
"\n",
" return {\"documents\": web_results, \"question\": question}\n",
"\n",
"\n",
"### Edges ###\n",
"\n",
"\n",
"def route_question(state):\n",
" \"\"\"\n",
" Route question to web search or RAG.\n",
@@ -602,14 +638,15 @@
"\n",
" print(\"---ROUTE QUESTION---\")\n",
" question = state[\"question\"]\n",
" source = question_router.invoke({\"question\": question}) \n",
" if source.datasource == 'web_search':\n",
" source = question_router.invoke({\"question\": question})\n",
" if source.datasource == \"web_search\":\n",
" print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n",
" return \"web_search\"\n",
" elif source.datasource == 'vectorstore':\n",
" elif source.datasource == \"vectorstore\":\n",
" print(\"---ROUTE QUESTION TO RAG---\")\n",
" return \"vectorstore\"\n",
"\n",
"\n",
"def decide_to_generate(state):\n",
" \"\"\"\n",
" Determines whether to generate an answer, or re-generate a question.\n",
@@ -628,13 +665,16 @@
" if not filtered_documents:\n",
" # All documents have been filtered check_relevance\n",
" # We will re-generate a new query\n",
" print(\"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\")\n",
" print(\n",
" \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
" )\n",
" return \"transform_query\"\n",
" else:\n",
" # We have relevant documents, so generate answer\n",
" print(\"---DECISION: GENERATE---\")\n",
" return \"generate\"\n",
"\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",
@@ -651,7 +691,9 @@
" documents = state[\"documents\"]\n",
" generation = state[\"generation\"]\n",
"\n",
" score = hallucination_grader.invoke({\"documents\": documents, \"generation\": generation})\n",
" score = hallucination_grader.invoke(\n",
" {\"documents\": documents, \"generation\": generation}\n",
" )\n",
" grade = score.binary_score\n",
"\n",
" # Check hallucination\n",
@@ -659,7 +701,7 @@
" 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",
" score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
" grade = score.binary_score\n",
" if grade == \"yes\":\n",
" print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
@@ -692,11 +734,11 @@
"workflow = StateGraph(GraphState)\n",
"\n",
"# Define the nodes\n",
"workflow.add_node(\"web_search\", web_search) # web search\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generatae\n",
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
"workflow.add_node(\"web_search\", web_search) # web search\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generatae\n",
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
"\n",
"# Build graph\n",
"workflow.set_conditional_entry_point(\n",
@@ -764,8 +806,10 @@
"source": [
"from pprint import pprint\n",
"\n",
"# Run \n",
"inputs = {\"question\": \"What player at the Bears expected to draft first in the 2024 NFL draft?\"}\n",
"# Run\n",
"inputs = {\n",
" \"question\": \"What player at the Bears expected to draft first in the 2024 NFL draft?\"\n",
"}\n",
"for output in app.stream(inputs):\n",
" for key, value in output.items():\n",
" # Node\n",
@@ -839,7 +883,7 @@
" pprint(\"\\n---\\n\")\n",
"\n",
"# Final generation\n",
"pprint(value [\"generation\"])"
"pprint(value[\"generation\"])"
]
},
{
@@ -193,19 +193,24 @@
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"from langchain_cohere import ChatCohere\n",
"\n",
"\n",
"# Data model\n",
"class web_search(BaseModel):\n",
" \"\"\"\n",
" The internet. Use web_search for questions that are related to anything else than agents, prompt engineering, and adversarial attacks.\n",
" \"\"\"\n",
"\n",
" query: str = Field(description=\"The query to use when searching the internet.\")\n",
"\n",
"\n",
"class vectorstore(BaseModel):\n",
" \"\"\"\n",
" A vectorstore containing documents related to agents, prompt engineering, and adversarial attacks. Use the vectorstore for questions on these topics.\n",
" \"\"\"\n",
"\n",
" query: str = Field(description=\"The query to use when searching the vectorstore.\")\n",
"\n",
"\n",
"# Preamble\n",
"preamble = \"\"\"You are an expert at routing a user question to a vectorstore or web search.\n",
"The vectorstore contains documents related to agents, prompt engineering, and adversarial attacks.\n",
@@ -213,7 +218,9 @@
"\n",
"# LLM with tool use and preamble\n",
"llm = ChatCohere(model=\"command-r\", temperature=0)\n",
"structured_llm_router = llm.bind_tools(tools=[web_search, vectorstore], preamble=preamble)\n",
"structured_llm_router = llm.bind_tools(\n",
" tools=[web_search, vectorstore], preamble=preamble\n",
")\n",
"\n",
"# Prompt\n",
"route_prompt = ChatPromptTemplate.from_messages(\n",
@@ -223,12 +230,14 @@
")\n",
"\n",
"question_router = route_prompt | structured_llm_router\n",
"response = question_router.invoke({\"question\": \"Who will the Bears draft first in the NFL draft?\"})\n",
"print(response.response_metadata['tool_calls'])\n",
"response = question_router.invoke(\n",
" {\"question\": \"Who will the Bears draft first in the NFL draft?\"}\n",
")\n",
"print(response.response_metadata[\"tool_calls\"])\n",
"response = question_router.invoke({\"question\": \"What are the types of agent memory?\"})\n",
"print(response.response_metadata['tool_calls'])\n",
"print(response.response_metadata[\"tool_calls\"])\n",
"response = question_router.invoke({\"question\": \"Hi how are you?\"})\n",
"print('tool_calls' in response.response_metadata)"
"print(\"tool_calls\" in response.response_metadata)"
]
},
{
@@ -254,11 +263,15 @@
"source": [
"### Retrieval Grader\n",
"\n",
"\n",
"# Data model\n",
"class GradeDocuments(BaseModel):\n",
" \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n",
"\n",
" binary_score: str = Field(description=\"Documents are relevant to the question, 'yes' or 'no'\")\n",
" binary_score: str = Field(\n",
" description=\"Documents are relevant to the question, 'yes' or 'no'\"\n",
" )\n",
"\n",
"\n",
"# Prompt\n",
"preamble = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n\n",
@@ -279,7 +292,7 @@
"question = \"types of agent memory\"\n",
"docs = retriever.invoke(question)\n",
"doc_txt = docs[1].page_content\n",
"response = retrieval_grader.invoke({\"question\": question, \"document\": doc_txt})\n",
"response = retrieval_grader.invoke({\"question\": question, \"document\": doc_txt})\n",
"print(response)"
]
},
@@ -377,11 +390,7 @@
"\n",
"# Prompt\n",
"prompt = lambda x: ChatPromptTemplate.from_messages(\n",
" [\n",
" HumanMessage(\n",
" f\"Question: {x['question']} \\nAnswer: \"\n",
" )\n",
" ]\n",
" [HumanMessage(f\"Question: {x['question']} \\nAnswer: \")]\n",
")\n",
"\n",
"# Chain\n",
@@ -419,11 +428,15 @@
"source": [
"### Hallucination Grader\n",
"\n",
"\n",
"# Data model\n",
"class GradeHallucinations(BaseModel):\n",
" \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n",
"\n",
" binary_score: str = Field(description=\"Answer is grounded in the facts, 'yes' or 'no'\")\n",
" binary_score: str = Field(\n",
" description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n",
" )\n",
"\n",
"\n",
"# Preamble\n",
"preamble = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n\n",
@@ -431,7 +444,9 @@
"\n",
"# LLM with function call\n",
"llm = ChatCohere(model=\"command-r\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeHallucinations, preamble=preamble)\n",
"structured_llm_grader = llm.with_structured_output(\n",
" GradeHallucinations, preamble=preamble\n",
")\n",
"\n",
"# Prompt\n",
"hallucination_prompt = ChatPromptTemplate.from_messages(\n",
@@ -471,11 +486,15 @@
"source": [
"### Answer Grader\n",
"\n",
"\n",
"# Data model\n",
"class GradeAnswer(BaseModel):\n",
" \"\"\"Binary score to assess answer addresses question.\"\"\"\n",
"\n",
" binary_score: str = Field(description=\"Answer addresses the question, 'yes' or 'no'\")\n",
" binary_score: str = Field(\n",
" description=\"Answer addresses the question, 'yes' or 'no'\"\n",
" )\n",
"\n",
"\n",
"# Preamble\n",
"preamble = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n\n",
@@ -493,7 +512,7 @@
")\n",
"\n",
"answer_grader = answer_prompt | structured_llm_grader\n",
"answer_grader.invoke({\"question\": question,\"generation\": generation})"
"answer_grader.invoke({\"question\": question, \"generation\": generation})"
]
},
{
@@ -519,6 +538,7 @@
"# os.environ['TAVILY_API_KEY'] = <your-api-key>\n",
"\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"\n",
"web_search_tool = TavilySearchResults()"
]
},
@@ -548,6 +568,7 @@
"from typing_extensions import TypedDict\n",
"from typing import List\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"|\n",
" Represents the state of our graph.\n",
@@ -557,9 +578,10 @@
" generation: LLM generation\n",
" documents: list of documents\n",
" \"\"\"\n",
" question : str\n",
" generation : str\n",
" documents : List[str]"
"\n",
" question: str\n",
" generation: str\n",
" documents: List[str]"
]
},
{
@@ -583,6 +605,7 @@
"source": [
"from langchain.schema import Document\n",
"\n",
"\n",
"def retrieve(state):\n",
" \"\"\"\n",
" Retrieve documents\n",
@@ -600,6 +623,7 @@
" documents = retriever.invoke(question)\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"def llm_fallback(state):\n",
" \"\"\"\n",
" Generate answer using the LLM w/o vectorstore\n",
@@ -615,6 +639,7 @@
" generation = llm_chain.invoke({\"question\": question})\n",
" return {\"question\": question, \"generation\": generation}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer using the vectorstore\n",
@@ -629,12 +654,13 @@
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" if not isinstance(documents, list):\n",
" documents = [documents]\n",
" documents = [documents]\n",
"\n",
" # RAG generation\n",
" generation = rag_chain.invoke({\"documents\": documents, \"question\": question})\n",
" return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
"\n",
"\n",
"def grade_documents(state):\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question.\n",
@@ -653,7 +679,9 @@
" # Score each doc\n",
" filtered_docs = []\n",
" for d in documents:\n",
" score = retrieval_grader.invoke({\"question\": question, \"document\": d.page_content})\n",
" score = retrieval_grader.invoke(\n",
" {\"question\": question, \"document\": d.page_content}\n",
" )\n",
" grade = score.binary_score\n",
" if grade == \"yes\":\n",
" print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
@@ -663,6 +691,7 @@
" continue\n",
" return {\"documents\": filtered_docs, \"question\": question}\n",
"\n",
"\n",
"def web_search(state):\n",
" \"\"\"\n",
" Web search based on the re-phrased question.\n",
@@ -684,8 +713,10 @@
"\n",
" return {\"documents\": web_results, \"question\": question}\n",
"\n",
"\n",
"### Edges ###\n",
"\n",
"\n",
"def route_question(state):\n",
" \"\"\"\n",
" Route question to web search or RAG.\n",
@@ -700,26 +731,27 @@
" print(\"---ROUTE QUESTION---\")\n",
" question = state[\"question\"]\n",
" source = question_router.invoke({\"question\": question})\n",
" \n",
"\n",
" # Fallback to LLM or raise error if no decision\n",
" if \"tool_calls\" not in source.additional_kwargs:\n",
" print(\"---ROUTE QUESTION TO LLM---\")\n",
" return \"llm_fallback\" \n",
" return \"llm_fallback\"\n",
" if len(source.additional_kwargs[\"tool_calls\"]) == 0:\n",
" raise \"Router could not decide source\"\n",
" raise \"Router could not decide source\"\n",
"\n",
" # Choose datasource\n",
" datasource = source.additional_kwargs[\"tool_calls\"][0][\"function\"][\"name\"]\n",
" if datasource == 'web_search':\n",
" if datasource == \"web_search\":\n",
" print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n",
" return \"web_search\"\n",
" elif datasource == 'vectorstore':\n",
" elif datasource == \"vectorstore\":\n",
" print(\"---ROUTE QUESTION TO RAG---\")\n",
" return \"vectorstore\"\n",
" else: \n",
" else:\n",
" print(\"---ROUTE QUESTION TO LLM---\")\n",
" return \"vectorstore\"\n",
"\n",
"\n",
"def decide_to_generate(state):\n",
" \"\"\"\n",
" Determines whether to generate an answer, or re-generate a question.\n",
@@ -745,6 +777,7 @@
" print(\"---DECISION: GENERATE---\")\n",
" return \"generate\"\n",
"\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",
@@ -761,7 +794,9 @@
" documents = state[\"documents\"]\n",
" generation = state[\"generation\"]\n",
"\n",
" score = hallucination_grader.invoke({\"documents\": documents, \"generation\": generation})\n",
" score = hallucination_grader.invoke(\n",
" {\"documents\": documents, \"generation\": generation}\n",
" )\n",
" grade = score.binary_score\n",
"\n",
" # Check hallucination\n",
@@ -769,7 +804,7 @@
" 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",
" score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
" grade = score.binary_score\n",
" if grade == \"yes\":\n",
" print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
@@ -808,11 +843,11 @@
"workflow = StateGraph(GraphState)\n",
"\n",
"# Define the nodes\n",
"workflow.add_node(\"web_search\", web_search) # web search\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # rag\n",
"workflow.add_node(\"llm_fallback\", llm_fallback) # llm\n",
"workflow.add_node(\"web_search\", web_search) # web search\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # rag\n",
"workflow.add_node(\"llm_fallback\", llm_fallback) # llm\n",
"\n",
"# Build graph\n",
"workflow.set_conditional_entry_point(\n",
@@ -837,8 +872,8 @@
" \"generate\",\n",
" grade_generation_v_documents_and_question,\n",
" {\n",
" \"not supported\": \"generate\", # Hallucinations: re-generate \n",
" \"not useful\": \"web_search\", # Fails to answer question: fall-back to web-search \n",
" \"not supported\": \"generate\", # Hallucinations: re-generate\n",
" \"not useful\": \"web_search\", # Fails to answer question: fall-back to web-search\n",
" \"useful\": END,\n",
" },\n",
")\n",
@@ -882,7 +917,9 @@
],
"source": [
"# Run\n",
"inputs = {\"question\": \"What player are the Bears expected to draft first in the 2024 NFL draft?\"}\n",
"inputs = {\n",
" \"question\": \"What player are the Bears expected to draft first in the 2024 NFL draft?\"\n",
"}\n",
"for output in app.stream(inputs):\n",
" for key, value in output.items():\n",
" # Node\n",
@@ -959,7 +996,7 @@
" pprint.pprint(\"\\n---\\n\")\n",
"\n",
"# Final generation\n",
"pprint.pprint(value [\"generation\"])"
"pprint.pprint(value[\"generation\"])"
]
},
{
@@ -1008,7 +1045,7 @@
" pprint.pprint(\"\\n---\\n\")\n",
"\n",
"# Final generation\n",
"pprint.pprint(value [\"generation\"])"
"pprint.pprint(value[\"generation\"])"
]
},
{
+48 -28
View File
@@ -214,7 +214,7 @@
}
],
"source": [
"### Retrieval Grader \n",
"### Retrieval Grader\n",
"\n",
"from langchain.prompts import PromptTemplate\n",
"from langchain_community.chat_models import ChatOllama\n",
@@ -268,10 +268,12 @@
"# LLM\n",
"llm = ChatOllama(model=local_llm, temperature=0)\n",
"\n",
"\n",
"# Post-processing\n",
"def format_docs(docs):\n",
" return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
"\n",
"\n",
"# Chain\n",
"rag_chain = prompt | llm | StrOutputParser()\n",
"\n",
@@ -299,7 +301,7 @@
}
],
"source": [
"### Hallucination Grader \n",
"### Hallucination Grader\n",
"\n",
"# LLM\n",
"llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
@@ -339,7 +341,7 @@
}
],
"source": [
"### Answer Grader \n",
"### Answer Grader\n",
"\n",
"# LLM\n",
"llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
@@ -358,7 +360,7 @@
")\n",
"\n",
"answer_grader = prompt | llm | JsonOutputParser()\n",
"answer_grader.invoke({\"question\": question,\"generation\": generation})"
"answer_grader.invoke({\"question\": question, \"generation\": generation})"
]
},
{
@@ -384,7 +386,7 @@
"# LLM\n",
"llm = ChatOllama(model=local_llm, temperature=0)\n",
"\n",
"# Prompt \n",
"# Prompt\n",
"re_write_prompt = PromptTemplate(\n",
" template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n",
" for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n",
@@ -414,6 +416,7 @@
"### Search\n",
"\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"\n",
"web_search_tool = TavilySearchResults(k=3)"
]
},
@@ -439,6 +442,7 @@
"from typing_extensions import TypedDict\n",
"from typing import List\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
@@ -446,11 +450,12 @@
" Attributes:\n",
" question: question\n",
" generation: LLM generation\n",
" documents: list of documents \n",
" documents: list of documents\n",
" \"\"\"\n",
" question : str\n",
" generation : str\n",
" documents : List[str]"
"\n",
" question: str\n",
" generation: str\n",
" documents: List[str]"
]
},
{
@@ -464,6 +469,7 @@
"\n",
"from langchain.schema import Document\n",
"\n",
"\n",
"def retrieve(state):\n",
" \"\"\"\n",
" Retrieve documents\n",
@@ -481,6 +487,7 @@
" documents = retriever.get_relevant_documents(question)\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer\n",
@@ -494,11 +501,12 @@
" print(\"---GENERATE---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" \n",
"\n",
" # RAG generation\n",
" generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
" return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
"\n",
"\n",
"def grade_documents(state):\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question.\n",
@@ -513,12 +521,14 @@
" print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" \n",
"\n",
" # Score each doc\n",
" filtered_docs = []\n",
" for d in documents:\n",
" score = retrieval_grader.invoke({\"question\": question, \"document\": d.page_content})\n",
" grade = score['score']\n",
" score = retrieval_grader.invoke(\n",
" {\"question\": question, \"document\": d.page_content}\n",
" )\n",
" grade = score[\"score\"]\n",
" if grade == \"yes\":\n",
" print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
" filtered_docs.append(d)\n",
@@ -527,6 +537,7 @@
" continue\n",
" return {\"documents\": filtered_docs, \"question\": question}\n",
"\n",
"\n",
"def transform_query(state):\n",
" \"\"\"\n",
" Transform the query to produce a better question.\n",
@@ -546,6 +557,7 @@
" better_question = question_rewriter.invoke({\"question\": question})\n",
" return {\"documents\": documents, \"question\": better_question}\n",
"\n",
"\n",
"def web_search(state):\n",
" \"\"\"\n",
" Web search based on the re-phrased question.\n",
@@ -567,8 +579,10 @@
"\n",
" return {\"documents\": web_results, \"question\": question}\n",
"\n",
"\n",
"### Edges ###\n",
"\n",
"\n",
"def route_question(state):\n",
" \"\"\"\n",
" Route question to web search or RAG.\n",
@@ -583,16 +597,17 @@
" print(\"---ROUTE QUESTION---\")\n",
" question = state[\"question\"]\n",
" print(question)\n",
" source = question_router.invoke({\"question\": question}) \n",
" source = question_router.invoke({\"question\": question})\n",
" print(source)\n",
" print(source['datasource'])\n",
" if source['datasource'] == 'web_search':\n",
" print(source[\"datasource\"])\n",
" if source[\"datasource\"] == \"web_search\":\n",
" print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n",
" return \"web_search\"\n",
" elif source['datasource'] == 'vectorstore':\n",
" elif source[\"datasource\"] == \"vectorstore\":\n",
" print(\"---ROUTE QUESTION TO RAG---\")\n",
" return \"vectorstore\"\n",
"\n",
"\n",
"def decide_to_generate(state):\n",
" \"\"\"\n",
" Determines whether to generate an answer, or re-generate a question.\n",
@@ -611,13 +626,16 @@
" if not filtered_documents:\n",
" # All documents have been filtered check_relevance\n",
" # We will re-generate a new query\n",
" print(\"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\")\n",
" print(\n",
" \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
" )\n",
" return \"transform_query\"\n",
" else:\n",
" # We have relevant documents, so generate answer\n",
" print(\"---DECISION: GENERATE---\")\n",
" return \"generate\"\n",
"\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",
@@ -634,16 +652,18 @@
" documents = state[\"documents\"]\n",
" generation = state[\"generation\"]\n",
"\n",
" score = hallucination_grader.invoke({\"documents\": documents, \"generation\": generation})\n",
" grade = score['score']\n",
" score = hallucination_grader.invoke(\n",
" {\"documents\": documents, \"generation\": generation}\n",
" )\n",
" grade = score[\"score\"]\n",
"\n",
" # Check hallucination\n",
" if grade == \"yes\":\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",
" score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
" grade = score[\"score\"]\n",
" if grade == \"yes\":\n",
" print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
" return \"useful\"\n",
@@ -675,11 +695,11 @@
"workflow = StateGraph(GraphState)\n",
"\n",
"# Define the nodes\n",
"workflow.add_node(\"web_search\", web_search) # web search\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generatae\n",
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
"workflow.add_node(\"web_search\", web_search) # web search\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generatae\n",
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
"\n",
"# Build graph\n",
"workflow.set_conditional_entry_point(\n",
@@ -753,7 +773,7 @@
"source": [
"from pprint import pprint\n",
"\n",
"# Run \n",
"# Run\n",
"inputs = {\"question\": \"What is the AlphaCodium paper about?\"}\n",
"for output in app.stream(inputs):\n",
" for key, value in output.items():\n",
+21 -15
View File
@@ -36,10 +36,12 @@
"import os\n",
"import getpass\n",
"\n",
"\n",
"def _set_env(key: str):\n",
" if key not in os.environ:\n",
" os.environ[key] = getpass.getpass(f\"{key}:\")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")\n",
"\n",
"# (Optional) For tracing\n",
@@ -211,6 +213,7 @@
"\n",
"### Edges\n",
"\n",
"\n",
"def grade_documents(state) -> Literal[\"generate\", \"rewrite\"]:\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question.\n",
@@ -254,12 +257,9 @@
"\n",
" question = messages[0].content\n",
" docs = last_message.content\n",
" \n",
" scored_result = chain.invoke(\n",
" {\"question\": question, \n",
" \"context\": docs}\n",
" )\n",
" \n",
"\n",
" scored_result = chain.invoke({\"question\": question, \"context\": docs})\n",
"\n",
" score = scored_result.binary_score\n",
"\n",
" if score == \"yes\":\n",
@@ -294,36 +294,40 @@
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"def rewrite(state):\n",
" \"\"\"\n",
" Transform the query to produce a better question.\n",
" \n",
"\n",
" Args:\n",
" state (messages): The current state\n",
" \n",
"\n",
" Returns:\n",
" dict: The updated state with re-phrased question\n",
" \"\"\"\n",
" \n",
"\n",
" print(\"---TRANSFORM QUERY---\")\n",
" messages = state[\"messages\"]\n",
" question = messages[0].content\n",
"\n",
" msg = [HumanMessage(\n",
" content=f\"\"\" \\n \n",
" msg = [\n",
" HumanMessage(\n",
" content=f\"\"\" \\n \n",
" Look at the input and try to reason about the underlying semantic intent / meaning. \\n \n",
" Here is the initial question:\n",
" \\n ------- \\n\n",
" {question} \n",
" \\n ------- \\n\n",
" Formulate an improved question: \"\"\",\n",
" )]\n",
" )\n",
" ]\n",
"\n",
" # Grader\n",
" model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n",
" response = model.invoke(msg)\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer\n",
@@ -360,8 +364,8 @@
" return {\"messages\": [response]}\n",
"\n",
"\n",
"print(\"*\"*20 + \"Prompt[rlm/rag-prompt]\" + \"*\"*20)\n",
"prompt = hub.pull(\"rlm/rag-prompt\").pretty_print() # Show what the prompt looks like"
"print(\"*\" * 20 + \"Prompt[rlm/rag-prompt]\" + \"*\" * 20)\n",
"prompt = hub.pull(\"rlm/rag-prompt\").pretty_print() # Show what the prompt looks like"
]
},
{
@@ -395,7 +399,9 @@
"retrieve = ToolNode([retriever_tool])\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieval\n",
"workflow.add_node(\"rewrite\", rewrite) # Re-writing the question\n",
"workflow.add_node(\"generate\", generate) # Generating a response after we know the documents are relevant\n",
"workflow.add_node(\n",
" \"generate\", generate\n",
") # Generating a response after we know the documents are relevant\n",
"# Call agent node to decide to retrieve or not\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
+40 -17
View File
@@ -180,23 +180,27 @@
}
],
"source": [
"### Retrieval Grader \n",
"### Retrieval Grader\n",
"\n",
"from langchain_openai import ChatOpenAI\n",
"from langchain_core.prompts import ChatPromptTemplate\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"\n",
"\n",
"# Data model\n",
"class GradeDocuments(BaseModel):\n",
" \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n",
"\n",
" binary_score: str = Field(description=\"Documents are relevant to the question, 'yes' or 'no'\")\n",
" binary_score: str = Field(\n",
" description=\"Documents are relevant to the question, 'yes' or 'no'\"\n",
" )\n",
"\n",
"# LLM with function call \n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
"\n",
"# Prompt \n",
"# Prompt\n",
"system = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
" If the document contains keyword(s) or semantic meaning related to the question, grade it as relevant. \\n\n",
" Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n",
@@ -240,10 +244,12 @@
"# LLM\n",
"llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n",
"\n",
"\n",
"# Post-processing\n",
"def format_docs(docs):\n",
" return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
"\n",
"\n",
"# Chain\n",
"rag_chain = prompt | llm | StrOutputParser()\n",
"\n",
@@ -272,16 +278,19 @@
"source": [
"### Question Re-writer\n",
"\n",
"# LLM \n",
"# LLM\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"\n",
"# Prompt \n",
"# Prompt\n",
"system = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n",
" for web search. Look at the input and try to reason about the underlying sematic intent / meaning.\"\"\"\n",
"re_write_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", system),\n",
" (\"human\", \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\"),\n",
" (\n",
" \"human\",\n",
" \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n",
" ),\n",
" ]\n",
")\n",
"\n",
@@ -307,6 +316,7 @@
"### Search\n",
"\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"\n",
"web_search_tool = TavilySearchResults(k=3)"
]
},
@@ -332,6 +342,7 @@
"from typing_extensions import TypedDict\n",
"from typing import List\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
@@ -340,12 +351,13 @@
" question: question\n",
" generation: LLM generation\n",
" web_search: whether to add search\n",
" documents: list of documents \n",
" documents: list of documents\n",
" \"\"\"\n",
" question : str\n",
" generation : str\n",
" web_search : str\n",
" documents : List[str]"
"\n",
" question: str\n",
" generation: str\n",
" web_search: str\n",
" documents: List[str]"
]
},
{
@@ -357,6 +369,7 @@
"source": [
"from langchain.schema import Document\n",
"\n",
"\n",
"def retrieve(state):\n",
" \"\"\"\n",
" Retrieve documents\n",
@@ -374,6 +387,7 @@
" documents = retriever.get_relevant_documents(question)\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer\n",
@@ -387,11 +401,12 @@
" print(\"---GENERATE---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" \n",
"\n",
" # RAG generation\n",
" generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
" return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
"\n",
"\n",
"def grade_documents(state):\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question.\n",
@@ -406,12 +421,14 @@
" print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" \n",
"\n",
" # Score each doc\n",
" filtered_docs = []\n",
" web_search = \"No\"\n",
" for d in documents:\n",
" score = retrieval_grader.invoke({\"question\": question, \"document\": d.page_content})\n",
" score = retrieval_grader.invoke(\n",
" {\"question\": question, \"document\": d.page_content}\n",
" )\n",
" grade = score.binary_score\n",
" if grade == \"yes\":\n",
" print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
@@ -422,6 +439,7 @@
" continue\n",
" return {\"documents\": filtered_docs, \"question\": question, \"web_search\": web_search}\n",
"\n",
"\n",
"def transform_query(state):\n",
" \"\"\"\n",
" Transform the query to produce a better question.\n",
@@ -440,7 +458,8 @@
" # Re-write question\n",
" better_question = question_rewriter.invoke({\"question\": question})\n",
" return {\"documents\": documents, \"question\": better_question}\n",
" \n",
"\n",
"\n",
"def web_search(state):\n",
" \"\"\"\n",
" Web search based on the re-phrased question.\n",
@@ -464,8 +483,10 @@
"\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"### Edges\n",
"\n",
"\n",
"def decide_to_generate(state):\n",
" \"\"\"\n",
" Determines whether to generate an answer, or re-generate a question.\n",
@@ -485,7 +506,9 @@
" if web_search == \"Yes\":\n",
" # All documents have been filtered check_relevance\n",
" # We will re-generate a new query\n",
" print(\"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\")\n",
" print(\n",
" \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
" )\n",
" return \"transform_query\"\n",
" else:\n",
" # We have relevant documents, so generate answer\n",
+30 -14
View File
@@ -162,7 +162,7 @@
"metadata": {},
"outputs": [],
"source": [
"run_local = 'Yes'\n",
"run_local = \"Yes\"\n",
"local_llm = \"mistral:latest\""
]
},
@@ -239,7 +239,7 @@
}
],
"source": [
"### Retrieval Grader \n",
"### Retrieval Grader\n",
"\n",
"from langchain.prompts import PromptTemplate\n",
"from langchain_community.chat_models import ChatOllama\n",
@@ -303,10 +303,12 @@
" model=\"mistral-medium\", temperature=0, mistral_api_key=mistral_api_key\n",
" )\n",
"\n",
"\n",
"# Post-processing\n",
"def format_docs(docs):\n",
" return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
"\n",
"\n",
"# Chain\n",
"rag_chain = prompt | llm | StrOutputParser()\n",
"\n",
@@ -343,7 +345,7 @@
" model=\"mistral-medium\", temperature=0, mistral_api_key=mistral_api_key\n",
" )\n",
"\n",
"# Prompt \n",
"# Prompt\n",
"re_write_prompt = PromptTemplate(\n",
" template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n",
" for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n",
@@ -373,6 +375,7 @@
"### Search\n",
"\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"\n",
"web_search_tool = TavilySearchResults(k=3)"
]
},
@@ -398,6 +401,7 @@
"from typing_extensions import TypedDict\n",
"from typing import List\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
@@ -406,12 +410,13 @@
" question: question\n",
" generation: LLM generation\n",
" web_search: whether to add search\n",
" documents: list of documents \n",
" documents: list of documents\n",
" \"\"\"\n",
" question : str\n",
" generation : str\n",
" web_search : str\n",
" documents : List[str]"
"\n",
" question: str\n",
" generation: str\n",
" web_search: str\n",
" documents: List[str]"
]
},
{
@@ -423,6 +428,7 @@
"source": [
"from langchain.schema import Document\n",
"\n",
"\n",
"def retrieve(state):\n",
" \"\"\"\n",
" Retrieve documents\n",
@@ -440,6 +446,7 @@
" documents = retriever.get_relevant_documents(question)\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer\n",
@@ -453,11 +460,12 @@
" print(\"---GENERATE---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" \n",
"\n",
" # RAG generation\n",
" generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
" return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
"\n",
"\n",
"def grade_documents(state):\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question.\n",
@@ -472,13 +480,15 @@
" print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" \n",
"\n",
" # Score each doc\n",
" filtered_docs = []\n",
" web_search = \"No\"\n",
" for d in documents:\n",
" score = retrieval_grader.invoke({\"question\": question, \"document\": d.page_content})\n",
" grade = score['score']\n",
" score = retrieval_grader.invoke(\n",
" {\"question\": question, \"document\": d.page_content}\n",
" )\n",
" grade = score[\"score\"]\n",
" if grade == \"yes\":\n",
" print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
" filtered_docs.append(d)\n",
@@ -488,6 +498,7 @@
" continue\n",
" return {\"documents\": filtered_docs, \"question\": question, \"web_search\": web_search}\n",
"\n",
"\n",
"def transform_query(state):\n",
" \"\"\"\n",
" Transform the query to produce a better question.\n",
@@ -506,7 +517,8 @@
" # Re-write question\n",
" better_question = question_rewriter.invoke({\"question\": question})\n",
" return {\"documents\": documents, \"question\": better_question}\n",
" \n",
"\n",
"\n",
"def web_search(state):\n",
" \"\"\"\n",
" Web search based on the re-phrased question.\n",
@@ -530,8 +542,10 @@
"\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"### Edges\n",
"\n",
"\n",
"def decide_to_generate(state):\n",
" \"\"\"\n",
" Determines whether to generate an answer, or re-generate a question.\n",
@@ -551,7 +565,9 @@
" if web_search == \"Yes\":\n",
" # All documents have been filtered check_relevance\n",
" # We will re-generate a new query\n",
" print(\"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\")\n",
" print(\n",
" \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
" )\n",
" return \"transform_query\"\n",
" else:\n",
" # We have relevant documents, so generate answer\n",
@@ -71,7 +71,7 @@
"source": [
"### LLM\n",
"\n",
"local_llm = 'llama3'"
"local_llm = \"llama3\""
]
},
{
@@ -126,7 +126,7 @@
}
],
"source": [
"### Retrieval Grader \n",
"### Retrieval Grader\n",
"\n",
"from langchain.prompts import PromptTemplate\n",
"from langchain_community.chat_models import ChatOllama\n",
@@ -189,10 +189,12 @@
"\n",
"llm = ChatOllama(model=local_llm, temperature=0)\n",
"\n",
"\n",
"# Post-processing\n",
"def format_docs(docs):\n",
" return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
"\n",
"\n",
"# Chain\n",
"rag_chain = prompt | llm | StrOutputParser()\n",
"\n",
@@ -221,7 +223,7 @@
}
],
"source": [
"### Hallucination Grader \n",
"### Hallucination Grader\n",
"\n",
"# LLM\n",
"llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
@@ -262,7 +264,7 @@
}
],
"source": [
"### Answer Grader \n",
"### Answer Grader\n",
"\n",
"# LLM\n",
"llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
@@ -281,7 +283,7 @@
")\n",
"\n",
"answer_grader = prompt | llm | JsonOutputParser()\n",
"answer_grader.invoke({\"question\": question,\"generation\": generation})"
"answer_grader.invoke({\"question\": question, \"generation\": generation})"
]
},
{
@@ -335,6 +337,7 @@
"### Search\n",
"\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"\n",
"web_search_tool = TavilySearchResults(k=3)"
]
},
@@ -358,6 +361,7 @@
"\n",
"### State\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
@@ -366,17 +370,20 @@
" question: question\n",
" generation: LLM generation\n",
" web_search: whether to add search\n",
" documents: list of documents \n",
" documents: list of documents\n",
" \"\"\"\n",
" question : str\n",
" generation : str\n",
" web_search : str\n",
" documents : List[str]\n",
"\n",
" question: str\n",
" generation: str\n",
" web_search: str\n",
" documents: List[str]\n",
"\n",
"\n",
"from langchain.schema import Document\n",
"\n",
"### Nodes\n",
"\n",
"\n",
"def retrieve(state):\n",
" \"\"\"\n",
" Retrieve documents from vectorstore\n",
@@ -394,6 +401,7 @@
" documents = retriever.invoke(question)\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer using RAG on retrieved documents\n",
@@ -407,11 +415,12 @@
" print(\"---GENERATE---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" \n",
"\n",
" # RAG generation\n",
" generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
" return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
"\n",
"\n",
"def grade_documents(state):\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question\n",
@@ -427,13 +436,15 @@
" print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" \n",
"\n",
" # Score each doc\n",
" filtered_docs = []\n",
" web_search = \"No\"\n",
" for d in documents:\n",
" score = retrieval_grader.invoke({\"question\": question, \"document\": d.page_content})\n",
" grade = score['score']\n",
" score = retrieval_grader.invoke(\n",
" {\"question\": question, \"document\": d.page_content}\n",
" )\n",
" grade = score[\"score\"]\n",
" # Document relevant\n",
" if grade.lower() == \"yes\":\n",
" print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
@@ -446,7 +457,8 @@
" web_search = \"Yes\"\n",
" continue\n",
" return {\"documents\": filtered_docs, \"question\": question, \"web_search\": web_search}\n",
" \n",
"\n",
"\n",
"def web_search(state):\n",
" \"\"\"\n",
" Web search based based on the question\n",
@@ -472,8 +484,10 @@
" documents = [web_results]\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"### Conditional edge\n",
"\n",
"\n",
"def route_question(state):\n",
" \"\"\"\n",
" Route question to web search or RAG.\n",
@@ -488,16 +502,17 @@
" print(\"---ROUTE QUESTION---\")\n",
" question = state[\"question\"]\n",
" print(question)\n",
" source = question_router.invoke({\"question\": question}) \n",
" source = question_router.invoke({\"question\": question})\n",
" print(source)\n",
" print(source['datasource'])\n",
" if source['datasource'] == 'web_search':\n",
" print(source[\"datasource\"])\n",
" if source[\"datasource\"] == \"web_search\":\n",
" print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n",
" return \"websearch\"\n",
" elif source['datasource'] == 'vectorstore':\n",
" elif source[\"datasource\"] == \"vectorstore\":\n",
" print(\"---ROUTE QUESTION TO RAG---\")\n",
" return \"vectorstore\"\n",
"\n",
"\n",
"def decide_to_generate(state):\n",
" \"\"\"\n",
" Determines whether to generate an answer, or add web search\n",
@@ -517,15 +532,19 @@
" if web_search == \"Yes\":\n",
" # All documents have been filtered check_relevance\n",
" # We will re-generate a new query\n",
" print(\"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, INCLUDE WEB SEARCH---\")\n",
" print(\n",
" \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, INCLUDE WEB SEARCH---\"\n",
" )\n",
" return \"websearch\"\n",
" else:\n",
" # We have relevant documents, so generate answer\n",
" print(\"---DECISION: GENERATE---\")\n",
" return \"generate\"\n",
"\n",
"\n",
"### Conditional edge\n",
"\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",
@@ -542,16 +561,18 @@
" documents = state[\"documents\"]\n",
" generation = state[\"generation\"]\n",
"\n",
" score = hallucination_grader.invoke({\"documents\": documents, \"generation\": generation})\n",
" grade = score['score']\n",
" score = hallucination_grader.invoke(\n",
" {\"documents\": documents, \"generation\": generation}\n",
" )\n",
" grade = score[\"score\"]\n",
"\n",
" # Check hallucination\n",
" if grade == \"yes\":\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",
" score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
" grade = score[\"score\"]\n",
" if grade == \"yes\":\n",
" print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
" return \"useful\"\n",
@@ -562,14 +583,16 @@
" pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n",
" return \"not supported\"\n",
"\n",
"\n",
"from langgraph.graph import END, StateGraph\n",
"\n",
"workflow = StateGraph(GraphState)\n",
"\n",
"# Define the nodes\n",
"workflow.add_node(\"websearch\", web_search) # web search\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generatae"
"workflow.add_node(\"websearch\", web_search) # web search\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generatae"
]
},
{
@@ -668,6 +691,7 @@
"\n",
"# Test\n",
"from pprint import pprint\n",
"\n",
"inputs = {\"question\": \"What are the types of agent memory?\"}\n",
"for output in app.stream(inputs):\n",
" for key, value in output.items():\n",
@@ -719,6 +743,7 @@
"\n",
"# Test\n",
"from pprint import pprint\n",
"\n",
"inputs = {\"question\": \"Who are the Bears expected to draft first in the NFL draft?\"}\n",
"for output in app.stream(inputs):\n",
" for key, value in output.items():\n",
+61 -30
View File
@@ -172,7 +172,7 @@
}
],
"source": [
"### Retrieval Grader \n",
"### Retrieval Grader\n",
"\n",
"from typing import Literal\n",
"\n",
@@ -185,13 +185,16 @@
"class GradeDocuments(BaseModel):\n",
" \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n",
"\n",
" binary_score: str = Field(description=\"Documents are relevant to the question, 'yes' or 'no'\")\n",
" binary_score: str = Field(\n",
" description=\"Documents are relevant to the question, 'yes' or 'no'\"\n",
" )\n",
"\n",
"# LLM with function call \n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
"\n",
"# Prompt \n",
"# Prompt\n",
"system = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
" It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n",
" If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n",
@@ -236,10 +239,12 @@
"# LLM\n",
"llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n",
"\n",
"\n",
"# Post-processing\n",
"def format_docs(docs):\n",
" return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
"\n",
"\n",
"# Chain\n",
"rag_chain = prompt | llm | StrOutputParser()\n",
"\n",
@@ -266,19 +271,23 @@
}
],
"source": [
"### Hallucination Grader \n",
"### Hallucination Grader\n",
"\n",
"\n",
"# Data model\n",
"class GradeHallucinations(BaseModel):\n",
" \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n",
"\n",
" binary_score: str = Field(description=\"Answer is grounded in the facts, 'yes' or 'no'\")\n",
" binary_score: str = Field(\n",
" description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n",
" )\n",
"\n",
"# LLM with function call \n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n",
"\n",
"# Prompt \n",
"# Prompt\n",
"system = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n \n",
" Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\n",
"hallucination_prompt = ChatPromptTemplate.from_messages(\n",
@@ -310,19 +319,23 @@
}
],
"source": [
"### Answer Grader \n",
"### Answer Grader\n",
"\n",
"\n",
"# Data model\n",
"class GradeAnswer(BaseModel):\n",
" \"\"\"Binary score to assess answer addresses question.\"\"\"\n",
"\n",
" binary_score: str = Field(description=\"Answer addresses the question, 'yes' or 'no'\")\n",
" binary_score: str = Field(\n",
" description=\"Answer addresses the question, 'yes' or 'no'\"\n",
" )\n",
"\n",
"# LLM with function call \n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeAnswer)\n",
"\n",
"# Prompt \n",
"# Prompt\n",
"system = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n \n",
" Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\n",
"answer_prompt = ChatPromptTemplate.from_messages(\n",
@@ -333,7 +346,7 @@
")\n",
"\n",
"answer_grader = answer_prompt | structured_llm_grader\n",
"answer_grader.invoke({\"question\": question,\"generation\": generation})"
"answer_grader.invoke({\"question\": question, \"generation\": generation})"
]
},
{
@@ -356,16 +369,19 @@
"source": [
"### Question Re-writer\n",
"\n",
"# LLM \n",
"# LLM\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"\n",
"# Prompt \n",
"# Prompt\n",
"system = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n",
" for vectorstore retrieval. Look at the input and try to reason about the underlying sematic intent / meaning.\"\"\"\n",
"re_write_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", system),\n",
" (\"human\", \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\"),\n",
" (\n",
" \"human\",\n",
" \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n",
" ),\n",
" ]\n",
")\n",
"\n",
@@ -395,6 +411,7 @@
"from typing_extensions import TypedDict\n",
"from typing import List\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
@@ -402,11 +419,12 @@
" Attributes:\n",
" question: question\n",
" generation: LLM generation\n",
" documents: list of documents \n",
" documents: list of documents\n",
" \"\"\"\n",
" question : str\n",
" generation : str\n",
" documents : List[str]"
"\n",
" question: str\n",
" generation: str\n",
" documents: List[str]"
]
},
{
@@ -420,6 +438,7 @@
"\n",
"from langchain.schema import Document\n",
"\n",
"\n",
"def retrieve(state):\n",
" \"\"\"\n",
" Retrieve documents\n",
@@ -437,6 +456,7 @@
" documents = retriever.get_relevant_documents(question)\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer\n",
@@ -450,11 +470,12 @@
" print(\"---GENERATE---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" \n",
"\n",
" # RAG generation\n",
" generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
" return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
"\n",
"\n",
"def grade_documents(state):\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question.\n",
@@ -469,11 +490,13 @@
" print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" \n",
"\n",
" # Score each doc\n",
" filtered_docs = []\n",
" for d in documents:\n",
" score = retrieval_grader.invoke({\"question\": question, \"document\": d.page_content})\n",
" score = retrieval_grader.invoke(\n",
" {\"question\": question, \"document\": d.page_content}\n",
" )\n",
" grade = score.binary_score\n",
" if grade == \"yes\":\n",
" print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
@@ -483,6 +506,7 @@
" continue\n",
" return {\"documents\": filtered_docs, \"question\": question}\n",
"\n",
"\n",
"def transform_query(state):\n",
" \"\"\"\n",
" Transform the query to produce a better question.\n",
@@ -502,8 +526,10 @@
" better_question = question_rewriter.invoke({\"question\": question})\n",
" return {\"documents\": documents, \"question\": better_question}\n",
"\n",
"\n",
"### Edges\n",
"\n",
"\n",
"def decide_to_generate(state):\n",
" \"\"\"\n",
" Determines whether to generate an answer, or re-generate a question.\n",
@@ -522,13 +548,16 @@
" if not filtered_documents:\n",
" # All documents have been filtered check_relevance\n",
" # We will re-generate a new query\n",
" print(\"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\")\n",
" print(\n",
" \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
" )\n",
" return \"transform_query\"\n",
" else:\n",
" # We have relevant documents, so generate answer\n",
" print(\"---DECISION: GENERATE---\")\n",
" return \"generate\"\n",
"\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",
@@ -545,7 +574,9 @@
" documents = state[\"documents\"]\n",
" generation = state[\"generation\"]\n",
"\n",
" score = hallucination_grader.invoke({\"documents\": documents, \"generation\": generation})\n",
" score = hallucination_grader.invoke(\n",
" {\"documents\": documents, \"generation\": generation}\n",
" )\n",
" grade = score.binary_score\n",
"\n",
" # Check hallucination\n",
@@ -553,7 +584,7 @@
" 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",
" score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
" grade = score.binary_score\n",
" if grade == \"yes\":\n",
" print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
@@ -588,10 +619,10 @@
"workflow = StateGraph(GraphState)\n",
"\n",
"# Define the nodes\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generatae\n",
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generatae\n",
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
"\n",
"# Build graph\n",
"workflow.set_entry_point(\"retrieve\")\n",
+39 -22
View File
@@ -188,7 +188,7 @@
}
],
"source": [
"### Retrieval Grader \n",
"### Retrieval Grader\n",
"\n",
"from langchain.prompts import PromptTemplate\n",
"from langchain_community.chat_models import ChatOllama\n",
@@ -241,10 +241,12 @@
"# LLM\n",
"llm = ChatOllama(model=local_llm, temperature=0)\n",
"\n",
"\n",
"# Post-processing\n",
"def format_docs(docs):\n",
" return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
"\n",
"\n",
"# Chain\n",
"rag_chain = prompt | llm | StrOutputParser()\n",
"\n",
@@ -271,7 +273,7 @@
}
],
"source": [
"### Hallucination Grader \n",
"### Hallucination Grader\n",
"\n",
"# LLM\n",
"llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
@@ -311,7 +313,7 @@
}
],
"source": [
"### Answer Grader \n",
"### Answer Grader\n",
"\n",
"# LLM\n",
"llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
@@ -330,7 +332,7 @@
")\n",
"\n",
"answer_grader = prompt | llm | JsonOutputParser()\n",
"answer_grader.invoke({\"question\": question,\"generation\": generation})"
"answer_grader.invoke({\"question\": question, \"generation\": generation})"
]
},
{
@@ -356,7 +358,7 @@
"# LLM\n",
"llm = ChatOllama(model=local_llm, temperature=0)\n",
"\n",
"# Prompt \n",
"# Prompt\n",
"re_write_prompt = PromptTemplate(\n",
" template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n",
" for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n",
@@ -390,6 +392,7 @@
"from typing_extensions import TypedDict\n",
"from typing import List\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
@@ -397,11 +400,12 @@
" Attributes:\n",
" question: question\n",
" generation: LLM generation\n",
" documents: list of documents \n",
" documents: list of documents\n",
" \"\"\"\n",
" question : str\n",
" generation : str\n",
" documents : List[str]"
"\n",
" question: str\n",
" generation: str\n",
" documents: List[str]"
]
},
{
@@ -415,6 +419,7 @@
"\n",
"from langchain.schema import Document\n",
"\n",
"\n",
"def retrieve(state):\n",
" \"\"\"\n",
" Retrieve documents\n",
@@ -432,6 +437,7 @@
" documents = retriever.get_relevant_documents(question)\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer\n",
@@ -445,11 +451,12 @@
" print(\"---GENERATE---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" \n",
"\n",
" # RAG generation\n",
" generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
" return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
"\n",
"\n",
"def grade_documents(state):\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question.\n",
@@ -464,12 +471,14 @@
" print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" \n",
"\n",
" # Score each doc\n",
" filtered_docs = []\n",
" for d in documents:\n",
" score = retrieval_grader.invoke({\"question\": question, \"document\": d.page_content})\n",
" grade = score['score']\n",
" score = retrieval_grader.invoke(\n",
" {\"question\": question, \"document\": d.page_content}\n",
" )\n",
" grade = score[\"score\"]\n",
" if grade == \"yes\":\n",
" print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
" filtered_docs.append(d)\n",
@@ -478,6 +487,7 @@
" continue\n",
" return {\"documents\": filtered_docs, \"question\": question}\n",
"\n",
"\n",
"def transform_query(state):\n",
" \"\"\"\n",
" Transform the query to produce a better question.\n",
@@ -497,8 +507,10 @@
" better_question = question_rewriter.invoke({\"question\": question})\n",
" return {\"documents\": documents, \"question\": better_question}\n",
"\n",
"\n",
"### Edges\n",
"\n",
"\n",
"def decide_to_generate(state):\n",
" \"\"\"\n",
" Determines whether to generate an answer, or re-generate a question.\n",
@@ -517,13 +529,16 @@
" if not filtered_documents:\n",
" # All documents have been filtered check_relevance\n",
" # We will re-generate a new query\n",
" print(\"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\")\n",
" print(\n",
" \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
" )\n",
" return \"transform_query\"\n",
" else:\n",
" # We have relevant documents, so generate answer\n",
" print(\"---DECISION: GENERATE---\")\n",
" return \"generate\"\n",
"\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",
@@ -540,16 +555,18 @@
" documents = state[\"documents\"]\n",
" generation = state[\"generation\"]\n",
"\n",
" score = hallucination_grader.invoke({\"documents\": documents, \"generation\": generation})\n",
" grade = score['score']\n",
" score = hallucination_grader.invoke(\n",
" {\"documents\": documents, \"generation\": generation}\n",
" )\n",
" grade = score[\"score\"]\n",
"\n",
" # Check hallucination\n",
" if grade == \"yes\":\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",
" score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
" grade = score[\"score\"]\n",
" if grade == \"yes\":\n",
" print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
" return \"useful\"\n",
@@ -583,10 +600,10 @@
"workflow = StateGraph(GraphState)\n",
"\n",
"# Define the nodes\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generatae\n",
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generatae\n",
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
"\n",
"# Build graph\n",
"workflow.set_entry_point(\"retrieve\")\n",
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -707,9 +707,10 @@
" return [{\"content\": r[\"content\"], \"url\": r[\"url\"]} for r in results]\n",
"'''\n",
"\n",
"# DDG \n",
"# DDG\n",
"search_engine = DuckDuckGoSearchAPIWrapper()\n",
"\n",
"\n",
"@tool\n",
"async def search_engine(query: str):\n",
" \"\"\"Search engine to the internet.\"\"\"\n",
@@ -1228,7 +1229,7 @@
" (\n",
" \"user\",\n",
" 'Write the complete Wiki article using markdown format. Organize citations using footnotes like \"[1]\",'\n",
" ' avoiding duplicates in the footer. Include URLs in the footer.',\n",
" \" avoiding duplicates in the footer. Include URLs in the footer.\",\n",
" ),\n",
" ]\n",
")\n",
File diff suppressed because one or more lines are too long
+9 -5
View File
@@ -381,7 +381,7 @@
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"thread = {\"configurable\": {\"thread_id\": '3'}}\n",
"thread = {\"configurable\": {\"thread_id\": \"3\"}}\n",
"for event in app.stream(\"hi! I'm bob\", thread):\n",
" for v in event.values():\n",
" print(v)"
@@ -551,7 +551,7 @@
}
],
"source": [
"thread = {\"configurable\": {\"thread_id\": '4'}}\n",
"thread = {\"configurable\": {\"thread_id\": \"4\"}}\n",
"for event in app_w_interrupt.stream(\"what is the weather in sf currently\", thread):\n",
" for v in event.values():\n",
" print(v)"
@@ -642,7 +642,9 @@
"metadata": {},
"outputs": [],
"source": [
"current_values.values[-1].tool_calls[0]['args']['query'] = \"weather in San Francisco today\""
"current_values.values[-1].tool_calls[0][\"args\"][\n",
" \"query\"\n",
"] = \"weather in San Francisco today\""
]
},
{
@@ -799,7 +801,7 @@
"source": [
"for state in app_w_interrupt.get_state_history(thread):\n",
" print(state)\n",
" print('--')\n",
" print(\"--\")\n",
" if len(state.values) == 2:\n",
" to_replay = state"
]
@@ -911,7 +913,9 @@
"metadata": {},
"outputs": [],
"source": [
"branch_config = app_w_interrupt.update_state(to_replay.config, AIMessage(content='All done here!', id=to_replay.values[-1].id))"
"branch_config = app_w_interrupt.update_state(\n",
" to_replay.config, AIMessage(content=\"All done here!\", id=to_replay.values[-1].id)\n",
")"
]
},
{
+4
View File
@@ -1323,10 +1323,14 @@
"builder.add_edge(\"draft\", \"retrieve\")\n",
"builder.add_edge(\"retrieve\", \"solve\")\n",
"builder.add_edge(\"solve\", \"evaluate\")\n",
"\n",
"\n",
"def control_edge(state: State):\n",
" if state.get(\"status\") == \"success\":\n",
" return END\n",
" return \"solve\"\n",
"\n",
"\n",
"builder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\n",
"checkpointer = SqliteSaver.from_conn_string(\":memory:\")"
]
+24 -20
View File
@@ -214,7 +214,7 @@
{
"cell_type": "code",
"execution_count": 6,
"id": "df39af17",
"id": "6b7dc713",
"metadata": {
"ExecuteTime": {
"end_time": "2024-04-19T11:25:40.358604Z",
@@ -227,8 +227,9 @@
"from IPython.display import display, HTML\n",
"import base64\n",
"\n",
"\n",
"def display_image(image_bytes: bytes, width=300):\n",
" decoded_img_bytes = base64.b64encode(image_bytes).decode('utf-8')\n",
" decoded_img_bytes = base64.b64encode(image_bytes).decode(\"utf-8\")\n",
" html = f'<img src=\"data:image/png;base64,{decoded_img_bytes}\" style=\"width: {width}px;\" />'\n",
" display(HTML(html))"
]
@@ -271,7 +272,7 @@
}
],
"source": [
"#%%capture --no-stderr\n",
"%%capture --no-stderr\n",
"%pip install pygraphviz"
]
},
@@ -374,10 +375,9 @@
}
],
"source": [
"# %%capture --no-stderr\n",
"%pip install pyppeteer\n",
"# %%capture --no-stderr\n",
"%pip install nest_asyncio"
"%%capture --no-stderr\n",
"%pip install --quiet pyppeteer\n",
"%pip install --quiet nest_asyncio"
]
},
{
@@ -418,17 +418,19 @@
"import nest_asyncio\n",
"from langchain_core.runnables.graph import CurveStyle, NodeColors, MermaidDrawMethod\n",
"\n",
"nest_asyncio.apply() # Required for Jupyter Notebook to run async functions\n",
"nest_asyncio.apply() # Required for Jupyter Notebook to run async functions\n",
"\n",
"display_image(app.get_graph().draw_mermaid_png(\n",
" curve_style=CurveStyle.LINEAR,\n",
" node_colors=NodeColors(start=\"#ffdfba\", end=\"#baffc9\", other=\"#fad7de\"),\n",
" wrap_label_n_words=9,\n",
" output_file_path=None,\n",
" draw_method=MermaidDrawMethod.PYPPETEER,\n",
" background_color=\"white\",\n",
" padding=10\n",
"))"
"display_image(\n",
" app.get_graph().draw_mermaid_png(\n",
" curve_style=CurveStyle.LINEAR,\n",
" node_colors=NodeColors(start=\"#ffdfba\", end=\"#baffc9\", other=\"#fad7de\"),\n",
" wrap_label_n_words=9,\n",
" output_file_path=None,\n",
" draw_method=MermaidDrawMethod.PYPPETEER,\n",
" background_color=\"white\",\n",
" padding=10,\n",
" )\n",
")"
]
},
{
@@ -469,9 +471,11 @@
}
],
"source": [
"display_image(app.get_graph().draw_mermaid_png(\n",
" draw_method=MermaidDrawMethod.API,\n",
"))"
"display_image(\n",
" app.get_graph().draw_mermaid_png(\n",
" draw_method=MermaidDrawMethod.API,\n",
" )\n",
")"
]
}
],
+1 -1
View File
@@ -154,7 +154,7 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager):
self.is_setup = True
@contextmanager
def cursor(self, transaction: bool = True):
def cursor(self, transaction: bool = True) -> Iterator[sqlite3.Cursor]:
"""Get a cursor for the SQLite database.
This method returns a cursor for the SQLite database. It is used internally