langchain-ai / langchain

🦜🔗 Build context-aware reasoning applications
https://python.langchain.com
MIT License
93.08k stars 14.97k forks source link

Langchain OPENAI_MULTI_FUNCTIONS Agent doesn't retrieve the data from the second Input (Chain). Can OPENAI_MULTI_FUNCTIONS Agent realize two actions, one after the other ? #12882

Closed JoffreyLemery closed 7 months ago

JoffreyLemery commented 11 months ago

System Info

Name: langchain Version: 0.0.308 Macos 13.2.1

Who can help?

@hwchase17 @agola11

Information

Related Components

Reproduction

Hi everybody !

I have setup a OPENAI_MULTI_FUNCTIONS Agent which seems to do great with a few token, but even if all the information are in the prompt, it is not able to use them for the tool input.

What is the issue :

I have two Custom tools :

I presented below, the agent use the first tools, write the ticket, inject it in the new prompt, but after that, the agent decide to use the second tool but miss some information

import os
import json
import langchain
import requests
from langchain.chat_models import ChatOpenAI
from requests.auth import HTTPBasicAuth
from typing import Optional, Type
from langchain.llms import OpenAI
from pydantic import BaseModel, Field
from langchain.agents import (AgentType, 
                              initialize_agent, )
from langchain.cache import InMemoryCache
from langchain.pydantic_v1 import BaseModel, Field
from langchain.chains import LLMChain, SequentialChain
from langchain.output_parsers import (ResponseSchema, 
                                      StructuredOutputParser)

from langchain.tools import BaseTool
from langchain.prompts import (ChatPromptTemplate,
                               MessagesPlaceholder,
                               FewShotChatMessagePromptTemplate)

from langchain.memory import ConversationBufferMemory
from langchain.schema import (SystemMessage,
                             )

from langchain.callbacks.manager import (
    AsyncCallbackManagerForToolRun,
    CallbackManagerForToolRun,
)

debug_mode = True
langchain.debug = True

email = 'XXXXXXXXXX'
url = "https://XXXXXXXXXX.atlassian.net/rest/api/3/issue"

langchain.llm_cache = InMemoryCache()

os.environ["JIRA_API_TOKEN"]= 'XXXXXXXXXX'
api_token_jira = os.environ["JIRA_API_TOKEN"]
os.environ["JIRA_USERNAME"]= 'XXXXXXXXXX'
os.environ["JIRA_INSTANCE_URL"] = "XXXXXXXXXX"
site_jira = os.environ["JIRA_INSTANCE_URL"]
os.environ['OPENAI_API_KEY']='XXXXXXXXXX'
openai_api_key = os.getenv('OPENAI_API_KEY')
api_key = os.getenv('OPENAI_API_KEY')

model_name = 'gpt-3.5-turbo'
temperature = 0.0

model_llm = OpenAI(model_name=model_name, 
                   temperature=temperature, 
                   max_tokens=3500)

turbo_llm = ChatOpenAI(
    temperature=temperature,
    model_name=model_name,
    max_tokens=3100,)

class SummaryTicket(BaseModel):
    """Input for writing Jira Ticket"""
    summary: str = Field(..., description="ticket summary")

class POST(BaseModel):
    """Input for POST a ticket"""
    ticket: str = Field(..., description="Jira ticket as a dictionnary")
    email: str = Field(..., description="email associated with the Jira Software account")
    url: str = Field(..., description="url to POST a Jira ticket at")
    token : str = Field(..., description="Identification JIRA API token") 

class CustomJiraTicketWriting(BaseTool):
    name = "Jira_Ticket_Write"
    description = ("""
        Useful to transform a summary into a real JSON Jira ticket.
        The input should be like :
        {{
            Action: Ticket_writing,
            Action Input: 
                "summary": <ticket summary>, 
        }}
        """)

    def _run(self, 
            summary: str,
            run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
             ) -> dict:

        response_schema = ResponseSchema(
            name="ticket",
            description="Jira ticket information",
            structure={
                "fields": {
                    "project": {
                        "key": str
                    },
                    "summary": str,
                    "issuetype": {
                        "name": str
                    },
                    "priority": {
                        "name": str
                    },
                    "description": {
                        "type": str,
                        "version": int,
                        "content": [
                        {
                            "type": str,
                            "content": [
                                {
                                    "type": str,
                                    "text": str
                                }
                            ]
                        },
                        {
                            "type": str,
                            "attrs": {
                                "level": int
                            },
                            "content": [
                                {
                                    "type": str,
                                    "text": str
                                }
                            ]
                        }
                        ]
                    }
                }
            }
        )

        response_schemas=[response_schema]
        output_parser = StructuredOutputParser.from_response_schemas(response_schemas)
        format_instructions = output_parser.get_format_instructions()     

        examples = [
          {
            "Jira_Ticket_Summary" : "Creation of the MySQL database",
            "output": """{"fields":{"project":{"key":"AJ"},"summary":"Create a Jira ticket to integrate my MySQL database into our current assets","issuetype":{"name":"Story"},"priority":{"name":"High"},"description":{"type":"doc","version":1,"content":[{"type":"paragraph","content":[{"type":"text","text":"As a developer, I want to integrate my MySQL database with our current assets to improve data management."}]},{"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"Acceptance Criteria:"}]},{"type":"paragraph","content":[{"type":"text","text":"- The MySQL database is successfully integrated with the application."}]},{"type":"paragraph","content":[{"type":"text","text":"- Data can be efficiently stored and retrieved from the integrated MySQL database."}]},{"type":"paragraph","content":[{"type":"text","text":"- The integration process follows best practices and security standards."}]},{"type":"paragraph","content":[{"type":"text","text":"- The integration is documented for future reference."}]},{"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"Subtasks:"}]},{"type":"paragraph","content":[{"type":"text","text":"- Analyze the structure of the MySQL database."}]},{"type":"paragraph","content":[{"type":"text","text":"- Create integration scripts for data migration."}]},{"type":"paragraph","content":[{"type":"text","text":"- Implement data synchronization with the application."}]},{"type":"paragraph","content":[{"type":"text","text":"- Perform testing and quality assurance of the integration."}]},{"type":"paragraph","content":[{"type":"text","text":"- Document the integration process and configurations."}]}]}}"""
          }]

        example_prompt = ChatPromptTemplate.from_messages(
            [
          ("human", "{Jira_Ticket_Summary}"),
            ("ai", "{output}")]
        )

        few_shot_prompt = FewShotChatMessagePromptTemplate(
            example_prompt=example_prompt,
            examples=examples,
        )  

        prompt_string = """
        Jira_Ticket_Summary: {Jira_Ticket_Summary}

        {format_instructions}
        """

        prompt = ChatPromptTemplate.from_messages(
            [
                ("system", "You are a Jira assistant specialized in creating technical tickets. You always develop the tickets with precise examples of sub-tasks and acceptance criteria. Remember to use double quotes for keys."),
                few_shot_prompt,
                ("human", prompt_string),
            ]
        )

        final_prompt = ChatPromptTemplate(
            messages=[
               prompt  
            ],
            input_variables=['Jira_Ticket_Summary'],
            partial_variables={"format_instructions": format_instructions},
            output_parser=output_parser
        )

        chain = LLMChain(llm=turbo_llm, 
                         prompt=final_prompt,
                         output_parser=output_parser,
                         output_key="ticket")

        sequential_chain = SequentialChain(chains=[chain],
                                           input_variables=['Jira_Ticket_Summary'],
                                           output_variables=['ticket'],
                                           verbose=True)

        input_data={'Jira_Ticket_Summary' : f'{summary}'}

        result = sequential_chain(input_data)

        print("\n\n\n result_writing : ",result)
        print("\n\n\n result_writing_type : ",type(result),'\n\n\n')
        return json.dumps(result['ticket'])

    def _arun(self, 
            summary:str,
            run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
             ) -> str:
        raise NotImplementedError("This tool does not support async")

    args_schema: Optional[Type[BaseModel]] = SummaryTicket

class ≠(BaseTool):

    name = "Jira_Ticket_Post"
    description = ("""\
                Useful to POST a ticket in Jira Software after you wrote it.
                The input should be like :
        {{
            Action: Jira_Post,
            Action Input: 
                "ticket": <JSON of the ticket>,
                "email": <email associated with the Jira Software account>,
                "url": <url to POST the ticket at>,
                "token": <identification JIRA API token>
        }}
        """)

    def _run(
        self,
        ticket: str, 
        email: str,
        url: str,
        token: str,
        run_manager: Optional[AsyncCallbackManagerForToolRun] = None
    ) -> str:

        # Retrieve the values using lowercase keys
        ticket = json.loads(ticket)
        body = ticket['Jira_ticket']

        auth = HTTPBasicAuth(email, api_token_jira)

        headers = {
          "Accept": "application/json",
          "Content-Type": "application/json"
        }

        response = requests.request(
           "POST",
           url,
           data=json.dumps(body),
           headers=headers,
           auth=auth
        )
        print("\n\nPOST : ", json.dumps(json.loads(response.text), sort_keys=True, indent=4, separators=(",", ": ")), "\n\n")

        return json.dumps(json.loads(response.text), sort_keys=True, indent=4, separators=(",", ": "))

    async def _arun(
        self,
        ticket: str, 
        email: str,
        url: str,
        token: str,
        run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
    ) -> str:
        """Use the tool asynchronously."""
        raise NotImplementedError("custom_search does not support async")

    args_schema: Optional[Type[BaseModel]] = POST

PREFIX = """You are a Jira Assistant.
You're designed to assist the user with a wide range of tasks related to Jira object management. 
It goes from understand the user's need for its tickets, writing technical and detailed Jira tickets with descriptions, subtasks and acceptance criteria’s to realize API call (POST, PUT or GET) with Jira objects.
Create a ticket means Write it then POST it.
Before to act, retrieve the inputs you need in the prompt.
"""

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

agent_kwargs = {
    "system_message": SystemMessage(content=f"{PREFIX}"),
    "input_variables": ["chat_history"]
}

tools = [CustomJiraTicketPOST(),CustomJiraTicketWriting()]

conversational_agent = initialize_agent(
    agent=AgentType.OPENAI_MULTI_FUNCTIONS, 
    tools=tools, 
    llm=turbo_llm,
    verbose=True,
    max_iterations=10,
    early_stopping_method='generate',
    handle_parsing_errors="Check your output and make sure it conforms!",
    memory=memory,
    agent_kwargs=agent_kwargs
)

payload = {\
    "summary":"Create a ticket to connect our backend to a new MySQL database. The project key is AJ",\
    "email":f"{email}",\
    "url":f"{url}",\
    "token":f"{api_token_jira}",
}

answer = conversational_agent.run(json.dumps(payload))

Expected behavior

Expected behaviour :

Actuel behaviour :

All the data are inside the input but the agent has trouble to use them.

File ~/miniconda3/envs/torch/lib/python3.10/site-packages/langchain/chains/base.py:501, in Chain.run(self, callbacks, tags, metadata, *args, **kwargs)
    499     if len(args) != 1:
    500         raise ValueError("`run` supports only one positional argument.")
--> 501     return self(args[0], callbacks=callbacks, tags=tags, metadata=metadata)[
    502         _output_key
    503     ]
    505 if kwargs and not args:
    506     return self(kwargs, callbacks=callbacks, tags=tags, metadata=metadata)[
...
  field required (type=value_error.missing)
url
  field required (type=value_error.missing)
token
  field required (type=value_error.missing)

And about the prompt

[llm/start] [1:chain:AgentExecutor > 4:llm:ChatOpenAI] Entering LLM run with input:
{
  "prompts": [
    "System: You are a Jira Assistant.\nYou're designed to assist the user with a wide range of tasks related to Jira object management. \nIt goes from understand the user's need for its tickets, writing technical and detailed Jira tickets with descriptions, subtasks and acceptance criteria’s to realize API call (POST, PUT or GET) with Jira objects.\nCreate a ticket means Write it then POST it.\nBefore to act, retrieve the inputs you need in the prompt.\n\nHuman: {\"summary\": \"Create a ticket to connect our backend to a new MySQL database. The project key is AJ\", \"email\": \"XXXXXXX@gmail.com\", \"url\": \"https://XXXXXXXX.atlassian.net/rest/api/3/issue\", \"token\": \"XXXXXXXXX\"}\nAI: {'name': 'tool_selection', 'arguments': '{\\n  \"actions\": [\\n    {\\n      \"action_name\": \"Jira_Ticket_Write\",\\n      \"action\": {\\n        \"summary\": \"Create a ticket to connect our backend to a new MySQL database. The project key is AJ\"\\n      }\\n    }\\n  ]\\n}'}\nFunction: {\"ticket\": {\"fields\": {\"project\": {\"key\": \"AJ\"}, \"summary\": \"Create a Jira ticket to integrate my MySQL database into our current assets\", \"issuetype\": {\"name\": \"Story\"}, \"priority\": {\"name\": \"High\"}, \"description\": {\"type\": \"doc\", \"version\": 1, \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"text\", \"text\": \"As a developer, I want to integrate my MySQL database with our current assets to improve data management.\"}]}, {\"type\": \"heading\", \"attrs\": {\"level\": 2}, \"content\": [{\"type\": \"text\", \"text\": \"Acceptance Criteria:\"}]}, {\"type\": \"paragraph\", \"content\": [{\"type\": \"text\", \"text\": \"- The MySQL database is successfully integrated with the application.\"}]}, {\"type\": \"paragraph\", \"content\": [{\"type\": \"text\", \"text\": \"- Data can be efficiently stored and retrieved from the integrated MySQL database.\"}]}, {\"type\": \"paragraph\", \"content\": [{\"type\": \"text\", \"text\": \"- The integration process follows best practices and security standards.\"}]}, {\"type\": \"paragraph\", \"content\": [{\"type\": \"text\", \"text\": \"- The integration is documented for future reference.\"}]}, {\"type\": \"heading\", \"attrs\": {\"level\": 2}, \"content\": [{\"type\": \"text\", \"text\": \"Subtasks:\"}]}, {\"type\": \"paragraph\", \"content\": [{\"type\": \"text\", \"text\": \"- Analyze the structure of the MySQL database.\"}]}, {\"type\": \"paragraph\", \"content\": [{\"type\": \"text\", \"text\": \"- Create integration scripts for data migration.\"}]}, {\"type\": \"paragraph\", \"content\": [{\"type\": \"text\", \"text\": \"- Implement data synchronization with the application.\"}]}, {\"type\": \"paragraph\", \"content\": [{\"type\": \"text\", \"text\": \"- Perform testing and quality assurance of the integration.\"}]}, {\"type\": \"paragraph\", \"content\": [{\"type\": \"text\", \"text\": \"- Document the integration process and configurations.\"}]}]}}}}"
  ]
}
[llm/end] [1:chain:AgentExecutor > 4:llm:ChatOpenAI] [83.60s] Exiting LLM run with output:
{
  "generations": [
    [
      {
        "text": "",
        "generation_info": {
          "finish_reason": "function_call"
        },
        "message": {
          "lc": 1,
          "type": "constructor",
          "id": [
            "langchain",
            "schema",
            "messages",
            "AIMessage"
          ],
          "kwargs": {
            "content": "",
            "additional_kwargs": {
              "function_call": {
                "name": "tool_selection",
                "arguments": "{\n  \"actions\": [\n    {\n      \"action_name\": \"Jira_Ticket_Post\",\n      \"action\": {\n        \"ticket\": \"{\\\"fields\\\": {\\\"project\\\": {\\\"key\\\": \\\"AJ\\\"}, \\\"summary\\\": \\\"Create a Jira ticket to integrate my MySQL database into our current assets\\\", \\\"issuetype\\\": {\\\"name\\\": \\\"Story\\\"}, \\\"priority\\\": {\\\"name\\\": \\\"High\\\"}, \\\"description\\\": {\\\"type\\\": \\\"doc\\\", \\\"version\\\": 1, \\\"content\\\": [{\\\"type\\\": \\\"paragraph\\\", \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"As a developer, I want to integrate my MySQL database with our current assets to improve data management.\\\"}]}, {\\\"type\\\": \\\"heading\\\", \\\"attrs\\\": {\\\"level\\\": 2}, \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"Acceptance Criteria:\\\"}]}, {\\\"type\\\": \\\"paragraph\\\", \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"- The MySQL database is successfully integrated with the application.\\\"}]}, {\\\"type\\\": \\\"paragraph\\\", \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"- Data can be efficiently stored and retrieved from the integrated MySQL database.\\\"}]}, {\\\"type\\\": \\\"paragraph\\\", \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"- The integration process follows best practices and security standards.\\\"}]}, {\\\"type\\\": \\\"paragraph\\\", \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"- The integration is documented for future reference.\\\"}]}, {\\\"type\\\": \\\"heading\\\", \\\"attrs\\\": {\\\"level\\\": 2}, \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"Subtasks:\\\"}]}, {\\\"type\\\": \\\"paragraph\\\", \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"- Analyze the structure of the MySQL database.\\\"}]}, {\\\"type\\\": \\\"paragraph\\\", \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"- Create integration scripts for data migration.\\\"}]}, {\\\"type\\\": \\\"paragraph\\\", \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"- Implement data synchronization with the application.\\\"}]}, {\\\"type\\\": \\\"paragraph\\\", \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"- Perform testing and quality assurance of the integration.\\\"}]}, {\\\"type\\\": \\\"paragraph\\\", \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"- Document the integration process and configurations.\\\"}]}]}}}\"\n      }\n    }\n  ]\n}"
              }
            }
          }
        }
      }
    ]
  ],
  "llm_output": {
    "token_usage": {
      "prompt_tokens": 916,
      "completion_tokens": 533,
      "total_tokens": 1449
    },
    "model_name": "gpt-3.5-turbo"
  },
  "run": null
}
[chain/error] [1:chain:AgentExecutor] [187.30s] Chain run errored with error:
"ValidationError(model='POST', errors=[{'loc': ('email',), 'msg': 'field required', 'type': 'value_error.missing'}, {'loc': ('url',), 'msg': 'field required', 'type': 'value_error.missing'}, {'loc': ('token',), 'msg': 'field required', 'type': 'value_error.missing'}])"

Thus, all the informations are in the Input, but i has difficulties to use them. How to handle this situation ?

Best regards

dosubot[bot] commented 11 months ago

🤖

Hello @JoffreyLemery,

I'm Dosu, an AI bot here to assist you with your LangChain issues, answer any questions you might have, and guide you on becoming a contributor. While we wait for a human maintainer, I'm available to help you out.

I'm currently looking into your issue and will provide you with a detailed response shortly.

dosubot[bot] commented 11 months ago

🤖

Based on the information you've provided and the context from the LangChain repository, it seems like the issue you're experiencing is related to how the CustomJiraTicketWriting and CustomJiraTicketPOST tools are handling the input parameters.

In LangChain, the tools use the get_from_dict_or_env function to fetch the 'email', 'url', and 'token' fields from either the input dictionary or the environment variables. If these fields are not found in either of these places, it will return None, which is likely why you're seeing the error message indicating that these fields are required.

To resolve this issue, you need to ensure that the input dictionary contains the keys 'jira_username', 'jira_api_token', and 'jira_instance_url' with the correct values. If you are relying on environment variables, make sure the 'JIRA_USERNAME', 'JIRA_API_TOKEN', and 'JIRA_INSTANCE_URL' variables are set.

Here is an example of how you can set these fields in the input dictionary:

jira_wrapper = JiraAPIWrapper(
    jira_username="your_username",
    jira_api_token="your_api_token",
    jira_instance_url="your_instance_url"
)

And here is how you can set the environment variables in Python:

import os

os.environ["JIRA_USERNAME"] = "your_username"
os.environ["JIRA_API_TOKEN"] = "your_api_token"
os.environ["JIRA_INSTANCE_URL"] = "your_instance_url"

Please replace "your_username", "your_api_token", and "your_instance_url" with your actual Jira username, API token, and instance URL.

This solution is based on the code found in the libs/langchain/langchain/utilities/jira.py file in the LangChain repository.

Additionally, there was a similar issue titled Tools with structured inputs are broken with new input parser logic when using REACT agents in the LangChain repository. The solution suggested there was to update the _run() method in the SendMessageTool class to match the action_input schema. This change was necessary due to a recent update in LangChain that supports stronger function typing on tool inputs.

I hope this helps! If you have any other questions or need further clarification, please let me know.

Sources


This response is meant to be useful and save you time. It is not meant to be a precise solution, but rather a starting point for your own research.

Help me be more useful! Please leave a 👍 if this is helpful and 👎 if it is irrelevant.

If you want to continue the conversation, start your reply with @dosu-bot.

JoffreyLemery commented 11 months ago

@dosu-bot.

Sorry but the idea is not to use actual Jira_wrapper. By having my own Jira tools, it allows me to have a better control on the quality of the ticket.

My question could be sum up by : Is Openai_multi_function able to realize two action one after the other based on an unique query ?

dosubot[bot] commented 11 months ago

🤖

Based on the information you've provided and the context from the LangChain repository, it seems that the OPENAI_MULTI_FUNCTIONS agent is designed to execute multiple actions sequentially based on a single query. This is done by parsing the AI message and extracting the function calls and their arguments in the _parse_ai_message function. Each function call and its arguments are used to create an instance of _FunctionsAgentAction, which represents a tool invocation. These instances are then added to a list, which is returned by the _parse_ai_message function.

However, the issue you're experiencing where the 'email', 'url', and 'token' fields are missing when using the CustomJiraTicketWriting and CustomJiraTicketPOST tools together in a chain might be due to the way the LLMChain object manages the sequence of function calls. Currently, the LLMChain object takes a list of functions and a prompt as input, and it uses the ChatOpenAI model to generate the output. It doesn't pass the output of one function as the input to the next function in the list.

To resolve this issue, you might need to modify the LLMChain object or create a new agent type that supports this behavior. For instance, you could alter the run method of the LLMChain object to pass the output of one function as the input to the next function in the list.

Here are the relevant sources for your reference:

Please note that this is just a potential solution and it might require some modifications to the LLMChain object. If you find this solution to be correct and it's not covered in the existing documentation, I encourage you to make a pull request to update the documentation. This would greatly benefit other users who might encounter the same issue.

Sources


This response is meant to be useful and save you time. It is not meant to be a precise solution, but rather a starting point for your own research.

Help me be more useful! Please leave a 👍 if this is helpful and 👎 if it is irrelevant.

If you want to continue the conversation, start your reply with @dosu-bot.

dosubot[bot] commented 8 months ago

Hi, @JoffreyLemery

I'm helping the LangChain team manage their backlog and am marking this issue as stale. From what I understand, the issue involves an OPENAI_MULTI_FUNCTIONS Agent struggling to retrieve data from the second input (Chain) and encountering difficulties in using the information provided in the prompt. There have been discussions around modifying the LLMChain object or creating a new agent type to support the desired behavior, along with references to relevant sources for further investigation.

Could you please confirm if this issue is still relevant to the latest version of the LangChain repository? If it is, please let the LangChain team know by commenting on the issue. Otherwise, feel free to close the issue yourself, or it will be automatically closed in 7 days. Thank you!