├── .gitattributes └── sql.ipynb /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /sql.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import os\n", 10 | "\n", 11 | "os.environ[\"OPENAI_API_KEY\"] = \" \"" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "metadata": {}, 18 | "outputs": [ 19 | { 20 | "name": "stdout", 21 | "output_type": "stream", 22 | "text": [ 23 | "File downloaded and saved as Chinook.db\n" 24 | ] 25 | } 26 | ], 27 | "source": [ 28 | "import requests\n", 29 | "\n", 30 | "url = \"https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db\"\n", 31 | "\n", 32 | "response = requests.get(url)\n", 33 | "\n", 34 | "if response.status_code == 200:\n", 35 | " # Open a local file in binary write mode\n", 36 | " with open(\"Chinook.db\", \"wb\") as file:\n", 37 | " # Write the content of the response (the file) to the local file\n", 38 | " file.write(response.content)\n", 39 | " print(\"File downloaded and saved as Chinook.db\")\n", 40 | "else:\n", 41 | " print(f\"Failed to download the file. Status code: {response.status_code}\")" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": 3, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "%%capture --no-stderr --no-display\n", 51 | "!pip install langgraph langchain_community langchain_openai" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 4, 57 | "metadata": {}, 58 | "outputs": [ 59 | { 60 | "name": "stdout", 61 | "output_type": "stream", 62 | "text": [ 63 | "sqlite\n", 64 | "['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track']\n" 65 | ] 66 | }, 67 | { 68 | "data": { 69 | "text/plain": [ 70 | "\"[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\"" 71 | ] 72 | }, 73 | "execution_count": 4, 74 | "metadata": {}, 75 | "output_type": "execute_result" 76 | } 77 | ], 78 | "source": [ 79 | "from langchain_community.utilities import SQLDatabase\n", 80 | "\n", 81 | "db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n", 82 | "print(db.dialect)\n", 83 | "print(db.get_usable_table_names())\n", 84 | "db.run(\"SELECT * FROM Artist LIMIT 10;\")" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 5, 90 | "metadata": {}, 91 | "outputs": [], 92 | "source": [ 93 | "from typing import Any\n", 94 | "\n", 95 | "from langchain_core.messages import ToolMessage\n", 96 | "from langchain_core.runnables import RunnableLambda, RunnableWithFallbacks\n", 97 | "from langgraph.prebuilt import ToolNode\n", 98 | "\n", 99 | "\n", 100 | "def create_tool_node_with_fallback(tools: list) -> RunnableWithFallbacks[Any, dict]:\n", 101 | " \"\"\"\n", 102 | " Create a ToolNode with a fallback to handle errors and surface them to the agent.\n", 103 | " \"\"\"\n", 104 | " return ToolNode(tools).with_fallbacks(\n", 105 | " [RunnableLambda(handle_tool_error)], exception_key=\"error\"\n", 106 | " )\n", 107 | "\n", 108 | "\n", 109 | "def handle_tool_error(state) -> dict:\n", 110 | " error = state.get(\"error\")\n", 111 | " tool_calls = state[\"messages\"][-1].tool_calls\n", 112 | " return {\n", 113 | " \"messages\": [\n", 114 | " ToolMessage(\n", 115 | " content=f\"Error: {repr(error)}\\n please fix your mistakes.\",\n", 116 | " tool_call_id=tc[\"id\"],\n", 117 | " )\n", 118 | " for tc in tool_calls\n", 119 | " ]\n", 120 | " }" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": 6, 126 | "metadata": {}, 127 | "outputs": [ 128 | { 129 | "name": "stdout", 130 | "output_type": "stream", 131 | "text": [ 132 | "Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\n", 133 | "\n", 134 | "CREATE TABLE \"Artist\" (\n", 135 | "\t\"ArtistId\" INTEGER NOT NULL, \n", 136 | "\t\"Name\" NVARCHAR(120), \n", 137 | "\tPRIMARY KEY (\"ArtistId\")\n", 138 | ")\n", 139 | "\n", 140 | "/*\n", 141 | "3 rows from Artist table:\n", 142 | "ArtistId\tName\n", 143 | "1\tAC/DC\n", 144 | "2\tAccept\n", 145 | "3\tAerosmith\n", 146 | "*/\n" 147 | ] 148 | } 149 | ], 150 | "source": [ 151 | "from langchain_community.agent_toolkits import SQLDatabaseToolkit\n", 152 | "from langchain_openai import ChatOpenAI\n", 153 | "\n", 154 | "toolkit = SQLDatabaseToolkit(db=db, llm=ChatOpenAI(model=\"gpt-4o\"))\n", 155 | "tools = toolkit.get_tools()\n", 156 | "\n", 157 | "list_tables_tool = next(tool for tool in tools if tool.name == \"sql_db_list_tables\")\n", 158 | "get_schema_tool = next(tool for tool in tools if tool.name == \"sql_db_schema\")\n", 159 | "\n", 160 | "print(list_tables_tool.invoke(\"\"))\n", 161 | "\n", 162 | "print(get_schema_tool.invoke(\"Artist\"))" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": 9, 168 | "metadata": {}, 169 | "outputs": [ 170 | { 171 | "name": "stdout", 172 | "output_type": "stream", 173 | "text": [ 174 | "[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\n" 175 | ] 176 | } 177 | ], 178 | "source": [ 179 | "from langchain_core.tools import tool\n", 180 | "\n", 181 | "\n", 182 | "@tool\n", 183 | "def db_query_tool(query: str) -> str:\n", 184 | " \"\"\"\n", 185 | " Execute a SQL query against the database and get back the result.\n", 186 | " If the query is not correct, an error message will be returned.\n", 187 | " If an error is returned, rewrite the query, check the query, and try again.\n", 188 | " \"\"\"\n", 189 | " result = db.run_no_throw(query)\n", 190 | " if not result:\n", 191 | " return \"Error: Query failed. Please rewrite your query and try again.\"\n", 192 | " return result\n", 193 | "\n", 194 | "\n", 195 | "print(db_query_tool.invoke(\"SELECT * FROM Artist LIMIT 10;\"))" 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": 54, 201 | "metadata": {}, 202 | "outputs": [ 203 | { 204 | "data": { 205 | "text/plain": [ 206 | "AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uw4YDXhwEcgtPJpGWXEdnvqb', 'function': {'arguments': '{\\n \"query\": \"SELECT * FROM Artist LIMIT 10;\"\\n}', 'name': 'db_query_tool'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 222, 'total_tokens': 242}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_dd932ca5d1', 'finish_reason': 'stop', 'logprobs': None}, id='run-a0f2dfe8-99da-4593-87c6-0a8ad5d877b7-0', tool_calls=[{'name': 'db_query_tool', 'args': {'query': 'SELECT * FROM Artist LIMIT 10;'}, 'id': 'call_uw4YDXhwEcgtPJpGWXEdnvqb'}], usage_metadata={'input_tokens': 222, 'output_tokens': 20, 'total_tokens': 242})" 207 | ] 208 | }, 209 | "execution_count": 54, 210 | "metadata": {}, 211 | "output_type": "execute_result" 212 | } 213 | ], 214 | "source": [ 215 | "from langchain_core.prompts import ChatPromptTemplate\n", 216 | "\n", 217 | "query_check_system = \"\"\"You are a SQL expert with a strong attention to detail.\n", 218 | "Double check the SQLite query for common mistakes, including:\n", 219 | "- Using NOT IN with NULL values\n", 220 | "- Using UNION when UNION ALL should have been used\n", 221 | "- Using BETWEEN for exclusive ranges\n", 222 | "- Data type mismatch in predicates\n", 223 | "- Properly quoting identifiers\n", 224 | "- Using the correct number of arguments for functions\n", 225 | "- Casting to the correct data type\n", 226 | "- Using the proper columns for joins\n", 227 | "\n", 228 | "If there are any of the above mistakes, rewrite the query. If there are no mistakes, just reproduce the original query.\n", 229 | "\n", 230 | "You will call the appropriate tool to execute the query after running this check.\"\"\"\n", 231 | "\n", 232 | "query_check_prompt = ChatPromptTemplate.from_messages(\n", 233 | " [(\"system\", query_check_system), (\"placeholder\", \"{messages}\")]\n", 234 | ")\n", 235 | "query_check = query_check_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n", 236 | " [db_query_tool], tool_choice=\"required\"\n", 237 | ")\n", 238 | "\n", 239 | "query_check.invoke({\"messages\": [(\"user\", \"SEECT * FROM Artist LIMIT 10;\")]})" 240 | ] 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": 51, 245 | "metadata": {}, 246 | "outputs": [], 247 | "source": [ 248 | "from typing import Annotated, Literal\n", 249 | "\n", 250 | "from langchain_core.messages import AIMessage\n", 251 | "from langchain_core.pydantic_v1 import BaseModel, Field\n", 252 | "from langchain_openai import ChatOpenAI\n", 253 | "from typing_extensions import TypedDict\n", 254 | "\n", 255 | "from langgraph.graph import END, StateGraph, START\n", 256 | "from langgraph.graph.message import AnyMessage, add_messages\n", 257 | "\n", 258 | "\n", 259 | "# Define the state for the agent\n", 260 | "class State(TypedDict):\n", 261 | " messages: Annotated[list[AnyMessage], add_messages]\n", 262 | "\n", 263 | "\n", 264 | "# Define a new graph\n", 265 | "workflow = StateGraph(State)\n", 266 | "\n", 267 | "# Get relevant tables\n", 268 | "def list_table_node(state: State):\n", 269 | " messages = [\n", 270 | " state[\"messages\"],\n", 271 | " AIMessage(\n", 272 | " content=\"\",\n", 273 | " tool_calls=[\n", 274 | " {\n", 275 | " \"name\": \"sql_db_list_tables\",\n", 276 | " \"args\": {},\n", 277 | " \"id\": \"tool_abcd123\",\n", 278 | " }\n", 279 | " ],\n", 280 | " ),\n", 281 | " ToolMessage(\n", 282 | " content=list_tables_tool.run(\"get\"), \n", 283 | " name='sql_db_list_tables', \n", 284 | " tool_call_id='tool_abcd123'\n", 285 | " ) \n", 286 | " ]\n", 287 | "\n", 288 | " model_get_schema = ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n", 289 | " [get_schema_tool]\n", 290 | " )\n", 291 | " model_message = model_get_schema.invoke(messages)\n", 292 | " messages.append(model_message)\n", 293 | "\n", 294 | " return {\"messages\": messages}\n", 295 | "\n", 296 | "workflow.add_node(\"list_table_tools\", list_table_node)\n", 297 | "\n", 298 | "# Get relevant schema\n", 299 | "workflow.add_node(\"get_schema_tool\", create_tool_node_with_fallback([get_schema_tool]))\n", 300 | "\n", 301 | "# Describe a tool to represent the end state\n", 302 | "class SubmitFinalAnswer(BaseModel):\n", 303 | " \"\"\"Submit the final answer to the user based on the query results.\"\"\"\n", 304 | "\n", 305 | " final_answer: str = Field(..., description=\"The final answer to the user\")\n", 306 | "\n", 307 | "\n", 308 | "# Add a node for a model to generate a query based on the question and schema\n", 309 | "query_gen_system = \"\"\"You are a SQL expert with a strong attention to detail.\n", 310 | "\n", 311 | "Given an input question, output a syntactically correct SQLite query to run, then look at the results of the query and return the answer.\n", 312 | "\n", 313 | "DO NOT call any tool besides SubmitFinalAnswer to submit the final answer.\n", 314 | "\n", 315 | "When generating the query:\n", 316 | "\n", 317 | "Output the SQL query that answers the input question without a tool call.\n", 318 | "\n", 319 | "Unless the user specifies a specific number of examples they wish to obtain, always limit your query to at most 5 results.\n", 320 | "You can order the results by a relevant column to return the most interesting examples in the database.\n", 321 | "Never query for all the columns from a specific table, only ask for the relevant columns given the question.\n", 322 | "\n", 323 | "If you get an error while executing a query, rewrite the query and try again.\n", 324 | "\n", 325 | "If you get an empty result set, you should try to rewrite the query to get a non-empty result set. \n", 326 | "NEVER make stuff up if you don't have enough information to answer the query... just say you don't have enough information.\n", 327 | "\n", 328 | "If you have enough information to answer the input question, simply invoke the appropriate tool to submit the final answer to the user.\n", 329 | "\n", 330 | "DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database.\"\"\"\n", 331 | "\n", 332 | "query_gen_prompt = ChatPromptTemplate.from_messages(\n", 333 | " [(\"system\", query_gen_system), (\"placeholder\", \"{messages}\")]\n", 334 | ")\n", 335 | "query_gen = query_gen_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n", 336 | " [SubmitFinalAnswer]\n", 337 | ")\n", 338 | "\n", 339 | "\n", 340 | "def query_gen_node(state: State):\n", 341 | " message = query_gen.invoke(state)\n", 342 | "\n", 343 | " # Sometimes, the LLM will hallucinate and call the wrong tool. We need to catch this and return an error message.\n", 344 | " tool_messages = []\n", 345 | " if message.tool_calls:\n", 346 | " for tc in message.tool_calls:\n", 347 | " if tc[\"name\"] != \"SubmitFinalAnswer\":\n", 348 | " tool_messages.append(\n", 349 | " ToolMessage(\n", 350 | " content=f\"Error: The wrong tool was called: {tc['name']}. Please fix your mistakes. Remember to only call SubmitFinalAnswer to submit the final answer. Generated queries should be outputted WITHOUT a tool call.\",\n", 351 | " tool_call_id=tc[\"id\"],\n", 352 | " )\n", 353 | " )\n", 354 | " else:\n", 355 | " tool_messages = []\n", 356 | " return {\"messages\": [message] + tool_messages}\n", 357 | "\n", 358 | "\n", 359 | "workflow.add_node(\"query_gen\", query_gen_node)\n", 360 | "\n", 361 | "\n", 362 | "# Add a node for the model to check the query before executing it\n", 363 | "def model_check_query(state: State) -> dict[str, list[AIMessage]]:\n", 364 | " \"\"\"\n", 365 | " Use this tool to double-check if your query is correct before executing it.\n", 366 | " \"\"\"\n", 367 | " return {\n", 368 | " \"messages\": [\n", 369 | " query_check.invoke({\"messages\": [state[\"messages\"][-1]]})]}\n", 370 | "\n", 371 | "workflow.add_node(\"correct_query\", model_check_query)\n", 372 | "\n", 373 | "\n", 374 | "# Add node for executing the query\n", 375 | "workflow.add_node(\"execute_query\", create_tool_node_with_fallback([db_query_tool]))\n", 376 | "\n", 377 | "\n", 378 | "\n", 379 | "# Define a conditional edge to decide whether to continue or end the workflow\n", 380 | "def should_continue(state: State) -> Literal[END, \"correct_query\", \"query_gen\"]:\n", 381 | " messages = state[\"messages\"]\n", 382 | " last_message = messages[-1]\n", 383 | " # If there is a tool call, then we finish\n", 384 | " if getattr(last_message, \"tool_calls\", None):\n", 385 | " return END\n", 386 | " if last_message.content.startswith(\"Error:\"):\n", 387 | " return \"query_gen\"\n", 388 | " else:\n", 389 | " return \"correct_query\"\n", 390 | "\n", 391 | "\n", 392 | "# Specify the edges between the nodes\n", 393 | "workflow.add_edge(START, \"list_table_tools\")\n", 394 | "workflow.add_edge(\"list_table_tools\", \"get_schema_tool\")\n", 395 | "workflow.add_edge(\"get_schema_tool\", \"query_gen\")\n", 396 | "workflow.add_conditional_edges(\n", 397 | " \"query_gen\",\n", 398 | " should_continue,\n", 399 | ")\n", 400 | "workflow.add_edge(\"correct_query\", \"execute_query\")\n", 401 | "workflow.add_edge(\"execute_query\", \"query_gen\")\n", 402 | "\n", 403 | "# Compile the workflow into a runnable\n", 404 | "app = workflow.compile()" 405 | ] 406 | }, 407 | { 408 | "cell_type": "code", 409 | "execution_count": 56, 410 | "metadata": {}, 411 | "outputs": [ 412 | { 413 | "data": { 414 | "image/jpeg": "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", 415 | "text/plain": [ 416 | "" 417 | ] 418 | }, 419 | "metadata": {}, 420 | "output_type": "display_data" 421 | } 422 | ], 423 | "source": [ 424 | "from IPython.display import Image, display\n", 425 | "from langchain_core.runnables.graph import MermaidDrawMethod\n", 426 | "\n", 427 | "display(\n", 428 | " Image(\n", 429 | " app.get_graph().draw_mermaid_png(\n", 430 | " draw_method=MermaidDrawMethod.API,\n", 431 | " )\n", 432 | " )\n", 433 | ")" 434 | ] 435 | }, 436 | { 437 | "cell_type": "code", 438 | "execution_count": 55, 439 | "metadata": {}, 440 | "outputs": [ 441 | { 442 | "data": { 443 | "text/plain": [ 444 | "'The sales agent who made the most in sales in 2009 is Steve Johnson with total sales of 164.34.'" 445 | ] 446 | }, 447 | "execution_count": 55, 448 | "metadata": {}, 449 | "output_type": "execute_result" 450 | } 451 | ], 452 | "source": [ 453 | "import json\n", 454 | "\n", 455 | "messages = app.invoke(\n", 456 | " {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n", 457 | ")\n", 458 | "json_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n", 459 | " \"arguments\"\n", 460 | "]\n", 461 | "json.loads(json_str)[\"final_answer\"]" 462 | ] 463 | }, 464 | { 465 | "cell_type": "code", 466 | "execution_count": 52, 467 | "metadata": {}, 468 | "outputs": [ 469 | { 470 | "name": "stdout", 471 | "output_type": "stream", 472 | "text": [ 473 | "{'list_table_tools': {'messages': [HumanMessage(content='Which sales agent made the most in sales in 2009?', id='5baa4d66-d372-4552-8453-a830b990b7f7'), AIMessage(content='', tool_calls=[{'name': 'sql_db_list_tables', 'args': {}, 'id': 'tool_abcd123'}]), ToolMessage(content='Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track', name='sql_db_list_tables', tool_call_id='tool_abcd123'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_7X2B7tHwserfvHZ8rXwM8nFu', 'function': {'arguments': '{\"table_names\":\"Employee, Invoice\"}', 'name': 'sql_db_schema'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 18, 'prompt_tokens': 177, 'total_tokens': 195}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-ea46a77b-694f-40a5-b602-c4190df23554-0', tool_calls=[{'name': 'sql_db_schema', 'args': {'table_names': 'Employee, Invoice'}, 'id': 'call_7X2B7tHwserfvHZ8rXwM8nFu'}], usage_metadata={'input_tokens': 177, 'output_tokens': 18, 'total_tokens': 195})]}}\n", 474 | "{'get_schema_tool': {'messages': [ToolMessage(content='\\nCREATE TABLE \"Employee\" (\\n\\t\"EmployeeId\" INTEGER NOT NULL, \\n\\t\"LastName\" NVARCHAR(20) NOT NULL, \\n\\t\"FirstName\" NVARCHAR(20) NOT NULL, \\n\\t\"Title\" NVARCHAR(30), \\n\\t\"ReportsTo\" INTEGER, \\n\\t\"BirthDate\" DATETIME, \\n\\t\"HireDate\" DATETIME, \\n\\t\"Address\" NVARCHAR(70), \\n\\t\"City\" NVARCHAR(40), \\n\\t\"State\" NVARCHAR(40), \\n\\t\"Country\" NVARCHAR(40), \\n\\t\"PostalCode\" NVARCHAR(10), \\n\\t\"Phone\" NVARCHAR(24), \\n\\t\"Fax\" NVARCHAR(24), \\n\\t\"Email\" NVARCHAR(60), \\n\\tPRIMARY KEY (\"EmployeeId\"), \\n\\tFOREIGN KEY(\"ReportsTo\") REFERENCES \"Employee\" (\"EmployeeId\")\\n)\\n\\n/*\\n3 rows from Employee table:\\nEmployeeId\\tLastName\\tFirstName\\tTitle\\tReportsTo\\tBirthDate\\tHireDate\\tAddress\\tCity\\tState\\tCountry\\tPostalCode\\tPhone\\tFax\\tEmail\\n1\\tAdams\\tAndrew\\tGeneral Manager\\tNone\\t1962-02-18 00:00:00\\t2002-08-14 00:00:00\\t11120 Jasper Ave NW\\tEdmonton\\tAB\\tCanada\\tT5K 2N1\\t+1 (780) 428-9482\\t+1 (780) 428-3457\\tandrew@chinookcorp.com\\n2\\tEdwards\\tNancy\\tSales Manager\\t1\\t1958-12-08 00:00:00\\t2002-05-01 00:00:00\\t825 8 Ave SW\\tCalgary\\tAB\\tCanada\\tT2P 2T3\\t+1 (403) 262-3443\\t+1 (403) 262-3322\\tnancy@chinookcorp.com\\n3\\tPeacock\\tJane\\tSales Support Agent\\t2\\t1973-08-29 00:00:00\\t2002-04-01 00:00:00\\t1111 6 Ave SW\\tCalgary\\tAB\\tCanada\\tT2P 5M5\\t+1 (403) 262-3443\\t+1 (403) 262-6712\\tjane@chinookcorp.com\\n*/\\n\\n\\nCREATE TABLE \"Invoice\" (\\n\\t\"InvoiceId\" INTEGER NOT NULL, \\n\\t\"CustomerId\" INTEGER NOT NULL, \\n\\t\"InvoiceDate\" DATETIME NOT NULL, \\n\\t\"BillingAddress\" NVARCHAR(70), \\n\\t\"BillingCity\" NVARCHAR(40), \\n\\t\"BillingState\" NVARCHAR(40), \\n\\t\"BillingCountry\" NVARCHAR(40), \\n\\t\"BillingPostalCode\" NVARCHAR(10), \\n\\t\"Total\" NUMERIC(10, 2) NOT NULL, \\n\\tPRIMARY KEY (\"InvoiceId\"), \\n\\tFOREIGN KEY(\"CustomerId\") REFERENCES \"Customer\" (\"CustomerId\")\\n)\\n\\n/*\\n3 rows from Invoice table:\\nInvoiceId\\tCustomerId\\tInvoiceDate\\tBillingAddress\\tBillingCity\\tBillingState\\tBillingCountry\\tBillingPostalCode\\tTotal\\n1\\t2\\t2009-01-01 00:00:00\\tTheodor-Heuss-Straße 34\\tStuttgart\\tNone\\tGermany\\t70174\\t1.98\\n2\\t4\\t2009-01-02 00:00:00\\tUllevålsveien 14\\tOslo\\tNone\\tNorway\\t0171\\t3.96\\n3\\t8\\t2009-01-03 00:00:00\\tGrétrystraat 63\\tBrussels\\tNone\\tBelgium\\t1000\\t5.94\\n*/', name='sql_db_schema', tool_call_id='call_7X2B7tHwserfvHZ8rXwM8nFu')]}}\n", 475 | "{'query_gen': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_xVTfaqdKieBCtQ6gRiPwm5O6', 'function': {'arguments': '{\"table_names\":\"Customer\"}', 'name': 'sql_db_schema'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 16, 'prompt_tokens': 1179, 'total_tokens': 1195}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_dd932ca5d1', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-a43b968e-9fdb-4ae9-856d-dc4420230df5-0', tool_calls=[{'name': 'sql_db_schema', 'args': {'table_names': 'Customer'}, 'id': 'call_xVTfaqdKieBCtQ6gRiPwm5O6'}], usage_metadata={'input_tokens': 1179, 'output_tokens': 16, 'total_tokens': 1195}), ToolMessage(content='Error: The wrong tool was called: sql_db_schema. Please fix your mistakes. Remember to only call SubmitFinalAnswer to submit the final answer. Generated queries should be outputted WITHOUT a tool call.', id='9e055c3b-c764-4323-8bf5-cdb215bd5124', tool_call_id='call_xVTfaqdKieBCtQ6gRiPwm5O6')]}}\n", 476 | "{'query_gen': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_NSM1mo7O2NxeU2PlPgKtZbWu', 'function': {'arguments': '{\"table_names\":[\"Customer\"]}', 'name': 'sql_db_schema'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 17, 'prompt_tokens': 1245, 'total_tokens': 1262}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-70b0487b-f867-4d3b-a541-1ee42d0d1843-0', tool_calls=[{'name': 'sql_db_schema', 'args': {'table_names': ['Customer']}, 'id': 'call_NSM1mo7O2NxeU2PlPgKtZbWu'}], usage_metadata={'input_tokens': 1245, 'output_tokens': 17, 'total_tokens': 1262}), ToolMessage(content='Error: The wrong tool was called: sql_db_schema. Please fix your mistakes. Remember to only call SubmitFinalAnswer to submit the final answer. Generated queries should be outputted WITHOUT a tool call.', id='f55d66fb-19cd-42dc-9728-863cd5daff38', tool_call_id='call_NSM1mo7O2NxeU2PlPgKtZbWu')]}}\n", 477 | "{'query_gen': {'messages': [AIMessage(content=\"To determine which sales agent made the most in sales in 2009, we need to join the `Invoice`, `Customer`, and `Employee` tables. Here's the query to find the top sales agent:\\n\\n```sql\\nSELECT e.FirstName, e.LastName, SUM(i.Total) as TotalSales\\nFROM Invoice i\\nJOIN Customer c ON i.CustomerId = c.CustomerId\\nJOIN Employee e ON c.SupportRepId = e.EmployeeId\\nWHERE strftime('%Y', i.InvoiceDate) = '2009'\\nGROUP BY e.EmployeeId\\nORDER BY TotalSales DESC\\nLIMIT 1;\\n```\", response_metadata={'token_usage': {'completion_tokens': 124, 'prompt_tokens': 1312, 'total_tokens': 1436}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'stop', 'logprobs': None}, id='run-53ed8165-f812-4892-bc2c-9d20cc8a9d24-0', usage_metadata={'input_tokens': 1312, 'output_tokens': 124, 'total_tokens': 1436})]}}\n", 478 | "{'correct_query': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_pDgr0GkUv0pqwXPngy58lL8B', 'function': {'arguments': '{\"query\":\"SELECT e.FirstName, e.LastName, SUM(i.Total) as TotalSales\\\\nFROM Invoice i\\\\nJOIN Customer c ON i.CustomerId = c.CustomerId\\\\nJOIN Employee e ON c.SupportRepId = e.EmployeeId\\\\nWHERE strftime(\\'%Y\\', i.InvoiceDate) = \\'2009\\'\\\\nGROUP BY e.EmployeeId\\\\nORDER BY TotalSales DESC\\\\nLIMIT 1;\"}', 'name': 'db_query_tool'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 90, 'prompt_tokens': 336, 'total_tokens': 426}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_dd932ca5d1', 'finish_reason': 'stop', 'logprobs': None}, id='run-79845919-aea8-4139-a50b-fc9696cb56aa-0', tool_calls=[{'name': 'db_query_tool', 'args': {'query': \"SELECT e.FirstName, e.LastName, SUM(i.Total) as TotalSales\\nFROM Invoice i\\nJOIN Customer c ON i.CustomerId = c.CustomerId\\nJOIN Employee e ON c.SupportRepId = e.EmployeeId\\nWHERE strftime('%Y', i.InvoiceDate) = '2009'\\nGROUP BY e.EmployeeId\\nORDER BY TotalSales DESC\\nLIMIT 1;\"}, 'id': 'call_pDgr0GkUv0pqwXPngy58lL8B'}], usage_metadata={'input_tokens': 336, 'output_tokens': 90, 'total_tokens': 426})]}}\n", 479 | "{'execute_query': {'messages': [ToolMessage(content=\"[('Steve', 'Johnson', 164.34)]\", name='db_query_tool', tool_call_id='call_pDgr0GkUv0pqwXPngy58lL8B')]}}\n", 480 | "{'query_gen': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_A3CKekODdXBGOYtZwCeqovTT', 'function': {'arguments': '{\"final_answer\":\"The sales agent who made the most in sales in 2009 is Steve Johnson with total sales of 164.34.\"}', 'name': 'SubmitFinalAnswer'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 41, 'prompt_tokens': 1552, 'total_tokens': 1593}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-476625ab-5983-4982-a484-406ab907bd30-0', tool_calls=[{'name': 'SubmitFinalAnswer', 'args': {'final_answer': 'The sales agent who made the most in sales in 2009 is Steve Johnson with total sales of 164.34.'}, 'id': 'call_A3CKekODdXBGOYtZwCeqovTT'}], usage_metadata={'input_tokens': 1552, 'output_tokens': 41, 'total_tokens': 1593})]}}\n" 481 | ] 482 | } 483 | ], 484 | "source": [ 485 | "for event in app.stream(\n", 486 | " {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n", 487 | "):\n", 488 | " print(event)" 489 | ] 490 | } 491 | ], 492 | "metadata": { 493 | "kernelspec": { 494 | "display_name": "Python 3", 495 | "language": "python", 496 | "name": "python3" 497 | }, 498 | "language_info": { 499 | "codemirror_mode": { 500 | "name": "ipython", 501 | "version": 3 502 | }, 503 | "file_extension": ".py", 504 | "mimetype": "text/x-python", 505 | "name": "python", 506 | "nbconvert_exporter": "python", 507 | "pygments_lexer": "ipython3", 508 | "version": "3.10.10" 509 | } 510 | }, 511 | "nbformat": 4, 512 | "nbformat_minor": 2 513 | } 514 | --------------------------------------------------------------------------------