diff --git a/src/main/java/integrations/telex/salesagent/lead/dto/PeopleLeadDto.java b/src/main/java/integrations/telex/salesagent/lead/dto/PeopleLeadDto.java new file mode 100644 index 0000000..ce86479 --- /dev/null +++ b/src/main/java/integrations/telex/salesagent/lead/dto/PeopleLeadDto.java @@ -0,0 +1,17 @@ +package integrations.telex.salesagent.lead.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PeopleLeadDto { + private String fullName; + private String headline; + private String location; + private String profileURL; + private String username; +} + diff --git a/src/main/java/integrations/telex/salesagent/lead/dto/PeopleSearchRequest.java b/src/main/java/integrations/telex/salesagent/lead/dto/PeopleSearchRequest.java index 0ada9e5..0616408 100644 --- a/src/main/java/integrations/telex/salesagent/lead/dto/PeopleSearchRequest.java +++ b/src/main/java/integrations/telex/salesagent/lead/dto/PeopleSearchRequest.java @@ -1,8 +1,31 @@ package integrations.telex.salesagent.lead.dto; +import integrations.telex.salesagent.lead.service.LocationMappingService; import lombok.Data; +import lombok.RequiredArgsConstructor; @Data +@RequiredArgsConstructor public class PeopleSearchRequest { - private String url; + private String keyword; + private String location; + + private transient LocationMappingService locationMappingService; + + public String buildLinkedInSearchUrl() { + StringBuilder urlBuilder = new StringBuilder("https://www.linkedin.com/search/results/people/?"); + + if (location != null && !location.isEmpty()) { + String geoUrn = locationMappingService.getGeoUrnForLocation(location); + if (geoUrn != null) { + urlBuilder.append("geoUrn=%5B%22").append(geoUrn).append("%22%5D&"); + } else { + // Handle case where location is not found in the mapping + System.out.println("Location not found in mapping: " + location); + } + } + + urlBuilder.append("origin=FACETED_SEARCH"); + return urlBuilder.toString(); + } } diff --git a/src/main/java/integrations/telex/salesagent/lead/service/LeadPeopleResearchService.java b/src/main/java/integrations/telex/salesagent/lead/service/LeadPeopleResearchService.java index 5a35a5c..b7fd9a6 100644 --- a/src/main/java/integrations/telex/salesagent/lead/service/LeadPeopleResearchService.java +++ b/src/main/java/integrations/telex/salesagent/lead/service/LeadPeopleResearchService.java @@ -1,9 +1,10 @@ package integrations.telex.salesagent.lead.service; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import integrations.telex.salesagent.lead.dto.PeopleLeadDto; import integrations.telex.salesagent.lead.dto.PeopleSearchRequest; -import integrations.telex.salesagent.lead.dto.RapidLeadDto; import integrations.telex.salesagent.telex.service.TelexClient; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -16,6 +17,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Slf4j @Service @@ -34,65 +36,94 @@ public class LeadPeopleResearchService { private final ObjectMapper objectMapper; private final TelexClient telexClient; - public List queryLeads(String channelID, PeopleSearchRequest request) { + public List queryLeads(String channelID, PeopleSearchRequest request) { try { - // Prepare headers - HttpHeaders headers = new HttpHeaders(); - headers.set("X-RapidAPI-Key", rapidApiKey); - headers.set("X-RapidAPI-Host", rapidApiHost); - headers.setContentType(MediaType.APPLICATION_JSON); - - // Build the request body (LinkedIn search URL) - Map payload = new HashMap<>(); - payload.put("url", request.getUrl()); - - HttpEntity> entity = new HttpEntity<>(payload, headers); - - // Call RapidAPI - log.info("Searching for people with URL: {}", request.getUrl()); - ResponseEntity response = restTemplate.exchange( - rapidApiUrl, HttpMethod.POST, entity, String.class - ); - - // Process the response - if (response.getStatusCode().is2xxSuccessful()) { - List leads = formatPeopleResponse(response.getBody()); - - if (leads.isEmpty()) { - String report = "🔍 No LinkedIn profiles found for the given search URL."; - telexClient.sendInstruction(channelID, report); - } else { - for (RapidLeadDto lead : leads) { - telexClient.processTelexPayload(channelID, lead); - } - } - return leads; - } else { - log.error("API Error: {}", response.getStatusCode()); - return new ArrayList<>(); + // Build URL with location filter + String searchUrl = request.buildLinkedInSearchUrl(); + log.info("Constructed LinkedIn search URL: {}", searchUrl); + + // Make API call + List leads = fetchLeadsFromApi(searchUrl); + + // Apply keyword filtering + if (request.getKeyword() != null && !request.getKeyword().isEmpty()) { + leads = filterByKeyword(leads, request.getKeyword()); + log.info("Filtered {} leads by keyword: {}", leads.size(), request.getKeyword()); } + + // Process results + processResults(channelID, leads); + + return leads; + } catch (Exception e) { log.error("Error calling RapidAPI: {}", e.getMessage(), e); + } + return new ArrayList<>(); + } + + private List filterByKeyword(List leads, String keyword) { + String lowerKeyword = keyword.toLowerCase(); + return leads.stream() + .filter(lead -> + (lead.getHeadline() != null && + lead.getHeadline().toLowerCase().contains(lowerKeyword)) || + (lead.getFullName() != null && + lead.getFullName().toLowerCase().contains(lowerKeyword))) + .collect(Collectors.toList()); + } + + private void processResults(String channelID, List leads) throws JsonProcessingException { + if (leads.isEmpty()) { + telexClient.sendInstruction(channelID, "🔍 No matching profiles found."); + } else { + leads.forEach(lead -> { + try { + telexClient.processTelexPayload(channelID, lead); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }); + } + } + + private List fetchLeadsFromApi(String searchUrl) { + HttpHeaders headers = new HttpHeaders(); + headers.set("X-RapidAPI-Key", rapidApiKey); + headers.set("X-RapidAPI-Host", rapidApiHost); + headers.setContentType(MediaType.APPLICATION_JSON); + + Map payload = new HashMap<>(); + payload.put("url", searchUrl); + + HttpEntity> entity = new HttpEntity<>(payload, headers); + + ResponseEntity response = restTemplate.exchange( + rapidApiUrl, HttpMethod.POST, entity, String.class + ); + + if (!response.getStatusCode().is2xxSuccessful()) { + log.error("API request failed with status: {}", response.getStatusCode()); return new ArrayList<>(); } + + return formatPeopleResponse(response.getBody()); } - // Parse API response into Lead objects - private List formatPeopleResponse(String responseBody) { - List leads = new ArrayList<>(); + private List formatPeopleResponse(String responseBody) { + List leads = new ArrayList<>(); try { JsonNode root = objectMapper.readTree(responseBody); JsonNode items = root.path("data").path("items"); if (items.isArray()) { for (JsonNode item : items) { - RapidLeadDto lead = new RapidLeadDto( - null, + PeopleLeadDto lead = new PeopleLeadDto( item.path("fullName").asText(), + item.path("headline").asText(), + item.path("location").asText(), item.path("profileURL").asText(), - String.format("%s | %s", - item.path("headline").asText(), - item.path("location").asText()) + item.path("username").asText() ); leads.add(lead); } diff --git a/src/main/java/integrations/telex/salesagent/lead/service/LocationMappingService.java b/src/main/java/integrations/telex/salesagent/lead/service/LocationMappingService.java new file mode 100644 index 0000000..bb5f626 --- /dev/null +++ b/src/main/java/integrations/telex/salesagent/lead/service/LocationMappingService.java @@ -0,0 +1,25 @@ +package integrations.telex.salesagent.lead.service; + +import org.springframework.stereotype.Service; + +import java.util.Map; + +import static java.util.Map.entry; + +@Service +public class LocationMappingService { + private static final Map LOCATION_TO_GEOURN = Map.of( + "lagos", "104197452", + "atlanta", "103644278", + "new york", "100288700", + "abuja", "101711968", + "port harcourt", "114378074", + "london", "90009496", + "kaduna", "103668447" + ); + + public String getGeoUrnForLocation(String locationName) { + if (locationName == null) return null; + return LOCATION_TO_GEOURN.get(locationName.toLowerCase()); + } +} diff --git a/src/main/java/integrations/telex/salesagent/lead/service/RapidLeadResearch.java b/src/main/java/integrations/telex/salesagent/lead/service/RapidLeadResearch.java index b2f0e55..fae399d 100644 --- a/src/main/java/integrations/telex/salesagent/lead/service/RapidLeadResearch.java +++ b/src/main/java/integrations/telex/salesagent/lead/service/RapidLeadResearch.java @@ -69,7 +69,7 @@ public List queryLeads(String channelID,CompanySearchRequest reque // Forward each lead to Telex for (RapidLeadDto lead : newLeads) { - telexClient.processTelexPayload(channelID, lead); + //telexClient.processTelexPayload(channelID, lead); log.info("Lead sent to Telex: {}", lead.getName()); } } else { diff --git a/src/main/java/integrations/telex/salesagent/telex/service/TelexClient.java b/src/main/java/integrations/telex/salesagent/telex/service/TelexClient.java index a68f6d6..087f5df 100644 --- a/src/main/java/integrations/telex/salesagent/telex/service/TelexClient.java +++ b/src/main/java/integrations/telex/salesagent/telex/service/TelexClient.java @@ -3,8 +3,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import integrations.telex.salesagent.config.AppConfig; +import integrations.telex.salesagent.lead.dto.PeopleLeadDto; import integrations.telex.salesagent.lead.dto.RapidLeadDto; import integrations.telex.salesagent.lead.model.Lead; +import integrations.telex.salesagent.lead.service.LeadPeopleResearchService; import integrations.telex.salesagent.telex.util.FormatTelexMessage; import integrations.telex.salesagent.user.dto.request.TelexPayload; import lombok.RequiredArgsConstructor; @@ -20,6 +22,7 @@ public class TelexClient { private final RestTemplate restTemplate; private final ObjectMapper objectMapper; private final FormatTelexMessage formatTelexMessage; + private final LeadPeopleResearchService leadPeopleResearchService; public void sendToTelexChannel(String channelID, String message) { try { @@ -32,7 +35,15 @@ public void sendToTelexChannel(String channelID, String message) { } } - public void processTelexPayload(String channelID, RapidLeadDto lead) throws JsonProcessingException { +// public void processTelexPayload(String channelID, RapidLeadDto lead) throws JsonProcessingException { +// String message = formatTelexMessage.formatNewLeadMessage(lead) + "\n\nSales Agent Bot"; +// +// TelexPayload telexPayload = new TelexPayload("New Lead Alert", "Sales Agent", "success", message); +// +// sendToTelexChannel(channelID, objectMapper.writeValueAsString(telexPayload)); +// } + + public void processTelexPayload(String channelID, PeopleLeadDto lead) throws JsonProcessingException { String message = formatTelexMessage.formatNewLeadMessage(lead) + "\n\nSales Agent Bot"; TelexPayload telexPayload = new TelexPayload("New Lead Alert", "Sales Agent", "success", message); diff --git a/src/main/java/integrations/telex/salesagent/telex/util/FormatTelexMessage.java b/src/main/java/integrations/telex/salesagent/telex/util/FormatTelexMessage.java index 837cbfb..cbb76e2 100644 --- a/src/main/java/integrations/telex/salesagent/telex/util/FormatTelexMessage.java +++ b/src/main/java/integrations/telex/salesagent/telex/util/FormatTelexMessage.java @@ -1,5 +1,6 @@ package integrations.telex.salesagent.telex.util; +import integrations.telex.salesagent.lead.dto.PeopleLeadDto; import integrations.telex.salesagent.lead.dto.RapidLeadDto; import org.springframework.stereotype.Component; @@ -15,12 +16,32 @@ public class FormatTelexMessage { Lead Company Summary : %s """; - public String formatNewLeadMessage(RapidLeadDto data) { - return String.format(NEW_LEAD, - data.getId(), - data.getName(), - data.getLinkedinUrl(), - data.getTagline() + private static final String NEW_LEAD_PEOPLE = """ + New lead has been found: + + Lead Name: %s + Lead Headline: %s + Lead Location: %s + Lead Profile URL: %s + Lead Username: %s + """; + +// public String formatNewLeadMessage(RapidLeadDto data) { +// return String.format(NEW_LEAD, +// data.getId(), +// data.getName(), +// data.getLinkedinUrl(), +// data.getTagline() +// ); +// } + + public String formatNewLeadMessage(PeopleLeadDto data) { + return String.format(NEW_LEAD_PEOPLE, + data.getFullName(), + data.getHeadline(), + data.getLocation(), + data.getProfileURL(), + data.getUsername() ); } } diff --git a/src/main/java/integrations/telex/salesagent/user/dto/request/LeadDetails.java b/src/main/java/integrations/telex/salesagent/user/dto/request/LeadDetails.java index 3b444bf..e71cdf9 100644 --- a/src/main/java/integrations/telex/salesagent/user/dto/request/LeadDetails.java +++ b/src/main/java/integrations/telex/salesagent/user/dto/request/LeadDetails.java @@ -10,7 +10,10 @@ @Getter @Setter public class LeadDetails { +// private String businessType; +// private String locations; +// private String companySizes; private String businessType; - private String locations; - private String companySizes; + private String location; + private String keyword; } diff --git a/src/main/java/integrations/telex/salesagent/user/service/OpenAIChatService.java b/src/main/java/integrations/telex/salesagent/user/service/OpenAIChatService.java index 16a7730..21b5564 100644 --- a/src/main/java/integrations/telex/salesagent/user/service/OpenAIChatService.java +++ b/src/main/java/integrations/telex/salesagent/user/service/OpenAIChatService.java @@ -4,8 +4,11 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import integrations.telex.salesagent.lead.dto.CompanySearchRequest; +import integrations.telex.salesagent.lead.dto.PeopleLeadDto; +import integrations.telex.salesagent.lead.dto.PeopleSearchRequest; import integrations.telex.salesagent.lead.dto.RapidLeadDto; import integrations.telex.salesagent.lead.enums.CompanySize; +import integrations.telex.salesagent.lead.service.LeadPeopleResearchService; import integrations.telex.salesagent.lead.service.RapidLeadResearch; import integrations.telex.salesagent.telex.service.TelexClient; import integrations.telex.salesagent.user.dto.request.LeadDetails; @@ -29,7 +32,8 @@ public class OpenAIChatService { private final MistralAiChatModel chatModel; private final ObjectMapper objectMapper; private final RequestFormatter requestFormatter; - private final RapidLeadResearch rapidLeadResearch; + //private final RapidLeadResearch rapidLeadResearch; + private final LeadPeopleResearchService leadPeopleResearchService; private enum ConversationState { INITIAL, @@ -75,11 +79,11 @@ private void handleInitialState(String channelId, String message) throws JsonPro conversationStates.put(channelId, ConversationState.AWAITING_DETAILS); String prompt = String.format(""" Analyze the following text to determine if it contains the necessary parameters - for business type, search location, and company size. If all parameters are present, respond with - "I have understood the requirements. You are looking for [business type] businesses in [search location] - with a size of [company size], let me get to that!".If any of the parameters are missing, respond with + for business type, search location, and keyword. If all parameters are present, respond with + "I have understood the requirements. You are looking for [keyword] leads in [search location] + for [business type], let me get to that!".If any of the parameters are missing, respond with 'Hi, to ensure accurate research, please confirm the business type and the specific location you're targeting, - along with any desired company size criteria.' + along with any desired lead e.g. software engineer.' Text: '%s'" """, message); String response = chatModel.call(prompt); @@ -93,41 +97,34 @@ private void handleDetailsInput(String channelId, String message) throws JsonPro log.info("Lead Details: {}", details); leadDetailsMap.put(channelId, details); - if (details.getLocations() == null || details.getBusinessType() == null) { + if (details.getLocation() == null || details.getBusinessType() == null) { restartConversation(channelId); return; } - String sizeCode = classifyCompanySize(details.getCompanySizes()); - - log.info("Company size code: {}", sizeCode); - - String prompt1 = "Thank you for the details, I'll now conduct research on " + - details.getCompanySizes() + " companies in " + details.getLocations() + + String prompt = "Thank you for the details, I'll now fetch leads for " + details.getKeyword() + " in " + details.getLocation() + " to compile a list of potential linkedIn profiles. Please hold on while we work on this."; - telexClient.sendInstruction(channelId, prompt1); - - CompanySearchRequest searchRequest = convertToCompanySearchRequest(details); - rapidLeadResearch.queryLeads(channelId, searchRequest); - List leads = rapidLeadResearch.queryLeads(channelId,searchRequest); - if (!leads.isEmpty()) { - String prompt2 = String.format("I have found %d leads! \nI would now generate pitches for them!",leads.size()); - telexClient.sendInstruction(channelId, prompt2); - List pitches = generatePitches(details,leads); - for (String pitch: pitches) { - telexClient.sendInstruction(channelId," Here is a pitch for you \n" + pitch); - } - } + telexClient.sendInstruction(channelId, prompt); - String prompt3 = "I'll now conduct researches on " - +details.getCompanySizes() + " "+ details.getBusinessType() + " companies in " + details.getLocations() + - " to compile a list of potential leads. Please hold on while we work on this."; - - telexClient.sendInstruction(channelId, prompt3); + PeopleSearchRequest peopleSearchRequest = new PeopleSearchRequest(); + peopleSearchRequest.setKeyword(details.getKeyword()); + peopleSearchRequest.setLocation(details.getLocation()); + leadPeopleResearchService.queryLeads(channelId, peopleSearchRequest); generateAndSendResearch(channelId, details); + List leads = leadPeopleResearchService.queryLeads(channelId, peopleSearchRequest); + + if (leads.isEmpty()) { + telexClient.sendInstruction(channelId, "🔍 No matching profiles found."); + } else { + List pitches = generatePitches(details, leads); + for (String pitch : pitches) { + telexClient.sendInstruction(channelId, pitch); + } + } + exitProcess(channelId); } catch (Exception e) { @@ -143,11 +140,11 @@ private LeadDetails extractLeadDetails(String userInput) throws JsonProcessingEx Rules: 1. "businessType": Singular form (e.g., "tech startup" → "tech startup"). - 2. "locations": Comma-separated if multiple (e.g., "Berlin, Munich"). - 3. "companySizes": Standardize to "small", "mid-sized", or "large". + 2. "location": Comma-separated if multiple (e.g., "Berlin, Munich"). + 3. "keyword": Singular form (e.g., "software engineer" → "software engineer"). Return ONLY valid JSON. Example: - {"businessType": "law firm", "locations": "London", "companySizes": "mid-sized"} + {"businessType": "laundromats", "location": "lagos", "keyword": "software engineer"} """, userInput); String response = chatModel.call(prompt); @@ -158,13 +155,13 @@ private void generateAndSendResearch(String channelId, LeadDetails details) thro try { // Generate research String researchPrompt = String.format(""" - Provide a detailed business lead research on %s %ss companies in %s. + Provide a detailed business lead research on %s companies in %s. Include: 1. List of 5-10 potential leads with brief descriptions 2. Key market trends in this sector 3. Recommended outreach approach """, - details.getCompanySizes(), details.getBusinessType(), details.getLocations()); + details.getBusinessType(), details.getLocation()); String research = chatModel.call(researchPrompt); telexClient.sendInstruction(channelId, research); @@ -176,27 +173,26 @@ private void generateAndSendResearch(String channelId, LeadDetails details) thro } } - private List generatePitches(LeadDetails details, List leads) throws JsonProcessingException { + private List generatePitches(LeadDetails details, List leads) throws JsonProcessingException { List pitches = new ArrayList<>(); - for (RapidLeadDto lead : leads) { + for (PeopleLeadDto lead : leads) { String samplePitch = String.format(""" Pitch --- - I hope this message finds you well. I lead [Company name] - a firm dedicated to helping %s companies %s. - We understand that every business faces unique challenges, and our tailored approach has empowered companies like [Example Client]. We specialize in [specific service] and believe we could add significant value to your operations. + I hope this message finds you well. I lead %s - a firm dedicated to helping %s. + We understand that every business faces unique challenges, and our tailored approach has empowered [Example Client]. We specialize in [specific service] and believe we could add significant value to your operations. Would you be available for a brief call next week to discuss how we might support your goals? """, - details.getCompanySizes(), - details.getBusinessType().isEmpty() ? "streamline operations and drive sustainable growth" : details.getBusinessType()); + details.getBusinessType(), details.getKeyword()); String prompt = String.format(""" - Generate a short pitch personalized for %s , a %s company, located in %s. Also create a place for my name \s + Generate a short pitch personalized for %s , a %s, located in %s. Also create a place for my name \s and my company. use sample pitch to improve your response. sample pitch : %s """, - lead.getName(), - details.getCompanySizes(), - details.getLocations(), + lead.getFullName(), + details.getKeyword(), + details.getLocation(), samplePitch); String pitch = chatModel.call(prompt); pitches.add(pitch); @@ -250,84 +246,4 @@ private boolean isRestartRequest(String message) { return response.equals("true"); } - private String classifyCompanySize(String companySize) { - if (companySize == null || companySize.isEmpty()) { - return "C"; - } - - companySize = companySize.toLowerCase().trim(); - - return switch (companySize) { - case "large" -> "G"; - case "very large" -> "I"; - case "mid-sized", "medium" -> "D"; - case "small" -> "B"; - default -> "C"; - }; - } - - private CompanySearchRequest convertToCompanySearchRequest(LeadDetails details) { - CompanySearchRequest request = new CompanySearchRequest(); - - // Set keyword from businessType - request.setKeyword(details.getBusinessType()); - - // Parse locations (assuming comma-separated string like "Lagos,New York") - if (details.getLocations() != null && !details.getLocations().isEmpty()) { - List locationIds = Arrays.stream(details.getLocations().split(",")) - .map(String::trim) - .map(this::convertLocationToId) - .filter(Objects::nonNull) - .toList(); - request.setLocations(locationIds); - } - - // Convert company sizes - if (details.getCompanySizes() != null && !details.getCompanySizes().isEmpty()) { - List companySizes = Arrays.stream(details.getCompanySizes().split(",")) - .map(String::trim) - .map(this::convertToCompanySize) - .filter(Objects::nonNull) - .toList(); - request.setCompanySizes(companySizes); - } - - return request; - } - - private Integer convertLocationToId(String locationName) { - Map locationMap =Map.ofEntries( - entry("US", 103644278), - entry("Lagos", 104197452), - entry("Nigeria",105365761), - entry("ABUJA, FCT Nigeria", 101711968), - entry("London Area, United Kingdom", 90009496), - entry("Lekki, Lagos State, Nigeria", 111964948), - entry("Ibeju Lekki, Lagos State, Nigeria", 105956099), - entry("Ikorodu, Lagos State, Nigeria", 103510932), - entry("Agege, Lagos State, Nigeria", 100686593), - entry("Port Harcourt, Rivers State, Nigeria", 114378074), - entry("Ibadan, Oyo State, Nigeria", 110864965), - entry("Kaduna, Kaduna State, Nigeria", 103668447), - entry("Worldwide", 92000000), - entry("Dubai, United Arab Emirates", 106204383), - entry("Asia", 102393603), - entry("North America", 102221843) - ); - return locationMap.getOrDefault(locationName, null); - } - - private CompanySize convertToCompanySize(String sizeString) { - return switch (sizeString.toLowerCase()) { - case "b" -> CompanySize.B; - case "c" -> CompanySize.C; - case "d" -> CompanySize.D; - case "e" -> CompanySize.E; - case "f" -> CompanySize.F; - case "g" -> CompanySize.G; - case "h" -> CompanySize.H; - case "i" -> CompanySize.I; - default -> null; - }; - } }