Skip to content

Feat/sales assistant #134

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 238 additions & 0 deletions backend/director/agents/sales_assistant.py
Original file line number Diff line number Diff line change
@@ -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

Comment on lines +104 to +117
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve prompt generation security.

The prompt generation method could be vulnerable to injection attacks.

     def _generate_prompt(self, transcript:str, prompt:str):
         final_prompt = SALES_ASSISTANT_PROMPT
+        # Sanitize inputs to prevent prompt injection
+        transcript = json.dumps(transcript)
+        prompt = json.dumps(prompt)
 
         final_prompt += f"""
             "transcript":
             {transcript}
 
             "user prompt":
             {prompt}
         """
 
         return final_prompt
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 _generate_prompt(self, transcript:str, prompt:str):
final_prompt = SALES_ASSISTANT_PROMPT
# Sanitize inputs to prevent prompt injection
transcript = json.dumps(transcript)
prompt = json.dumps(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"
)
Comment on lines +137 to +143
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve security of token handling.

The Hubspot access token should be handled more securely:

  1. Validate token format
  2. Avoid logging the token value
-    HUBSPOT_ACCESS_TOKEN = os.getenv("HUBSPOT_ACCESS_TOKEN")
+    HUBSPOT_ACCESS_TOKEN = os.getenv("HUBSPOT_ACCESS_TOKEN", "").strip()
+    if not HUBSPOT_ACCESS_TOKEN or not HUBSPOT_ACCESS_TOKEN.startswith("pat-"):
+        logger.error("Invalid or missing Hubspot token")
         return AgentResponse(
             status=AgentStatus.ERROR,
-            message="Hubspot token not present"
+            message="Invalid or missing Hubspot token configuration"
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
HUBSPOT_ACCESS_TOKEN = os.getenv("HUBSPOT_ACCESS_TOKEN")
if not HUBSPOT_ACCESS_TOKEN:
return AgentResponse(
status=AgentStatus.ERROR,
message="Hubspot token not present"
)
HUBSPOT_ACCESS_TOKEN = os.getenv("HUBSPOT_ACCESS_TOKEN", "").strip()
if not HUBSPOT_ACCESS_TOKEN or not HUBSPOT_ACCESS_TOKEN.startswith("pat-"):
logger.error("Invalid or missing Hubspot token")
return AgentResponse(
status=AgentStatus.ERROR,
message="Invalid or missing Hubspot token configuration"
)


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)

Comment on lines +159 to +167
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add timeout handling for external API calls.

The API calls to VideoDB and LLM services should include timeout handling to prevent hanging operations.

             try:
-                transcript_text = videodb_tool.get_transcript(video_id)
+                transcript_text = videodb_tool.get_transcript(video_id, timeout=30)
             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)
+                videodb_tool.index_spoken_words(video_id, timeout=60)
+                transcript_text = videodb_tool.get_transcript(video_id, timeout=30)

             # ... later in the code ...
-            llm_response = self.llm.chat_completions([sales_assist_llm_message.to_llm_msg()])
+            llm_response = self.llm.chat_completions([sales_assist_llm_message.to_llm_msg()], timeout=30)

Also applies to: 175-185

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.",
)
Comment on lines +171 to +185
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Implement rate limiting for LLM calls.

Add rate limiting to prevent excessive LLM API usage and potential costs.

+    def __init__(self, session: Session, **kwargs):
+        super().__init__(session=session, **kwargs)
+        self.agent_name = "sales_assistant"
+        self.description = "This agent will transcribe sales calls, automatically create deal summaries & update CRM software like Salesforce & Hubspot"
+        self.parameters = SALES_ASSISTANT_PARAMETER
+        self.llm = get_default_llm()
+        self._rate_limiter = RateLimiter(max_calls=10, time_window=60)  # 10 calls per minute

     def run(self, video_id:str, collection_id:str, prompt="", *args, **kwargs) -> AgentResponse:
         # ... existing code ...

-            llm_response = self.llm.chat_completions([sales_assist_llm_message.to_llm_msg()])
+            with self._rate_limiter:
+                llm_response = self.llm.chat_completions([sales_assist_llm_message.to_llm_msg()])

         # ... later in the code ...

-            llm_response = self.llm.chat_completions([final_message.to_llm_msg()])
+            with self._rate_limiter:
+                llm_response = self.llm.chat_completions([final_message.to_llm_msg()])

Also applies to: 219-222


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
)
Comment on lines +200 to +207
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for composio_tool response.

The composio_tool response should be validated to ensure it was successful.

     composio_response = composio_tool(
         task=composio_prompt,
         auth_data={
             "name": "HUBSPOT",
             "token": HUBSPOT_ACCESS_TOKEN
         },
         tools_type=ToolsType.actions
     )
+    if not composio_response.get("success"):
+        raise Exception(f"Composio failed: {composio_response.get('error', 'Unknown error')}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
composio_response = composio_tool(
task=composio_prompt,
auth_data={
"name": "HUBSPOT",
"token": HUBSPOT_ACCESS_TOKEN
},
tools_type=ToolsType.actions
)
composio_response = composio_tool(
task=composio_prompt,
auth_data={
"name": "HUBSPOT",
"token": HUBSPOT_ACCESS_TOKEN
},
tools_type=ToolsType.actions
)
if not composio_response.get("success"):
raise Exception(f"Composio failed: {composio_response.get('error', 'Unknown error')}")


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={},
)
Comment on lines +234 to +238
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect variable usage in success message.

The success message uses self.name which doesn't exist, should be self.agent_name.

     return AgentResponse(
         status=AgentStatus.SUCCESS,
-        message=f"Agent {self.name} completed successfully.",
+        message=f"Agent {self.agent_name} completed successfully.",
         data={},
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return AgentResponse(
status=AgentStatus.SUCCESS,
message=f"Agent {self.name} completed successfully.",
data={},
)
return AgentResponse(
status=AgentStatus.SUCCESS,
message=f"Agent {self.agent_name} completed successfully.",
data={},
)

3 changes: 2 additions & 1 deletion backend/director/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -67,6 +67,7 @@ def __init__(self, db, **kwargs):
ComposioAgent,
ComparisonAgent,
WebSearchAgent,
SalesAssistantAgent
]

def add_videodb_state(self, session):
Expand Down
29 changes: 26 additions & 3 deletions backend/director/tools/composio_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand Down