Querying Structured Data (SQL)

Open in Colab

In the previous tutorial, we explored how agentic RAG can handle complex queries on structured data in the form of tables using pandas. Now, we’ll see how we can do the same for SQL databases.

Consider a scenario similar to the previous tutorial where we have evaluation results for an LLM application. However, instead of a CSV file, this data is now stored in a SQLite database. Users might still ask questions like “What’s the average score for a specific use case?” or “Which configuration has the lowest latency?”, but now we’ll answer these using SQL queries instead of pandas operations.

In this tutorial, we’ll cover:

  • Setting up a SQLite database
  • Creating a function to execute SQL queries
  • Building an agent for querying SQL databases
  • Running the agent with various types of queries

By implementing these techniques, we’ll expand our agentic RAG system to handle structured data in SQL databases, complementing our previous work with tabular data in pandas.

Setup

To get started, first we need to install the cohere library and create a Cohere client.

PYTHON
1import json
2import os
3import cohere
4import sqlite3
5import pandas as pd
6
7co = cohere.ClientV2("COHERE_API_KEY") # Get your free API key: https://dashboard.cohere.com/api-keys

Creating a SQLite database

Next, we’ll create a SQLite database to store our evaluation results. SQLite is a lightweight, serverless database engine that’s perfect for small to medium-sized applications. Here’s what we’re going to do:

  1. Create a new SQLite database file named evaluation_results.db.
  2. Create a table called evaluation_results with columns for usecase, run, score, temperature, tokens, and latency.
  3. Insert sample data into the table to simulate our evaluation results.
PYTHON
1# Create a connection to a new SQLite database (or connect to an existing one)
2conn = sqlite3.connect('evaluation_results.db')
3cursor = conn.cursor()
4
5# Execute the CREATE TABLE command
6cursor.execute('''
7CREATE TABLE evaluation_results (
8 usecase TEXT,
9 run TEXT,
10 score FLOAT,
11 temperature FLOAT,
12 tokens INTEGER,
13 latency FLOAT
14)
15''')
16
17# Execute the INSERT commands
18data = [
19 ('extract_names', 'A', 0.5, 0.3, 103, 1.12),
20 ('draft_email', 'A', 0.6, 0.3, 252, 2.5),
21 ('summarize_article', 'A', 0.8, 0.3, 350, 4.2),
22 ('extract_names', 'B', 0.2, 0.3, 101, 2.85),
23 ('draft_email', 'B', 0.4, 0.3, 230, 3.2),
24 ('summarize_article', 'B', 0.6, 0.3, 370, 4.2),
25 ('extract_names', 'C', 0.7, 0.3, 101, 2.22),
26 ('draft_email', 'C', 0.5, 0.3, 221, 2.5),
27 ('summarize_article', 'C', 0.1, 0.3, 361, 3.9),
28 ('extract_names', 'D', 0.7, 0.5, 120, 3.2),
29 ('draft_email', 'D', 0.8, 0.5, 280, 3.4),
30 ('summarize_article', 'D', 0.9, 0.5, 342, 4.8)
31]
32
33cursor.executemany('INSERT INTO evaluation_results VALUES (?,?,?,?,?,?)', data)
34
35# Commit the changes and close the connection
36conn.commit()
37conn.close()

Creating a function to query a SQL database

Next, we’ll define a function called sql_table_query that allows us to execute SQL queries on our evaluation_results database.

This function will enable us to retrieve and analyze data from our evaluation_results table, allowing for dynamic querying based on our specific needs.

PYTHON
1def sql_table_query(query: str) -> dict:
2 """
3 Execute an SQL query on the evaluation_results table and return the result as a dictionary.
4
5 Args:
6 query (str): SQL query to execute on the evaluation_results table
7
8 Returns:
9 dict: Result of the SQL query
10 """
11 try:
12 # Connect to the SQLite database
13 conn = sqlite3.connect('evaluation_results.db')
14
15 # Execute the query and fetch the results into a DataFrame
16 df = pd.read_sql_query(query, conn)
17
18 # Close the connection
19 conn.close()
20
21 # Convert DataFrame to dictionary
22 result_dict = df.to_dict(orient='records')
23
24 return result_dict
25
26 except sqlite3.Error as e:
27 print(f"An error occurred: {e}")
28 return str(e)
29 except Exception as e:
30 print(f"An unexpected error occurred: {e}")
31 return str(e)
32
33functions_map = {
34 "sql_table_query": sql_table_query
35}

We can test the function by running a simple query:

PYTHON
1result = sql_table_query("SELECT * FROM evaluation_results WHERE usecase = 'extract_names'")
2print(result)
[{'usecase': 'extract_names', 'run': 'A', 'score': 0.5, 'temperature': 0.3, 'tokens': 103, 'latency': 1.12}, {'usecase': 'extract_names', 'run': 'B', 'score': 0.2, 'temperature': 0.3, 'tokens': 101, 'latency': 2.85}, {'usecase': 'extract_names', 'run': 'C', 'score': 0.7, 'temperature': 0.3, 'tokens': 101, 'latency': 2.22}, {'usecase': 'extract_names', 'run': 'D', 'score': 0.7, 'temperature': 0.5, 'tokens': 120, 'latency': 3.2}]

Setting up a tool to interact with the database

Next, we’ll create a tool that will allow the agent to interact with the SQLite database containing our evaluation results.

PYTHON
1sql_table_query_tool = {
2 "type": "function",
3 "function": {
4 "name": "sql_table_query",
5 "description": "Execute an SQL query on the evaluation_results table in the SQLite database. The table has columns 'usecase', 'run', 'score', 'temperature', 'tokens', and 'latency'.",
6 "parameters": {
7 "type": "object",
8 "properties": {
9 "query": {
10 "type": "string",
11 "description": "SQL query to execute on the evaluation_results table"
12 }
13 },
14 "required": ["query"]
15 }
16 }
17}
18
19tools = [sql_table_query_tool]

Building an agent for querying SQL data

Next, let’s create a run_agent function to run the agentic RAG workflow, the same as in Part 1.

The only change we are making here is to make the system message more specific and describe the database schema to the agent.

PYTHON
1system_message="""## Task and Context
2You are an assistant who helps developers analyze LLM application evaluation results from a SQLite database. The database contains a table named 'evaluation_results' with the following schema:
3
4- usecase (TEXT): The type of task being evaluated
5- run (TEXT): The identifier for a specific evaluation run
6- score (REAL): The performance score of the run
7- temperature (REAL): The temperature setting used for the LLM
8- tokens (INTEGER): The number of tokens used in the run
9- latency (REAL): The time taken for the run in seconds
10
11You can use SQL queries to analyze this data and provide insights to the developers."""
PYTHON
1model = "command-r-plus-08-2024"
2
3def run_agent(query, messages=None):
4 if messages is None:
5 messages = []
6
7 if "system" not in {m.get("role") for m in messages}:
8 messages.append({"role": "system", "content": system_message})
9
10 # Step 1: get user message
11 print(f"Question:\n{query}")
12 print("="*50)
13
14 messages.append({"role": "user", "content": query})
15
16 # Step 2: Generate tool calls (if any)
17 response = co.chat(
18 model=model,
19 messages=messages,
20 tools=tools,
21 temperature=0.1
22 )
23
24 while response.message.tool_calls:
25
26 print("Tool plan:")
27 print(response.message.tool_plan,"\n")
28 print("Tool calls:")
29 for tc in response.message.tool_calls:
30 # print(f"Tool name: {tc.function.name} | Parameters: {tc.function.arguments}")
31 if tc.function.name == "analyze_evaluation_results":
32 print(f"Tool name: {tc.function.name}")
33 tool_call_prettified = print("\n".join(f" {line}" for line_num, line in enumerate(json.loads(tc.function.arguments)["code"].splitlines())))
34 print(tool_call_prettified)
35 else:
36 print(f"Tool name: {tc.function.name} | Parameters: {tc.function.arguments}")
37 print("="*50)
38
39 messages.append({"role": "assistant", "tool_calls": response.message.tool_calls, "tool_plan": response.message.tool_plan})
40
41 # Step 3: Get tool results
42 for tc in response.message.tool_calls:
43 tool_result = functions_map[tc.function.name](**json.loads(tc.function.arguments))
44 tool_content = [({"type": "document", "document": {"data": json.dumps(tool_result)}})]
45
46 messages.append({"role": "tool", "tool_call_id": tc.id, "content": tool_content})
47
48 # Step 4: Generate response and citations
49 response = co.chat(
50 model=model,
51 messages=messages,
52 tools=tools,
53 temperature=0.1
54 )
55
56 messages.append({"role": "assistant", "content": response.message.content[0].text})
57
58 # Print final response
59 print("Response:")
60 print(response.message.content[0].text)
61 print("="*50)
62
63 # Print citations (if any)
64 verbose_source = False # Change to True to display the contents of a source
65 if response.message.citations:
66 print("CITATIONS:\n")
67 for citation in response.message.citations:
68 print(f"Start: {citation.start}| End:{citation.end}| Text:'{citation.text}' ")
69 print("Sources:")
70 for idx, source in enumerate(citation.sources):
71 print(f"{idx+1}. {source.id}")
72 if verbose_source:
73 print(f"{source.tool_output}")
74 print("\n")
75
76 return messages

Running the agent

Let’s now ask the agent the same set of questions we asked in the previous chapter. While the previous chapter translates the questions into pandas Python code, this time the agent will be using SQL queries.

PYTHON
1messages = run_agent("What's the average evaluation score in run A")
2# Answer: 0.63
Question:
What's the average evaluation score in run A
==================================================
Tool plan:
I will write a SQL query to find the average evaluation score in run A.
Tool calls:
Tool name: sql_table_query | Parameters: {"query":"SELECT AVG(score) FROM evaluation_results WHERE run = 'A'"}
==================================================
Response:
The average evaluation score in run A is **0.63**.
==================================================
CITATIONS:
Start: 43| End:47| Text:'0.63'
Sources:
1. sql_table_query_jm4e5yp0ptad:0
PYTHON
1messages = run_agent("What's the latency of the highest-scoring run for the summarize_article use case?")
2# Answer: 4.8
Question:
What's the latency of the highest-scoring run for the summarize_article use case?
==================================================
Tool plan:
I will write and execute a SQL query to find the latency of the highest-scoring run for the summarize_article use case.
Tool calls:
Tool name: sql_table_query | Parameters: {"query":"SELECT latency FROM evaluation_results WHERE usecase = 'summarize_article' ORDER BY score DESC LIMIT 1"}
==================================================
Response:
The latency of the highest-scoring run for the summarize_article use case is 4.8 seconds.
==================================================
CITATIONS:
Start: 77| End:89| Text:'4.8 seconds.'
Sources:
1. sql_table_query_mxyzvcnsgdab:0
PYTHON
1messages = run_agent("Which use case uses the least amount of tokens on average? Show the comparison of all use cases in a markdown table.")
2# Answer: extract_names (106.25), draft_email (245.75), summarize_article (355.75)
Question:
Which use case uses the least amount of tokens on average? Show the comparison of all use cases in a markdown table.
==================================================
Tool plan:
I will use the SQL tool to query the database for the average number of tokens used for each use case. I will then use the directly_answer tool to present the results in a markdown table.
Tool calls:
Tool name: sql_table_query | Parameters: {"query":"SELECT usecase, AVG(tokens) as avg_tokens FROM evaluation_results GROUP BY usecase ORDER BY avg_tokens ASC"}
==================================================
Response:
The use case that uses the least amount of tokens on average is `extract_names`, with an average of 106.25 tokens. Here's a table comparing the average number of tokens used for all use cases:
| Use Case | Average Tokens |
|---|---|
| `extract_names` | 106.25 |
| `draft_email` | 245.75 |
| `summarize_article` | 355.75 |
==================================================
CITATIONS:
Start: 64| End:114| Text:'`extract_names`, with an average of 106.25 tokens.'
Sources:
1. sql_table_query_2qyr8vpqrf2v:0
Start: 236| End:250| Text:'`extract_names'
Sources:
1. sql_table_query_2qyr8vpqrf2v:0
Start: 254| End:260| Text:'106.25'
Sources:
1. sql_table_query_2qyr8vpqrf2v:0
Start: 265| End:277| Text:'`draft_email'
Sources:
1. sql_table_query_2qyr8vpqrf2v:0
Start: 281| End:287| Text:'245.75'
Sources:
1. sql_table_query_2qyr8vpqrf2v:0
Start: 292| End:310| Text:'`summarize_article'
Sources:
1. sql_table_query_2qyr8vpqrf2v:0
Start: 314| End:320| Text:'355.75'
Sources:
1. sql_table_query_2qyr8vpqrf2v:0

Summary

In this tutorial, we learned about:

  • How to set up a SQLite database for structured data
  • How to create a function to execute SQL queries
  • How to build an agent for querying the database
  • How to run the agent

By implementing these techniques, we’ve further expanded our agentic RAG system to handle structured data in the form of SQL databases. This allows for more powerful and flexible querying capabilities, especially when dealing with large datasets or complex relationships between data.

This tutorial completes our exploration of structured data handling in the agentic RAG system, covering both tabular data (using pandas) and relational databases (using SQL). These capabilities significantly enhance the system’s ability to work with diverse data formats and structures.