Building an Agent around a Query Pipeline#
In this cookbook we show you how to build an agent around a query pipeline.
Agents offer the ability to do complex, sequential reasoning on top of any query DAG that you have setup. Conceptually this is also one of the ways you can add a “loop” to the graph.
We show you two examples of agents you can implement:
a full ReAct agent that can do tool picking
a “simple” agent that adds a retry layer around a text-to-sql query engine.
Setup Data#
We use the chinook database as sample data. Source.
!curl "https://www.sqlitetutorial.net/wp-content/uploads/2018/03/chinook.zip" -O ./chinook.zip
!unzip ./chinook.zip
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 298k 100 298k 0 0 2327k 0 --:--:-- --:--:-- --:--:-- 2387k
curl: (6) Could not resolve host: .
Archive: ./chinook.zip
inflating: chinook.db
from llama_index import SQLDatabase
from sqlalchemy import (
create_engine,
MetaData,
Table,
Column,
String,
Integer,
select,
column,
)
engine = create_engine("sqlite:///chinook.db")
sql_database = SQLDatabase(engine)
from llama_index.query_pipeline import QueryPipeline
Setup Observability#
We setup Arize Phoenix for observability.
# setup Arize Phoenix for logging/observability
import phoenix as px
import llama_index
px.launch_app()
llama_index.set_global_handler("arize_phoenix")
🌍 To view the Phoenix app in your browser, visit http://127.0.0.1:6006/
đź“ş To view the Phoenix app in a notebook, run `px.active_session().view()`
đź“– For more information on how to use Phoenix, check out https://docs.arize.com/phoenix
Setup Text-to-SQL Query Engine / Tool#
Now we setup a simple text-to-SQL tool: given a query, translate text to SQL, execute against database, and get back a result.
from llama_index.query_engine import NLSQLTableQueryEngine
from llama_index.tools.query_engine import QueryEngineTool
sql_query_engine = NLSQLTableQueryEngine(
sql_database=sql_database,
tables=["albums", "tracks", "artists"],
verbose=True,
)
sql_tool = QueryEngineTool.from_defaults(
query_engine=sql_query_engine,
name="sql_tool",
description=(
"Useful for translating a natural language query into a SQL query"
),
)
Setup ReAct Agent Pipeline#
We now setup a ReAct pipeline for a single step using our Query Pipeline syntax. This is a multi-part process that does the following:
Takes in agent inputs
Calls ReAct prompt using LLM to generate next action/tool (or returns a response).
If tool/action is selected, call tool pipeline to execute tool + collect response.
If response is generated, get response.
Throughout this we’ll use a variety of agent-specific query components. Unlike normal query pipelines, these are specifically designed for query pipelines that are used in a QueryPipelineAgentWorker
:
An
AgentInputComponent
that allows you to convert the agent inputs (Task, state dictionary) into a set of inputs for the query pipeline.An
AgentFnComponent
: a general processor that allows you to take in the current Task, state, as well as any arbitrary inputs, and returns an output. In this cookbook we define a function component to format the ReAct prompt. However, you can put this anywhere.[Not used in this notebook] An
CustomAgentComponent
: similar toAgentFnComponent
, you can implement_run_component
to define your own logic, with access to Task and state. It is more verbose but more flexible thanAgentFnComponent
(e.g. you can define init variables, and callbacks are in the base class).
Note that any function passed into AgentFnComponent
and AgentInputComponent
MUST include task
and state
as input variables, as these are inputs passed from the agent.
Note that the output of an agentic query pipeline MUST be Tuple[AgentChatResponse, bool]
. You’ll see this below.
from llama_index.query_pipeline import QueryPipeline as QP
qp = QP(verbose=True)
Define Agent Input Component#
Here we define the agent input component, called at the beginning of every agent step. Besides passing along the input, we also do initialization/state modification.
from llama_index.agent.react.types import (
ActionReasoningStep,
ObservationReasoningStep,
ResponseReasoningStep,
)
from llama_index.agent import Task, AgentChatResponse
from llama_index.query_pipeline import (
AgentInputComponent,
AgentFnComponent,
CustomAgentComponent,
QueryComponent,
ToolRunnerComponent,
)
from llama_index.llms import MessageRole
from typing import Dict, Any, Optional, Tuple, List, cast
## Agent Input Component
## This is the component that produces agent inputs to the rest of the components
## Can also put initialization logic here.
def agent_input_fn(task: Task, state: Dict[str, Any]) -> Dict[str, Any]:
"""Agent input function.
Returns:
A Dictionary of output keys and values. If you are specifying
src_key when defining links between this component and other
components, make sure the src_key matches the specified output_key.
"""
# initialize current_reasoning
if "current_reasoning" not in state:
state["current_reasoning"] = []
reasoning_step = ObservationReasoningStep(observation=task.input)
state["current_reasoning"].append(reasoning_step)
return {"input": task.input}
agent_input_component = AgentInputComponent(fn=agent_input_fn)
Define Agent Prompt#
Here we define the agent component that generates a ReAct prompt, and after the output is generated from the LLM, parses into a structured object.
from llama_index.agent.react.formatter import ReActChatFormatter
from llama_index.query_pipeline import InputComponent, Link
from llama_index.llms import ChatMessage
from llama_index.tools import BaseTool
## define prompt function
def react_prompt_fn(
task: Task, state: Dict[str, Any], input: str, tools: List[BaseTool]
) -> List[ChatMessage]:
# Add input to reasoning
chat_formatter = ReActChatFormatter()
return chat_formatter.format(
tools,
chat_history=task.memory.get() + state["memory"].get_all(),
current_reasoning=state["current_reasoning"],
)
react_prompt_component = AgentFnComponent(
fn=react_prompt_fn, partial_dict={"tools": [sql_tool]}
)
Define Agent Output Parser + Tool Pipeline#
Once the LLM gives an output, we have a decision tree:
If an answer is given, then we’re done. Process the output
If an action is given, we need to execute the specified tool with the specified args, and then process the output.
Tool calling can be done via the ToolRunnerComponent
module. This is a simple wrapper module that takes in a list of tools, and can be “executed” with the specified tool name (every tool has a name) and tool action.
We implement this overall module OutputAgentComponent
that subclasses CustomAgentComponent
.
Note: we also implement sub_query_components
to pass through higher-level callback managers to the tool runner submodule.
from typing import Set, Optional
from llama_index.agent.react.output_parser import ReActOutputParser
from llama_index.llms import ChatResponse
from llama_index.agent.types import Task
def parse_react_output_fn(
task: Task, state: Dict[str, Any], chat_response: ChatResponse
):
"""Parse ReAct output into a reasoning step."""
output_parser = ReActOutputParser()
reasoning_step = output_parser.parse(chat_response.message.content)
return {"done": reasoning_step.is_done, "reasoning_step": reasoning_step}
parse_react_output = AgentFnComponent(fn=parse_react_output_fn)
def run_tool_fn(
task: Task, state: Dict[str, Any], reasoning_step: ActionReasoningStep
):
"""Run tool and process tool output."""
tool_runner_component = ToolRunnerComponent(
[sql_tool], callback_manager=task.callback_manager
)
tool_output = tool_runner_component.run_component(
tool_name=reasoning_step.action,
tool_input=reasoning_step.action_input,
)
observation_step = ObservationReasoningStep(observation=str(tool_output))
state["current_reasoning"].append(observation_step)
# TODO: get output
return {"response_str": observation_step.get_content(), "is_done": False}
run_tool = AgentFnComponent(fn=run_tool_fn)
def process_response_fn(
task: Task, state: Dict[str, Any], response_step: ResponseReasoningStep
):
"""Process response."""
state["current_reasoning"].append(response_step)
response_str = response_step.response
# Now that we're done with this step, put into memory
state["memory"].put(ChatMessage(content=task.input, role=MessageRole.USER))
state["memory"].put(
ChatMessage(content=response_str, role=MessageRole.ASSISTANT)
)
return {"response_str": response_str, "is_done": True}
process_response = AgentFnComponent(fn=process_response_fn)
def process_agent_response_fn(
task: Task, state: Dict[str, Any], response_dict: dict
):
"""Process agent response."""
return (
AgentChatResponse(response_dict["response_str"]),
response_dict["is_done"],
)
process_agent_response = AgentFnComponent(fn=process_agent_response_fn)
Stitch together Agent Query Pipeline#
We can now stitch together the top-level agent pipeline: agent_input -> react_prompt -> llm -> react_output.
The last component is the if-else component that calls sub-components.
from llama_index.query_pipeline import QueryPipeline as QP
from llama_index.llms import OpenAI
qp.add_modules(
{
"agent_input": agent_input_component,
"react_prompt": react_prompt_component,
"llm": OpenAI(model="gpt-4-1106-preview"),
"react_output_parser": parse_react_output,
"run_tool": run_tool,
"process_response": process_response,
"process_agent_response": process_agent_response,
}
)
# link input to react prompt to parsed out response (either tool action/input or observation)
qp.add_chain(["agent_input", "react_prompt", "llm", "react_output_parser"])
# add conditional link from react output to tool call (if not done)
qp.add_link(
"react_output_parser",
"run_tool",
condition_fn=lambda x: not x["done"],
input_fn=lambda x: x["reasoning_step"],
)
# add conditional link from react output to final response processing (if done)
qp.add_link(
"react_output_parser",
"process_response",
condition_fn=lambda x: x["done"],
input_fn=lambda x: x["reasoning_step"],
)
# whether response processing or tool output processing, add link to final agent response
qp.add_link("process_response", "process_agent_response")
qp.add_link("run_tool", "process_agent_response")
Visualize Query Pipeline#
from pyvis.network import Network
net = Network(notebook=True, cdn_resources="in_line", directed=True)
net.from_nx(qp.clean_dag)
net.show("agent_dag.html")
agent_dag.html
Setup Agent Worker around Text-to-SQL Query Pipeline#
This is our way to setup an agent around a text-to-SQL Query Pipeline
from llama_index.agent import QueryPipelineAgentWorker, AgentRunner
from llama_index.callbacks import CallbackManager
agent_worker = QueryPipelineAgentWorker(qp)
agent = AgentRunner(
agent_worker, callback_manager=CallbackManager([]), verbose=True
)
Run the Agent#
Let’s try the agent on some sample queries.
# start task
task = agent.create_task(
"What are some tracks from the artist AC/DC? Limit it to 3"
)
step_output = agent.run_step(task.task_id)
> Running step 1778ae52-0a31-4199-be65-af574fbf70f1. Step input: What are some tracks from the artist AC/DC? Limit it to 3
> Running module agent_input with input:
state: {'sources': [], 'memory': ChatMemoryBuffer(token_limit=3000, tokenizer_fn=functools.partial(<bound method Encoding.encode of <Encoding 'cl100k_base'>>, allowed_special='all'), chat_store=SimpleChatSto...
task: task_id='0c7b3203-ce9e-496f-9be3-c66425b8eb85' input='What are some tracks from the artist AC/DC? Limit it to 3' memory=ChatMemoryBuffer(token_limit=3000, tokenizer_fn=functools.partial(<bound method ...
> Running module react_prompt with input:
input: What are some tracks from the artist AC/DC? Limit it to 3
> Running module llm with input:
messages: [ChatMessage(role=<MessageRole.SYSTEM: 'system'>, content='\nYou are designed to help with a variety of tasks, from answering questions to providing summaries to other types of analyses.\n\n## Too...
> Running module react_output_parser with input:
chat_response: assistant: Thought: I need to use a tool to help me answer the question.
Action: sql_tool
Action Input: {"input": "What are some tracks from the artist AC/DC? Limit it to 3"}
> Running module run_tool with input:
reasoning_step: thought='I need to use a tool to help me answer the question.' action='sql_tool' action_input={'input': 'What are some tracks from the artist AC/DC? Limit it to 3'}
> Running module process_agent_response with input:
response_dict: {'response_str': 'Observation: {\'output\': ToolOutput(content=\'Some tracks from the artist AC/DC are "For Those About To Rock (We Salute You)", "Put The Finger On You", and "Let\\\'s Get It Up".\', ...
step_output = agent.run_step(task.task_id)
> Running step 07ac733e-d234-4751-bcad-002a655fb2e3. Step input: None
> Running module agent_input with input:
state: {'sources': [], 'memory': ChatMemoryBuffer(token_limit=3000, tokenizer_fn=functools.partial(<bound method Encoding.encode of <Encoding 'cl100k_base'>>, allowed_special='all'), chat_store=SimpleChatSto...
task: task_id='0c7b3203-ce9e-496f-9be3-c66425b8eb85' input='What are some tracks from the artist AC/DC? Limit it to 3' memory=ChatMemoryBuffer(token_limit=3000, tokenizer_fn=functools.partial(<bound method ...
> Running module react_prompt with input:
input: What are some tracks from the artist AC/DC? Limit it to 3
> Running module llm with input:
messages: [ChatMessage(role=<MessageRole.SYSTEM: 'system'>, content='\nYou are designed to help with a variety of tasks, from answering questions to providing summaries to other types of analyses.\n\n## Too...
> Running module react_output_parser with input:
chat_response: assistant: Thought: The user has repeated the request, but it seems they might not have noticed the previous response. I will reiterate the information provided by the tool.
Answer: Some tracks from ...
> Running module process_response with input:
response_step: thought='The user has repeated the request, but it seems they might not have noticed the previous response. I will reiterate the information provided by the tool.' response='Some tracks from the artis...
> Running module process_agent_response with input:
response_dict: {'response_str': 'Some tracks from the artist AC/DC are "For Those About To Rock (We Salute You)", "Put The Finger On You", and "Let\'s Get It Up".', 'is_done': True}
step_output.is_last
True
response = agent.finalize_response(task.task_id)
print(str(response))
Some tracks from the artist AC/DC are "For Those About To Rock (We Salute You)", "Put The Finger On You", and "Let's Get It Up".
# run this e2e
agent.reset()
response = agent.chat(
"What are some tracks from the artist AC/DC? Limit it to 3"
)
> Running step 0b0ed367-3246-442e-a8be-34ee4ef320c4. Step input: What are some tracks from the artist AC/DC? Limit it to 3
> Running module agent_input with input:
state: {'sources': [], 'memory': ChatMemoryBuffer(token_limit=3000, tokenizer_fn=functools.partial(<bound method Encoding.encode of <Encoding 'cl100k_base'>>, allowed_special='all'), chat_store=SimpleChatSto...
task: task_id='d699cfb9-9632-4ea0-ae43-0fe86f623a6e' input='What are some tracks from the artist AC/DC? Limit it to 3' memory=ChatMemoryBuffer(token_limit=3000, tokenizer_fn=functools.partial(<bound method ...
> Running module react_prompt with input:
input: What are some tracks from the artist AC/DC? Limit it to 3
> Running module llm with input:
messages: [ChatMessage(role=<MessageRole.SYSTEM: 'system'>, content='\nYou are designed to help with a variety of tasks, from answering questions to providing summaries to other types of analyses.\n\n## Too...
> Running module react_output_parser with input:
chat_response: assistant: Thought: I need to use a tool to help me answer the question.
Action: sql_tool
Action Input: {"input": "SELECT track_name FROM tracks WHERE artist_name = 'AC/DC' LIMIT 3"}
> Running module run_tool with input:
reasoning_step: thought='I need to use a tool to help me answer the question.' action='sql_tool' action_input={'input': "SELECT track_name FROM tracks WHERE artist_name = 'AC/DC' LIMIT 3"}
> Running module process_agent_response with input:
response_dict: {'response_str': 'Observation: {\'output\': ToolOutput(content=\'The top 3 tracks by AC/DC are "For Those About To Rock (We Salute You)", "Put The Finger On You", and "Let\\\'s Get It Up".\', tool_nam...
> Running step 2d670abc-4d8f-4cd6-8ad3-36152340ec8b. Step input: None
> Running module agent_input with input:
state: {'sources': [], 'memory': ChatMemoryBuffer(token_limit=3000, tokenizer_fn=functools.partial(<bound method Encoding.encode of <Encoding 'cl100k_base'>>, allowed_special='all'), chat_store=SimpleChatSto...
task: task_id='d699cfb9-9632-4ea0-ae43-0fe86f623a6e' input='What are some tracks from the artist AC/DC? Limit it to 3' memory=ChatMemoryBuffer(token_limit=3000, tokenizer_fn=functools.partial(<bound method ...
> Running module react_prompt with input:
input: What are some tracks from the artist AC/DC? Limit it to 3
> Running module llm with input:
messages: [ChatMessage(role=<MessageRole.SYSTEM: 'system'>, content='\nYou are designed to help with a variety of tasks, from answering questions to providing summaries to other types of analyses.\n\n## Too...
> Running module react_output_parser with input:
chat_response: assistant: Thought: The user has repeated the request for tracks from the artist AC/DC, limited to 3, despite having already received an answer. It's possible that the user did not see the previous re...
> Running module process_response with input:
response_step: thought="The user has repeated the request for tracks from the artist AC/DC, limited to 3, despite having already received an answer. It's possible that the user did not see the previous response or t...
> Running module process_agent_response with input:
response_dict: {'response_str': 'The top 3 tracks by AC/DC are "For Those About To Rock (We Salute You)", "Put The Finger On You", and "Let\'s Get It Up".', 'is_done': True}
print(str(response))
The top 3 tracks by AC/DC are "For Those About To Rock (We Salute You)", "Put The Finger On You", and "Let's Get It Up".
Setup Simple Retry Agent Pipeline for Text-to-SQL#
Instead of the full ReAct pipeline that does tool picking, let’s try a much simpler agent pipeline that only does text-to-SQL, with retry-logic.
We try a simple text-based “retry” prompt where given the user input and previous conversation history, can generate a modified query that outputs the right result.
Define Core Modules#
agent input
retry prompt
output processor (including a validation prompt)
from llama_index.llms import OpenAI
# llm = OpenAI(model="gpt-3.5-turbo")
llm = OpenAI(model="gpt-4-1106-preview")
from llama_index.agent import Task, AgentChatResponse
from typing import Dict, Any
from llama_index.query_pipeline import AgentInputComponent, AgentFnComponent
def agent_input_fn(task: Task, state: Dict[str, Any]) -> Dict:
"""Agent input function."""
# initialize current_reasoning
if "convo_history" not in state:
state["convo_history"] = []
state["count"] = 0
state["convo_history"].append(f"User: {task.input}")
convo_history_str = "\n".join(state["convo_history"]) or "None"
return {"input": task.input, "convo_history": convo_history_str}
agent_input_component = AgentInputComponent(fn=agent_input_fn)
from llama_index.prompts import PromptTemplate
retry_prompt_str = """\
You are trying to generate a proper natural language query given a user input.
This query will then be interpreted by a downstream text-to-SQL agent which
will convert the query to a SQL statement. If the agent triggers an error,
then that will be reflected in the current conversation history (see below).
If the conversation history is None, use the user input. If its not None,
generate a new SQL query that avoids the problems of the previous SQL query.
Input: {input}
Convo history (failed attempts):
{convo_history}
New input: """
retry_prompt = PromptTemplate(retry_prompt_str)
from llama_index.response import Response
from typing import Tuple
validate_prompt_str = """\
Given the user query, validate whether the inferred SQL query and response from executing the query is correct and answers the query.
Answer with YES or NO.
Query: {input}
Inferred SQL query: {sql_query}
SQL Response: {sql_response}
Result: """
validate_prompt = PromptTemplate(validate_prompt_str)
MAX_ITER = 3
def agent_output_fn(
task: Task, state: Dict[str, Any], output: Response
) -> Tuple[AgentChatResponse, bool]:
"""Agent output component."""
print(f"> Inferred SQL Query: {output.metadata['sql_query']}")
print(f"> SQL Response: {str(output)}")
state["convo_history"].append(
f"Assistant (inferred SQL query): {output.metadata['sql_query']}"
)
state["convo_history"].append(f"Assistant (response): {str(output)}")
# run a mini chain to get response
validate_prompt_partial = validate_prompt.as_query_component(
partial={
"sql_query": output.metadata["sql_query"],
"sql_response": str(output),
}
)
qp = QP(chain=[validate_prompt_partial, llm])
validate_output = qp.run(input=task.input)
state["count"] += 1
is_done = False
if state["count"] >= MAX_ITER:
is_done = True
if "YES" in validate_output.message.content:
is_done = True
return AgentChatResponse(response=str(output)), is_done
agent_output_component = AgentFnComponent(fn=agent_output_fn)
from llama_index.query_pipeline import (
QueryPipeline as QP,
Link,
InputComponent,
)
qp = QP(
modules={
"input": agent_input_component,
"retry_prompt": retry_prompt,
"llm": llm,
"sql_query_engine": sql_query_engine,
"output_component": agent_output_component,
},
verbose=True,
)
qp.add_link("input", "retry_prompt", src_key="input", dest_key="input")
qp.add_link(
"input", "retry_prompt", src_key="convo_history", dest_key="convo_history"
)
qp.add_chain(["retry_prompt", "llm", "sql_query_engine", "output_component"])
Visualize Query Pipeline#
from pyvis.network import Network
net = Network(notebook=True, cdn_resources="in_line", directed=True)
net.from_nx(qp.dag)
net.show("agent_dag.html")
agent_dag.html
Define Agent Worker#
from llama_index.agent import QueryPipelineAgentWorker, AgentRunner
from llama_index.callbacks import CallbackManager
# callback manager is passed from query pipeline to agent worker/agent
agent_worker = QueryPipelineAgentWorker(qp)
agent = AgentRunner(
agent_worker, callback_manager=CallbackManager(), verbose=False
)
response = agent.chat(
"How many albums did the artist who wrote 'Restless and Wild' release? (answer should be non-zero)?"
)
print(str(response))
> Running module input with input:
state: {'sources': [], 'memory': ChatMemoryBuffer(token_limit=3000, tokenizer_fn=functools.partial(<bound method Encoding.encode of <Encoding 'cl100k_base'>>, allowed_special='all'), chat_store=SimpleChatSto...
task: task_id='0952d9f0-458d-4bca-8531-56fd8f880d47' input="How many albums did the artist who wrote 'Restless and Wild' release? (answer should be non-zero)?" memory=ChatMemoryBuffer(token_limit=3000, toke...
> Running module retry_prompt with input:
input: How many albums did the artist who wrote 'Restless and Wild' release? (answer should be non-zero)?
convo_history: User: How many albums did the artist who wrote 'Restless and Wild' release? (answer should be non-zero)?
> Running module llm with input:
messages: You are trying to generate a proper natural language query given a user input.
This query will then be interpreted by a downstream text-to-SQL agent which
will convert the query to a SQL statement. I...
> Running module sql_query_engine with input:
input: assistant: Given the user's input and the requirement that the answer should be non-zero, a proper natural language query that could be interpreted by a text-to-SQL agent might be:
"Find the total nu...
> Running module output_component with input:
output: The total number of albums released by the artist credited with writing the song 'Restless and Wild' is 1.
> Inferred SQL Query: SELECT COUNT(DISTINCT albums.AlbumId) AS TotalAlbums
FROM albums
JOIN tracks ON albums.AlbumId = tracks.AlbumId
WHERE tracks.Name = 'Restless and Wild'
AND albums.ArtistId = (
SELECT ArtistId
FROM tracks
JOIN albums ON tracks.AlbumId = albums.AlbumId
WHERE tracks.Name = 'Restless and Wild'
LIMIT 1
)
HAVING TotalAlbums > 0;
> SQL Response: The total number of albums released by the artist credited with writing the song 'Restless and Wild' is 1.
> Running module input with input:
state: {'sources': [], 'memory': ChatMemoryBuffer(token_limit=3000, tokenizer_fn=functools.partial(<bound method Encoding.encode of <Encoding 'cl100k_base'>>, allowed_special='all'), chat_store=SimpleChatSto...
task: task_id='0952d9f0-458d-4bca-8531-56fd8f880d47' input="How many albums did the artist who wrote 'Restless and Wild' release? (answer should be non-zero)?" memory=ChatMemoryBuffer(token_limit=3000, toke...
> Running module retry_prompt with input:
input: How many albums did the artist who wrote 'Restless and Wild' release? (answer should be non-zero)?
convo_history: User: How many albums did the artist who wrote 'Restless and Wild' release? (answer should be non-zero)?
Assistant (inferred SQL query): SELECT COUNT(DISTINCT albums.AlbumId) AS TotalAlbums
FROM album...
> Running module llm with input:
messages: You are trying to generate a proper natural language query given a user input.
This query will then be interpreted by a downstream text-to-SQL agent which
will convert the query to a SQL statement. I...
> Running module sql_query_engine with input:
input: assistant: Given the conversation history, it seems that the previous query was successful in fetching a non-zero count of albums released by the artist who wrote 'Restless and Wild'. However, the use...
> Running module output_component with input:
output: Based on the revised query, it seems that there are no distinct albums released by the composer who wrote the song 'Restless and Wild'.
> Inferred SQL Query: SELECT COUNT(DISTINCT albums.AlbumId) AS AlbumCount
FROM albums
JOIN tracks ON albums.AlbumId = tracks.AlbumId
WHERE tracks.Composer = 'Restless and Wild'
HAVING AlbumCount > 0;
> SQL Response: Based on the revised query, it seems that there are no distinct albums released by the composer who wrote the song 'Restless and Wild'.
KeyboardInterrupt