diff --git a/backend/director/agents/sales_assistant.py b/backend/director/agents/sales_assistant.py new file mode 100644 index 00000000..3fb442da --- /dev/null +++ b/backend/director/agents/sales_assistant.py @@ -0,0 +1,238 @@ +import logging +import json +import os +from director.agents.base import BaseAgent, AgentResponse, AgentStatus +from director.core.session import ( + Session, + MsgStatus, + TextContent, + ContextMessage, + RoleTypes + +) +from director.llm import get_default_llm +from director.llm.base import LLMResponseStatus + +from director.tools.videodb_tool import VideoDBTool +from director.tools.composio_tool import composio_tool, ToolsType + +logger = logging.getLogger(__name__) + +SALES_ASSISTANT_PARAMETER = { + "type": "object", + "properties": { + "video_id": { + "type": "string", + "description": "The ID of the sales call video", + }, + "collection_id": { + "type": "string", + "description": "The ID of the collection which the video belongs to" + }, + "prompt": { + "type": "string", + "description": "Additional information/query given by the user to make the appropriate action to take place" + } + }, + "required": ["video_id", "collection_id"] +} + + +SALES_ASSISTANT_PROMPT = """ + Under "transcript", transcript of a sales call video is present. + Under "user prompt", the user has given additional context or information given + Generate a sales summary from it and generate answers from the transcript for the following properties + + Following are the properties for which you need to find answers for and the Field type definition or the only possible answers to answer are given below + + Each field has a predefined format or set of possible values and can also have **default** property which needs to be used if a field is missing in transcript and user prompt: + + #### **Fields & Expected Answers** + Field: dealname + description: (The company or individuals name with whom we are making a deal with) + type: text (which says the name of person we are dealing with or the company) + + Field: dealstage + Possible Answers: appointmentscheduled, qualifiedtobuy, presentationscheduled, decisionmakerboughtin, contractsent, closedwon, closedlost + default: appointmentscheduled + + Field: budget + type: Multi line text (Around 150 words description) + description: + The multi line text answer for this field must consist of a detailed analysis of the budget situation of the company. + If numbers are mentioned, do include those details aswell. + If the deal is overpriced, underpriced or considered fair, should also be added if mentioned + + + Field: authority + type: Multi line text (Around 150 words description) + description: + The multi line text answer for this field must consist of a detailed analysis of the authority the client possesses for the conclusion of the deal. + If decision making powers are mentioned, do include those details. + If the client mention that they are the final signing authority, or any other details signifying their level of power in the deal. mention them + + + Field: need + type: Multi line text (Around 150 words description) + description: + The multi line text answer for this field must consist of a detailed analysis of how much the client wants the product. + Need can be found from the level or urgency, the depth or importance of problem they want to get solved or the amount of hurry they have + + + Field: timeline + type: Multi line text (Around 150 words description) + description: + The multi line text answer for this field must consist of a detailed analysis of how the timeline of the project looks like + Mention when they need the product, when they want to test the product etc. Important details about the timelines must be added here. + + Since this is a BANT analysis. Do not forget to generate a response for these properties. + ALL THE ABOVE FIELDS ARE MANDATORY TO BE GENERATED AN OUTPUT FOR. + AN ANALYSIS FOR budget, authority, need and timeline IS COMPULSORY + + Only give answers to the field. Do not give any additional texts such as introduction, conclusion etc. +""" + + +class SalesAssistantAgent(BaseAgent): + def __init__(self, session: Session, **kwargs): + self.agent_name = "sales_assistant" + self.description = "This agent will transcribe, study and analyse sales calls, automatically create deal summaries & update CRM software like Salesforce & Hubspot" + self.parameters = SALES_ASSISTANT_PARAMETER + self.llm = get_default_llm() + super().__init__(session=session, **kwargs) + + + def _generate_prompt(self, transcript:str, prompt:str): + final_prompt = SALES_ASSISTANT_PROMPT + + final_prompt += f""" + "transcript": + {transcript} + + "user prompt": + {prompt} + """ + + return final_prompt + + def run(self, + video_id:str, + collection_id:str, + prompt="", + *args, + **kwargs + ) -> AgentResponse: + """ + Create deal summaries and update the users CRM software + + :param str video_id: The sales call video ID + :param str collection_id: The videos collection ID + :param str prompt: Additional context or query given by the user for the task + :param args: Additional positional arguments. + :param kwargs: Additional keyword arguments. + :return: The response containing information about the sample processing operation. + :rtype: AgentResponse + """ + try: + HUBSPOT_ACCESS_TOKEN = os.getenv("HUBSPOT_ACCESS_TOKEN") + + if not HUBSPOT_ACCESS_TOKEN: + return AgentResponse( + status=AgentStatus.ERROR, + message="Hubspot token not present" + ) + + text_content = TextContent( + agent_name=self.agent_name, + status=MsgStatus.progress, + status_message="Making magic happen with VideoDB Director...", + ) + + self.output_message.content.append(text_content) + self.output_message.push_update() + + videodb_tool = VideoDBTool(collection_id=collection_id) + + self.output_message.actions.append("Extracting the transcript") + self.output_message.push_update() + + try: + transcript_text = videodb_tool.get_transcript(video_id) + except Exception: + logger.error("Transcript not found. Indexing spoken words..") + self.output_message.actions.append("Indexing spoken words..") + self.output_message.push_update() + videodb_tool.index_spoken_words(video_id) + transcript_text = videodb_tool.get_transcript(video_id) + + self.output_message.actions.append("Processing the transcript") + self.output_message.push_update() + + sales_assist_llm_prompt = self._generate_prompt(transcript=transcript_text, prompt=prompt) + sales_assist_llm_message = ContextMessage( + content=sales_assist_llm_prompt, role=RoleTypes.user + ) + llm_response = self.llm.chat_completions([sales_assist_llm_message.to_llm_msg()]) + + if not llm_response.status: + logger.error(f"LLM failed with {llm_response}") + text_content.status = MsgStatus.error + text_content.status_message = "Failed to generate the response." + self.output_message.publish() + return AgentResponse( + status=AgentStatus.ERROR, + message="Sales assistant failed due to LLM error.", + ) + + composio_prompt = f""" + Create a new deal in HubSpot with the following details: + + --- + {llm_response.content} + --- + + Use the HUBSPOT_CREATE_CRM_OBJECT_WITH_PROPERTIES action to accomplish this. + """ + + self.output_message.actions.append("Adding it into the Hubspot CRM") + self.output_message.push_update() + + composio_response = composio_tool( + task=composio_prompt, + auth_data={ + "name": "HUBSPOT", + "token": HUBSPOT_ACCESS_TOKEN + }, + tools_type=ToolsType.actions + ) + + llm_prompt = ( + f"User has asked to run a task: {composio_prompt} in Composio. \n" + "Dont mention the action name directly as is" + "Comment on the fact whether the composio call was sucessful or not" + "Make this message short and crisp" + f"{json.dumps(composio_response)}" + "If there are any errors or if it was not successful, do tell about that as well" + "If the response is successful, Show the details given to create a new deal in a markdown table format" + ) + final_message = ContextMessage(content=llm_prompt, role=RoleTypes.user) + llm_response = self.llm.chat_completions([final_message.to_llm_msg()]) + if llm_response.status == LLMResponseStatus.ERROR: + raise Exception(f"LLM Failed with error {llm_response.content}") + + text_content.text = llm_response.content + text_content.status = MsgStatus.success + text_content.status_message = "Here's the response from your sales assistant" + self.output_message.publish() + except Exception as e: + logger.exception(f"Error in {self.agent_name}") + text_content.status = MsgStatus.error + text_content.status_message = "Error in sales assistant" + self.output_message.publish() + error_message = f"Agent failed with error {e}" + return AgentResponse(status=AgentStatus.ERROR, message=error_message) + return AgentResponse( + status=AgentStatus.SUCCESS, + message=f"Agent {self.name} completed successfully.", + data={}, + ) diff --git a/backend/director/handler.py b/backend/director/handler.py index 3157cc4f..be90c531 100644 --- a/backend/director/handler.py +++ b/backend/director/handler.py @@ -25,7 +25,7 @@ from director.agents.transcription import TranscriptionAgent from director.agents.comparison import ComparisonAgent from director.agents.web_search_agent import WebSearchAgent - +from director.agents.sales_assistant import SalesAssistantAgent from director.core.session import Session, InputMessage, MsgStatus from director.core.reasoning import ReasoningEngine @@ -67,6 +67,7 @@ def __init__(self, db, **kwargs): ComposioAgent, ComparisonAgent, WebSearchAgent, + SalesAssistantAgent ] def add_videodb_state(self, session): diff --git a/backend/director/tools/composio_tool.py b/backend/director/tools/composio_tool.py index f35f8856..665df8d4 100644 --- a/backend/director/tools/composio_tool.py +++ b/backend/director/tools/composio_tool.py @@ -3,8 +3,14 @@ from director.llm.openai import OpenAIChatModel +from enum import Enum -def composio_tool(task: str): + +class ToolsType(str, Enum): + apps = "apps" + actions = "actions" + +def composio_tool(task: str, auth_data: dict = None, tools_type:ToolsType=ToolsType.apps): from composio_openai import ComposioToolSet from openai import OpenAI @@ -18,8 +24,25 @@ def composio_tool(task: str): openai_client = OpenAI(api_key=key, base_url=base_url) toolset = ComposioToolSet(api_key=os.getenv("COMPOSIO_API_KEY")) - tools = toolset.get_tools(apps=json.loads(os.getenv("COMPOSIO_APPS"))) - print(tools) + + if auth_data and "name" in auth_data and "token" in auth_data: + toolset.add_auth( + app=auth_data["name"].upper(), + parameters=[ + { + "name": "Authorization", + "in_": "header", + "value": f"Bearer {auth_data['token']}" + } + ] + ) + + if tools_type == ToolsType.apps: + tools = toolset.get_tools(apps=json.loads(os.getenv("COMPOSIO_APPS"))) + + elif tools_type == ToolsType.actions: + tools = toolset.get_tools(actions=json.loads(os.getenv("COMPOSIO_ACTIONS"))) + response = openai_client.chat.completions.create( model=OpenAIChatModel.GPT4o,