Conversational agent

Tutorial 5

Jan Kirenz

Conversational agent

Let’s build a conversational agent

Setup

import os
import openai

from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv()) 
openai.api_key = os.environ['OPENAI_API_KEY']
import datetime
import requests
import wikipedia
from pydantic import BaseModel, Field

from langchain.tools import tool
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.tools.render import format_tool_to_openai_function
from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser
from langchain.prompts import MessagesPlaceholder
from langchain.agents.format_scratchpad import format_to_openai_functions
from langchain.schema.agent import AgentFinish
from langchain.schema.runnable import RunnablePassthrough
from langchain.agents import AgentExecutor
from langchain.memory import ConversationBufferMemory

import panel as pn 
pn.extension()
import panel as pn
import param

Prepare

Define weather tool

# Define the input schema
class OpenMeteoInput(BaseModel):
    latitude: float = Field(..., description="Latitude of the location to fetch weather data for")
    longitude: float = Field(..., description="Longitude of the location to fetch weather data for")

@tool(args_schema=OpenMeteoInput)
def get_current_temperature(latitude: float, longitude: float) -> dict:
    """Fetch current temperature for given coordinates."""
    
    BASE_URL = "https://api.open-meteo.com/v1/forecast"
    
    # Parameters for the request
    params = {
        'latitude': latitude,
        'longitude': longitude,
        'hourly': 'temperature_2m',
        'forecast_days': 1,
    }

    # Make the request
    response = requests.get(BASE_URL, params=params)
    
    if response.status_code == 200:
        results = response.json()
    else:
        raise Exception(f"API Request failed with status code: {response.status_code}")

    current_utc_time = datetime.datetime.utcnow()
    time_list = [datetime.datetime.fromisoformat(time_str.replace('Z', '+00:00')) for time_str in results['hourly']['time']]
    temperature_list = results['hourly']['temperature_2m']
    
    closest_time_index = min(range(len(time_list)), key=lambda i: abs(time_list[i] - current_utc_time))
    current_temperature = temperature_list[closest_time_index]
    
    return f'The current temperature is {current_temperature}°C'

Define Wikipedia tool

@tool
def search_wikipedia(query: str) -> str:
    """Run Wikipedia search and get page summaries."""
    page_titles = wikipedia.search(query)
    summaries = []
    for page_title in page_titles[: 3]:
        try:
            wiki_page =  wikipedia.page(title=page_title, auto_suggest=False)
            summaries.append(f"Page: {page_title}\nSummary: {wiki_page.summary}")
        except (
            self.wiki_client.exceptions.PageError,
            self.wiki_client.exceptions.DisambiguationError,
        ):
            pass
    if not summaries:
        return "No good Wikipedia Search Result was found"
    return "\n\n".join(summaries)

Save list of tools

tools = [get_current_temperature, search_wikipedia]

Set up chain

functions = [format_tool_to_openai_function(f) for f in tools]

model = ChatOpenAI(temperature=0).bind(functions=functions)

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are helpful but sassy assistant"),
    ("user", "{input}"),
])

chain = prompt | model | OpenAIFunctionsAgentOutputParser()

Invoke chain

result = chain.invoke({"input": "what is the weather in stuttgart?"})
result.tool
  • ‘get_current_temperature’
result.tool_input
  • {‘latitude’: 48.7758, ‘longitude’: 9.1829}

Pass back in history

Modify prompt

  • Use MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are helpful but sassy assistant"),
    ("user", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad")
])

Create chain

chain = prompt | model | OpenAIFunctionsAgentOutputParser()

Invoke chain

  • We use an empty list because we don’t have any input so far
result1 = chain.invoke({
    "input": "what is the weather is stuttgart?",
    "agent_scratchpad": []
})

Inspect result1

result1.tool
  • ‘get_current_temperature’
observation = get_current_temperature(result1.tool_input)
observation
  • ‘The current temperature is 10.1°C’
type(result1)
  • langchain.schema.agent.AgentActionMessageLog

Show log

result1.message_log
  • [AIMessage(content=’‘, additional_kwargs={’function_call’: {‘name’: ‘get_current_temperature’, ‘arguments’: ‘{“latitude”: 48.7758,“longitude”: 9.1829}’}})]

Format to OpenAI functions

format_to_openai_functions([(result1, observation), ])
  • [AIMessage(content=’‘, additional_kwargs={’function_call’: {‘name’: ‘get_current_temperature’, ‘arguments’: ‘{“latitude”: 48.7758,“longitude”: 9.1829}’}}), FunctionMessage(content=‘The current temperature is 10.1°C’, name=‘get_current_temperature’)]

Update chain

result2 = chain.invoke({
    "input": "what is the weather in stuttgart?", 
    "agent_scratchpad": format_to_openai_functions([(result1, observation)])
})
result2
  • AgentFinish(return_values={‘output’: ‘The current temperature in Stuttgart is 10.1°C.’}, log=‘The current temperature in Stuttgart is 10.1°C.’)

Final function

Function for agent

def run_agent(user_input):
    intermediate_steps = []
    while True:
        result = chain.invoke({
            "input": user_input, 
            "agent_scratchpad": format_to_openai_functions(intermediate_steps)
        })
        if isinstance(result, AgentFinish):
            return result
        tool = {
            "search_wikipedia": search_wikipedia, 
            "get_current_temperature": get_current_temperature,
        }[result.tool]
        observation = tool.run(result.tool_input)
        intermediate_steps.append((result, observation))

RunnablePassthrough

agent_chain = RunnablePassthrough.assign(
    agent_scratchpad= lambda x: format_to_openai_functions(x["intermediate_steps"])
) | chain
def run_agent(user_input):
    intermediate_steps = []
    while True:
        result = agent_chain.invoke({
            "input": user_input, 
            "intermediate_steps": intermediate_steps
        })
        if isinstance(result, AgentFinish):
            return result
        tool = {
            "search_wikipedia": search_wikipedia, 
            "get_current_temperature": get_current_temperature,
        }[result.tool]
        observation = tool.run(result.tool_input)
        intermediate_steps.append((result, observation))

Run agent with weather question

run_agent("what is the weather in stuttgart?")
  • AgentFinish(return_values={‘output’: ‘The current temperature in Stuttgart is 10.1°C.’}, log=‘The current temperature in Stuttgart is 10.1°C.’)

Run agent with general question

run_agent("what is langchain?")
  • AgentFinish(return_values={‘output’: ‘LangChain is a framework designed to simplify the creation of applications using large language models (LLMs). It is a language model integration framework that can be used for various tasks such as document analysis and summarization, chatbots, and code analysis. LangChain helps developers leverage the power of language models in their applications.’}, log=‘LangChain is a framework designed to simplify the creation of applications using large language models (LLMs). It is a language model integration framework that can be used for various tasks such as document analysis and summarization, chatbots, and code analysis. LangChain helps developers leverage the power of language models in their applications.’)

Just say hi

run_agent("hi!")
  • AgentFinish(return_values={‘output’: ‘Hello! How can I assist you today?’}, log=‘Hello! How can I assist you today?’)

Agent Executor

Define Agent Executor

agent_executor = AgentExecutor(agent=agent_chain, tools=tools, verbose=True)

Invoke executor

agent_executor.invoke({"input": "what is langchain?"})

Entering new AgentExecutor chain…

Invoking: search_wikipedia with {'query': 'langchain'}

Page: LangChain Summary: LangChain is a framework designed to simplify the creation of applications using large language models (LLMs). As a language model integration framework, LangChain’s use-cases largely overlap with those of language models in general, including document analysis and summarization, chatbots, and code analysis.

Page: Prompt engineering Summary: Prompt engineering is the process of structuring text that can be interpreted and understood by a generative AI model. A prompt is natural language text describing the task that an AI should perform.A prompt for a text-to-text model can be a query such as “what is Fermat’s little theorem?”, a command such as “write a poem about leaves falling”, a short statement of feedback (for example, “too verbose”, “too formal”, “rephrase again”, “omit this word”) or a longer statement including context, instructions, and input data. Prompt engineering may involve phrasing a query, specifying a style, providing relevant context or assigning a role to the AI such as “Act as a native French speaker”. A prompt may include a few examples for a model to learn from, such as “maison -> house, chat -> cat, chien ->”, an approach called few-shot learning.When communicating with a text-to-image or a text-to-audio model, a typical prompt is a description of a desired output such as “a high-quality photo of an astronaut riding a horse” or “Lo-fi slow BPM electro chill with organic samples”. Prompting a text-to-image model may involve adding, removing, emphasizing and re-ordering words to achieve a desired subject, style, layout, lighting, and aesthetic.

Page: Sentence embedding Summary: In natural language processing, a sentence embedding refers to a numeric representation of a sentence in the form of a vector of real numbers which encodes meaningful semantic information.State of the art embeddings are based on the learned hidden layer representation of dedicated sentence transformer models. BERT pioneered an approach involving the use of a dedicated [CLS] token prepended to the beginning of each sentence inputted into the model; the final hidden state vector of this token encodes information about the sentence and can be fine-tuned for use in sentence classification tasks. In practice however, BERT’s sentence embedding with the [CLS] token achieves poor performance, often worse than simply averaging non-contextual word embeddings. SBERT later achieved superior sentence embedding performance by fine tuning BERT’s [CLS] token embeddings through the usage of a siamese neural network architecture on the SNLI dataset. Other approaches are loosely based on the idea of distributional semantics applied to sentences. Skip-Thought trains an encoder-decoder structure for the task of neighboring sentences predictions. Though this has been shown to achieve worse performance than approaches such as InferSent or SBERT. An alternative direction is to aggregate word embeddings, such as those returned by Word2vec, into sentence embeddings. The most straightforward approach is to simply compute the average of word vectors, known as continuous bag-of-words (CBOW). However, more elaborate solutions based on word vector quantization have also been proposed. One such approach is the vector of locally aggregated word embeddings (VLAWE), which demonstrated performance improvements in downstream text classification tasks. LangChain is a framework designed to simplify the creation of applications using large language models (LLMs). It is a language model integration framework that can be used for various tasks such as document analysis and summarization, chatbots, and code analysis. LangChain helps developers leverage the power of language models in their applications.

Finished chain.

{‘input’: ‘what is langchain?’, ‘output’: ‘LangChain is a framework designed to simplify the creation of applications using large language models (LLMs). It is a language model integration framework that can be used for various tasks such as document analysis and summarization, chatbots, and code analysis. LangChain helps developers leverage the power of language models in their applications.’}

Conversation example

agent_executor.invoke({"input": "my name is bob"})

Entering new AgentExecutor chain… Hello Bob! How can I assist you today?

Finished chain.

{‘input’: ‘my name is bob’, ‘output’: ‘Hello Bob! How can I assist you today?’}

Ask a question

agent_executor.invoke({"input": "what is my name"})

Add previous messages in prompt

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are helpful but sassy assistant"),
    MessagesPlaceholder(variable_name="chat_history"),
    ("user", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad")
])

Create agent chain

agent_chain = RunnablePassthrough.assign(
    agent_scratchpad= lambda x: format_to_openai_functions(x["intermediate_steps"])
) | prompt | model | OpenAIFunctionsAgentOutputParser()

Create memory object

memory = ConversationBufferMemory(return_messages=True,memory_key="chat_history")

Agent executor

agent_executor = AgentExecutor(agent=agent_chain, tools=tools, verbose=True, memory=memory)

Provide input with name

agent_executor.invoke({"input": "my name is bob"})

Entering new AgentExecutor chain… Hello Bob! How can I assist you today?

Finished chain.

{‘input’: ‘my name is bob’, ‘chat_history’: [HumanMessage(content=‘my name is bob’), AIMessage(content=‘Hello Bob! How can I assist you today?’)], ‘output’: ‘Hello Bob! How can I assist you today?’}

Ask about name

agent_executor.invoke({"input": "whats my name"})

Entering new AgentExecutor chain… Your name is Bob.

Finished chain.

{‘input’: ‘whats my name’, ‘chat_history’: [HumanMessage(content=‘my name is bob’), AIMessage(content=‘Hello Bob! How can I assist you today?’), HumanMessage(content=‘whats my name’), AIMessage(content=‘Your name is Bob.’)], ‘output’: ‘Your name is Bob.’}

Ask about the weather

agent_executor.invoke({"input": "whats the weather in stuttgart?"})

Entering new AgentExecutor chain…

Invoking: get_current_temperature with {'latitude': 48.7758, 'longitude': 9.1829}

The current temperature is 9.5°CThe current temperature in Stuttgart is 9.5°C.

Finished chain.

{‘input’: ‘whats the weather in stuttgart?’, ‘chat_history’: [HumanMessage(content=‘my name is bob’), AIMessage(content=‘Hello Bob! How can I assist you today?’), HumanMessage(content=‘whats my name’), AIMessage(content=‘Your name is Bob.’), HumanMessage(content=‘whats the weather in stuttgart?’), AIMessage(content=‘The current temperature in Stuttgart is 9.5°C.’)], ‘output’: ‘The current temperature in Stuttgart is 9.5°C.’}

Create a chatbot

Define a custom function

@tool
def create_your_own(query: str) -> str:
    """This function can do whatever you would like once you fill it in """
    print(type(query))
    return query[::-1]

Create tool list

tools = [get_current_temperature, search_wikipedia, create_your_own]

Define Chatbot function

class cbfs(param.Parameterized):
    
    def __init__(self, tools, **params):
        super(cbfs, self).__init__( **params)
        self.panels = []
        self.functions = [format_tool_to_openai_function(f) for f in tools]
        self.model = ChatOpenAI(temperature=0).bind(functions=self.functions)
        self.memory = ConversationBufferMemory(return_messages=True,memory_key="chat_history")
        self.prompt = ChatPromptTemplate.from_messages([
            ("system", "You are helpful but sassy assistant"),
            MessagesPlaceholder(variable_name="chat_history"),
            ("user", "{input}"),
            MessagesPlaceholder(variable_name="agent_scratchpad")
        ])
        self.chain = RunnablePassthrough.assign(
            agent_scratchpad = lambda x: format_to_openai_functions(x["intermediate_steps"])
        ) | self.prompt | self.model | OpenAIFunctionsAgentOutputParser()
        self.qa = AgentExecutor(agent=self.chain, tools=tools, verbose=False, memory=self.memory)
    
    def convchain(self, query):
        if not query:
            return
        inp.value = ''
        result = self.qa.invoke({"input": query})
        self.answer = result['output'] 
        self.panels.extend([
            pn.Row('User:', pn.pane.Markdown(query, width=450)),
            pn.Row('ChatBot:', pn.pane.Markdown(self.answer, width=450, styles={'background-color': '#F6F6F6'}))
        ])
        return pn.WidgetBox(*self.panels, scroll=True)


    def clr_history(self,count=0):
        self.chat_history = []
        return 

Panel UI

cb = cbfs(tools)

inp = pn.widgets.TextInput( placeholder='Enter text here…')

conversation = pn.bind(cb.convchain, inp) 

tab1 = pn.Column(
    pn.Row(inp),
    pn.layout.Divider(),
    pn.panel(conversation,  loading_indicator=True, height=400),
    pn.layout.Divider(),
)

dashboard = pn.Column(
    pn.Row(pn.pane.Markdown('# QnA_Bot')),
    pn.Tabs(('Conversation', tab1))
)
dashboard

Acknowledgments

What’s next?

Congratulations! You have completed this tutorial 👍

Next, you may want to go back to the lab’s website