Repository: clearfeed/quix
Pull request #204: feat(llm-service): inject Slack thread link into LLM context for ticket and issue
Author: login: PranavKeshav24
Created: 2025-05-14T05:18:13Z | Merged: 2025-05-16T12:04:52Z

## Describe your changes
Closes #92 
This PR updates enhanceMessagesWithToolContext to always inject the correct Slack thread link pulled from SlackWorkspace.domain into every LLM prompt. Key changes:

1. New Slack utilities: Added isParentSlackMessage and getSlackMessageUrl in src/lib/utils/slack.ts to determine when a message is the parent vs. a threaded reply and to build the correct archives URL.
2. Converted enhanceMessagesWithToolContext to async so it can fetch SlackWorkspace by teamId.
3. Domain lookup: Reads the workspace’s domain column directly from the database.
4. Helper-based URL builder: Uses getSlackMessageUrl to construct /archives/{channel}/p{ts} links.
5. Universal injection: Pushes a system message containing the Slack URL ahead of any ticket-creation or comment flows (Jira, GitHub, HubSpot, etc.) whenever channelId & threadTs are present.
6. Preserves tool summary: Retains the existing “previous tools” summary exactly as before.

## How has this been tested?

1. Logger output: Verified this.logger.log prints the fetched slackDomain and constructed permalink on every request.
2. Manual end-to-end:
   1. In a Slack thread, invoked Quix to create a Jira/GitHub/HubSpot ticket/issue.
   2. Confirmed the outgoing payload includes the exact Slack thread URL in its description or comment fields.

## Screenshots (if appropriate):

![image](https://github.com/user-attachments/assets/7e5f189d-a8a4-446a-8029-bbc1263bca9c)
![image](https://github.com/user-attachments/assets/0478d72d-fde9-4b8f-9be7-e03716d9adcc)
![image](https://github.com/user-attachments/assets/8b345a1f-0217-40cf-9f9d-a3f0fdf97e85)
![image](https://github.com/user-attachments/assets/ea77d1cd-f069-4693-8a94-40f09524ac54)
![image](https://github.com/user-attachments/assets/3c4108ad-19cd-4d85-8ffd-2571c4fdeeb6)



Linked issue #92: Pass slack link to the LLM during querying
Slack Links aren't added to Jiras/tickets filed via quix. Added slack links to description

Diff:
diff --git a/src/lib/utils/slack.ts b/src/lib/utils/slack.ts
index 2187969b..6d293b57 100644
--- a/src/lib/utils/slack.ts
+++ b/src/lib/utils/slack.ts
@@ -237,3 +237,39 @@ export const getConnectedIntegrations = (
   }
   return connectedIntegration;
 };
+
+/**
+ * @returns true if the given ts & threadTs are of a parent message.
+ * @returns false if it is of a threaded message.
+ */
+export const isParentSlackMessage = ({
+  threadTs,
+  ts
+}: {
+  threadTs: Nullable<string>;
+  ts: string;
+}): boolean => isEmpty(threadTs) || threadTs === ts;
+
+/**
+ * @returns the link to the message on Slack.
+ */
+export function getSlackMessageUrl(payload: {
+  slackDomain: string;
+  channelId: string;
+  messageExternalId: string;
+  parentMessageExternalId?: string;
+}): string {
+  const url = `https://${payload.slackDomain}.slack.com/archives/${
+    payload.channelId
+  }/p${payload.messageExternalId.replace('.', '')}`;
+  if (
+    isParentSlackMessage({
+      ts: payload.messageExternalId,
+      threadTs: payload.parentMessageExternalId ?? ''
+    })
+  ) {
+    return url;
+  } else {
+    return url + '?thread_ts=' + payload.parentMessageExternalId;
+  }
+}
diff --git a/src/llm/llm.service.ts b/src/llm/llm.service.ts
index 8bcd8866..25544fa6 100644
--- a/src/llm/llm.service.ts
+++ b/src/llm/llm.service.ts
@@ -4,7 +4,8 @@ import {
   AvailableToolsWithConfig,
   LLMContext,
   MessageProcessingArgs,
-  SupportedChatModels
+  SupportedChatModels,
+  ToolContextParams
 } from './types';
 import { LlmProviderService } from './llm.provider';
 import { DynamicStructuredTool } from '@langchain/core/tools';
@@ -30,6 +31,7 @@ import { encryptForLogs } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
 import slackify = require('slackify-markdown');
 
 @Injectable()
@@ -42,12 +44,28 @@ export class LlmService {
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
-    previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+  private enhanceMessagesWithToolContext({
+    previousMessages,
+    lastToolCalls,
+    channelId,
+    threadTs,
+    slackWorkspaceDomain
+  }: ToolContextParams): LLMContext[] {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (slackWorkspaceDomain) {
+      const slackUrl = getSlackMessageUrl({
+        slackDomain: slackWorkspaceDomain,
+        channelId,
+        messageExternalId: threadTs ?? ''
+      });
+
+      enhancedPreviousMessages.push({
+        role: 'system',
+        content: `Slack thread URL: ${slackUrl}\nYou must include this link in the description or comment whenever you create or update a resource—such as an issue, ticket, record, or case.`
+      });
+    }
+
     if (lastToolCalls) {
       enhancedPreviousMessages.push({
         role: 'system',
@@ -115,10 +133,13 @@ To continue, you can start a new conversation or ${Md.link(slackWorkspace.getApp
     // Add previous tool calls to system context for better continuity
     let enhancedPreviousMessages: LLMContext[] = [];
     try {
-      enhancedPreviousMessages = this.enhanceMessagesWithToolContext(
+      enhancedPreviousMessages = this.enhanceMessagesWithToolContext({
         previousMessages,
-        conversationState.last_tool_calls
-      );
+        lastToolCalls: conversationState.last_tool_calls,
+        channelId: conversationState.channel_id,
+        threadTs: conversationState.thread_ts,
+        slackWorkspaceDomain: slackWorkspace.domain
+      });
     } catch (error) {
       this.logger.error(`Error enhancing messages with tool context`, error);
       enhancedPreviousMessages = previousMessages;
diff --git a/src/llm/types.ts b/src/llm/types.ts
index 5304f747..84e795ac 100644
--- a/src/llm/types.ts
+++ b/src/llm/types.ts
@@ -1,6 +1,7 @@
 import { ToolConfig } from '@clearfeed-ai/quix-common-agent';
 import { SlackWorkspace } from '@quix/database/models';
 import { Connections } from '@quix/lib/types/common';
+import { ConversationState } from '@quix/database/models';
 
 export type LLMContext = {
   role: 'user' | 'assistant' | 'system';
@@ -25,3 +26,11 @@ export type AvailableToolsWithConfig = Record<
   string,
   { toolConfig: ToolConfig; config?: Connections | SlackWorkspace }
 >;
+
+export interface ToolContextParams {
+  previousMessages: LLMContext[];
+  lastToolCalls: ConversationState['last_tool_calls'];
+  channelId: string;
+  threadTs?: string;
+  slackWorkspaceDomain?: string;
+}


Reviews:
user: login: gemini-code-assist[bot]
body: Hello @PranavKeshav24, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

## Summary of Changes
This pull request introduces a feature to inject the Slack thread link into the LLM context for ticket and issue creation. It modifies `src/lib/utils/slack.ts` to add helper functions for Slack message URL construction and parent message identification. It also updates `src/llm/llm.service.ts` to fetch the Slack workspace domain and inject the Slack thread URL into the LLM prompt as a system message, ensuring the link is included in Jira, GitHub, and HubSpot tickets/issues.

### Highlights
* **Slack URL Injection**: The PR injects the Slack thread URL into the LLM context, ensuring it's included in generated tickets and issues.
* **New Slack Utilities**: The PR introduces `isParentSlackMessage` and `getSlackMessageUrl` in `src/lib/utils/slack.ts` to handle Slack message links.
* **Asynchronous Context Enhancement**: The `enhanceMessagesWithToolContext` function is converted to async to fetch Slack workspace data.

### Changelog
* **src/lib/utils/slack.ts**
  * Added `isParentSlackMessage` function to determine if a message is a parent or a threaded reply (lines 241-251).
  * Added `getSlackMessageUrl` function to construct the Slack message URL based on domain, channel ID, and message ID (lines 256-275).
* **src/llm/llm.service.ts**
  * Imported `SlackWorkspace` model and `getSlackMessageUrl` function (lines 33-34).
  * Injected `SlackWorkspace` model into the constructor (lines 41-42).
  * Modified `enhanceMessagesWithToolContext` to be asynchronous and accept `teamId`, `channelId`, and `threadTs` (lines 49-55).
  * Fetched Slack workspace domain from the database based on `teamId` (lines 58-62).
  * Constructed Slack message URL using `getSlackMessageUrl` and injected it as a system message into the LLM context (lines 64-73).
  * Modified the call to `enhanceMessagesWithToolContext` to be awaited and pass the `team_id`, `channel_id`, and `thread_ts` from the conversation state (lines 143-149).


<details>

<summary><b>Using Gemini Code Assist</b></summary>
<br>

The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.

<b>Invoking Gemini</b>

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands.

Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments.
Help | `/gemini help` | Displays a list of available commands.

<b>Customization</b>

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github).

<b>Limitations & Feedback</b>

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments to provide feedback.

</details>

<b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b>


[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/review-github-code). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).

---

> A link in Slack's domain,
> To context, it's now chained.
> LLM's insight,
> Tickets shine bright,
> With threads correctly explained.


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T05:18:23Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This PR introduces a valuable feature by injecting the Slack thread link into the LLM context, enhancing the information available to the LLM for ticket and issue creation. The changes are well-structured and address the problem effectively. However, there are a few areas that could be improved for clarity and maintainability.

### Summary of Findings
* **Function Naming**: The function `isParentSlackMessage` could benefit from a more descriptive name that clearly indicates its purpose. Consider renaming it to `isRootSlackMessage` or `isOriginalSlackMessage` to better convey that it checks if the message is the root of a thread.
* **Error Handling**: Consider adding a check to ensure that `ws` is not null before accessing its `domain` property. This will prevent potential errors if the Slack workspace is not found in the database.
* **Logging**: It's good to see the slackDomain being logged, but consider using `this.logger.debug` instead of `this.logger.log` for debugging information. This allows you to control the verbosity of the logs and avoid cluttering production logs with debugging statements.

### Merge Readiness
The code changes are well-structured and effectively address the problem of injecting the Slack thread link into the LLM context. However, I recommend addressing the suggestions provided in the review comments before merging. I am unable to approve this pull request, and users should have others review and approve this code before merging.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T05:18:37Z
user: login: PranavKeshav24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T05:28:21Z
user: login: PranavKeshav24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T05:32:22Z
user: login: PranavKeshav24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T05:34:26Z
user: login: batflarrow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T12:55:31Z
user: login: batflarrow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T12:56:15Z
user: login: batflarrow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T12:57:11Z
user: login: batflarrow
body: I don't think it would be helpfull to include the link of the DM, let's add the slack channel link to the LLM context only in cases when Quix is mentioned in the thread and not invoked from a DM.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T12:58:33Z
user: login: PranavKeshav24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T04:31:31Z
user: login: PranavKeshav24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T04:50:00Z
user: login: PranavKeshav24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T04:50:59Z
user: login: PranavKeshav24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:50:45Z
user: login: batflarrow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T09:12:56Z
user: login: batflarrow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T09:13:27Z
user: login: batflarrow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T09:15:06Z
user: login: batflarrow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T09:24:00Z
user: login: PranavKeshav24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T09:42:07Z
user: login: PranavKeshav24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T09:45:47Z
user: login: PranavKeshav24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T09:46:10Z
user: login: PranavKeshav24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T09:46:39Z
user: login: PranavKeshav24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T09:48:26Z
user: login: batflarrow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T10:09:28Z
user: login: batflarrow
body: There is some merge conflict related to encrypt function, please revert those changes
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T10:10:06Z
user: login: PranavKeshav24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T10:22:08Z
user: login: PranavKeshav24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T10:22:23Z
user: login: batflarrow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T11:00:32Z
user: login: PranavKeshav24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T11:11:01Z
user: login: batflarrow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T11:23:19Z
user: login: batflarrow
body: Left one minor comment, please resolve and test before merging
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T11:24:00Z
user: login: PranavKeshav24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T11:37:43Z

Review comments:
diff_hunk: @@ -237,3 +237,39 @@ export const getConnectedIntegrations = (
   }
   return connectedIntegration;
 };
+
+/**
+ * @returns true if the given ts & threadTs are of a parent message.
+ * @returns false if it is of a threaded message.
+ */
+export const isParentSlackMessage = ({
path: src/lib/utils/slack.ts
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The function `isParentSlackMessage` could benefit from a more descriptive name that clearly indicates its purpose. Consider renaming it to `isRootSlackMessage` or `isOriginalSlackMessage` to better convey that it checks if the message is the root of a thread.

```suggestion
export const isRootSlackMessage = ({
```
created_at: 2025-05-14T05:18:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,24 +30,49 @@ import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    teamId: string,
+    channelId: string,
+    threadTs?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (channelId && threadTs) {
+      const ws = await this.slackWorkspaceModel.findOne({
+        where: { team_id: teamId }
+      });
+      const slackDomain = ws?.domain;
path: src/llm/llm.service.ts
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider adding a check to ensure that `ws` is not null before accessing its `domain` property. This will prevent potential errors if the Slack workspace is not found in the database.  You can use optional chaining for a more concise check.

```suggestion
      const slackDomain = ws?.domain ?? '';
```
created_at: 2025-05-14T05:18:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,24 +30,49 @@ import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    teamId: string,
+    channelId: string,
+    threadTs?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (channelId && threadTs) {
+      const ws = await this.slackWorkspaceModel.findOne({
+        where: { team_id: teamId }
+      });
path: src/llm/llm.service.ts
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider adding a more descriptive error message when the Slack workspace is not found. This will help with debugging and troubleshooting.

```typescript
      });
      if (!ws) {
        this.logger.error(`Slack workspace not found for team ID: ${teamId}`);
        return;
      }
```
created_at: 2025-05-14T05:18:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -237,3 +237,39 @@ export const getConnectedIntegrations = (
   }
   return connectedIntegration;
 };
+
+/**
+ * @returns true if the given ts & threadTs are of a parent message.
+ * @returns false if it is of a threaded message.
+ */
+export const isParentSlackMessage = ({
path: src/lib/utils/slack.ts
user: login: PranavKeshav24
body: The term “parent” here aligns with our existing threading terminology and makes it clear we’re checking against the message’s parent-TS.
created_at: 2025-05-14T05:28:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,24 +30,49 @@ import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    teamId: string,
+    channelId: string,
+    threadTs?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (channelId && threadTs) {
+      const ws = await this.slackWorkspaceModel.findOne({
+        where: { team_id: teamId }
+      });
path: src/llm/llm.service.ts
user: login: PranavKeshav24
body: That will lead to the issue/ticket not being created, leading the flow to fail if we return. Will add a logger for it.  
created_at: 2025-05-14T05:32:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,24 +30,49 @@ import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    teamId: string,
+    channelId: string,
+    threadTs?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (channelId && threadTs) {
+      const ws = await this.slackWorkspaceModel.findOne({
+        where: { team_id: teamId }
+      });
+      const slackDomain = ws?.domain;
path: src/llm/llm.service.ts
user: login: PranavKeshav24
body: Implemented in the latest commit. 
created_at: 2025-05-14T05:34:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,24 +30,54 @@ import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    teamId: string,
+    channelId: string,
+    threadTs?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (channelId && threadTs) {
path: src/llm/llm.service.ts
user: login: batflarrow
body: Why do we need a null check on channelId?
created_at: 2025-05-14T12:55:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,24 +30,54 @@ import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    teamId: string,
+    channelId: string,
+    threadTs?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (channelId && threadTs) {
+      const ws = await this.slackWorkspaceModel.findOne({
path: src/llm/llm.service.ts
user: login: batflarrow
body: Slack workspace must present somewhere in the caller. DB queries are expensive and should be avoided when data can be passed around functions
created_at: 2025-05-14T12:56:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,24 +30,54 @@ import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    teamId: string,
+    channelId: string,
+    threadTs?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (channelId && threadTs) {
+      const ws = await this.slackWorkspaceModel.findOne({
+        where: { team_id: teamId }
+      });
+      if (!ws) {
+        this.logger.error(
+          `SlackWorkspace not found for team ID: ${teamId}; skipping Slack URL injection.`
+        );
+      }
+      const slackDomain = ws?.domain ?? '';
+
+      const slackUrl = getSlackMessageUrl({
+        slackDomain: slackDomain || '',
+        channelId,
+        messageExternalId: threadTs
+      });
+
+      enhancedPreviousMessages.push({
+        role: 'system',
+        content: `Slack thread URL: ${slackUrl}\nPlease include this link in every Jira, Github Issue and HubSpot tickets,  description or comment.`
path: src/llm/llm.service.ts
user: login: batflarrow
body: Can you ask ChatGPT to come up with a more generic version of this based on all the integrations we support?
created_at: 2025-05-14T12:57:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,24 +30,54 @@ import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    teamId: string,
+    channelId: string,
+    threadTs?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (channelId && threadTs) {
path: src/llm/llm.service.ts
user: login: PranavKeshav24
body: I added that as a safety check actually. But, the `channelId` and the `threadTs` are always set in the `conversations_states` anyway, so we can actually omit that if needed. 
created_at: 2025-05-15T04:31:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,24 +30,54 @@ import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    teamId: string,
+    channelId: string,
+    threadTs?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (channelId && threadTs) {
+      const ws = await this.slackWorkspaceModel.findOne({
+        where: { team_id: teamId }
+      });
+      if (!ws) {
+        this.logger.error(
+          `SlackWorkspace not found for team ID: ${teamId}; skipping Slack URL injection.`
+        );
+      }
+      const slackDomain = ws?.domain ?? '';
+
+      const slackUrl = getSlackMessageUrl({
+        slackDomain: slackDomain || '',
+        channelId,
+        messageExternalId: threadTs
+      });
+
+      enhancedPreviousMessages.push({
+        role: 'system',
+        content: `Slack thread URL: ${slackUrl}\nPlease include this link in every Jira, Github Issue and HubSpot tickets,  description or comment.`
path: src/llm/llm.service.ts
user: login: PranavKeshav24
body: Let me know if this is okay:
Please include this link in the description or comment whenever you create or update any resource—issues, tickets, records, or cases across all integrations (Jira, GitHub, HubSpot, Salesforce, Notion, Linear, and beyond).
created_at: 2025-05-15T04:49:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,24 +30,54 @@ import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    teamId: string,
+    channelId: string,
+    threadTs?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (channelId && threadTs) {
+      const ws = await this.slackWorkspaceModel.findOne({
path: src/llm/llm.service.ts
user: login: PranavKeshav24
body: I'll use the slackWorkspace to get the domain instead of the expensive db query.
created_at: 2025-05-15T04:50:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,24 +30,54 @@ import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    teamId: string,
+    channelId: string,
+    threadTs?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (channelId && threadTs) {
path: src/llm/llm.service.ts
user: login: PranavKeshav24
body: I've omitted the null check on channelId in the latest commit since it is guaranteed that it is always present inside the conversation_states. 
created_at: 2025-05-15T14:50:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,28 +26,48 @@ import { ConversationState } from '../database/models/conversation-state.model';
 import { InjectModel } from '@nestjs/sequelize';
 import { TRIAL_MAX_MESSAGE_PER_CONVERSATION_COUNT } from '../lib/utils/slack-constants';
 import { Md } from 'slack-block-builder';
-import { encryptForLogs } from '../lib/utils/encryption';
+import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
path: src/llm/llm.service.ts
user: login: batflarrow
body: unused
created_at: 2025-05-16T09:12:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,28 +26,48 @@ import { ConversationState } from '../database/models/conversation-state.model';
 import { InjectModel } from '@nestjs/sequelize';
 import { TRIAL_MAX_MESSAGE_PER_CONVERSATION_COUNT } from '../lib/utils/slack-constants';
 import { Md } from 'slack-block-builder';
-import { encryptForLogs } from '../lib/utils/encryption';
+import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
path: src/llm/llm.service.ts
user: login: batflarrow
body: Let's revert the changes related to the function being async. It's no longer async
created_at: 2025-05-16T09:13:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,28 +26,48 @@ import { ConversationState } from '../database/models/conversation-state.model';
 import { InjectModel } from '@nestjs/sequelize';
 import { TRIAL_MAX_MESSAGE_PER_CONVERSATION_COUNT } from '../lib/utils/slack-constants';
 import { Md } from 'slack-block-builder';
-import { encryptForLogs } from '../lib/utils/encryption';
+import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    channelId: string,
+    threadTs?: string,
+    domain?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (threadTs) {
+      const slackUrl = getSlackMessageUrl({
+        slackDomain: domain || '',
+        channelId,
+        messageExternalId: threadTs
+      });
path: src/llm/llm.service.ts
user: login: batflarrow
body: We can add the link even when threadTs is absent, the thread might get new messages in the future. Instead of this check add a check on domain being null instead
```suggestion
    if (domain) {
      const slackUrl = getSlackMessageUrl({
        slackDomain: domain,
        channelId,
        messageExternalId: threadTs
      });
```
created_at: 2025-05-16T09:14:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,28 +26,48 @@ import { ConversationState } from '../database/models/conversation-state.model';
 import { InjectModel } from '@nestjs/sequelize';
 import { TRIAL_MAX_MESSAGE_PER_CONVERSATION_COUNT } from '../lib/utils/slack-constants';
 import { Md } from 'slack-block-builder';
-import { encryptForLogs } from '../lib/utils/encryption';
+import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    channelId: string,
+    threadTs?: string,
+    domain?: string
path: src/llm/llm.service.ts
user: login: batflarrow
body: 
```suggestion
    slackWorkspaceDomain?: string
```
created_at: 2025-05-16T09:15:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,28 +26,48 @@ import { ConversationState } from '../database/models/conversation-state.model';
 import { InjectModel } from '@nestjs/sequelize';
 import { TRIAL_MAX_MESSAGE_PER_CONVERSATION_COUNT } from '../lib/utils/slack-constants';
 import { Md } from 'slack-block-builder';
-import { encryptForLogs } from '../lib/utils/encryption';
+import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    channelId: string,
+    threadTs?: string,
+    domain?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (threadTs) {
+      const slackUrl = getSlackMessageUrl({
+        slackDomain: domain || '',
+        channelId,
+        messageExternalId: threadTs
+      });
+
+      enhancedPreviousMessages.push({
+        role: 'system',
+        content: `Slack thread URL: ${slackUrl}\nPlease include this link in the description or comment whenever you create or update any resource—issues, tickets, records, or cases—across all integrations (Jira, GitHub, HubSpot, Salesforce, Notion, Linear, and beyond).`
+      });
path: src/llm/llm.service.ts
user: login: batflarrow
body: LLM doesn't care about integrations so that could be noise for LLM. We can have something like this. Can you test with this and let me know if it works?
```suggestion
    enhancedPreviousMessages.push({
    role: 'system',
    content: `Slack thread URL: ${slackUrl}\nYou must include this link in the description or comment whenever you create or update a resource—such as an issue, ticket, record, or case.`
  });
```
created_at: 2025-05-16T09:23:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,28 +26,48 @@ import { ConversationState } from '../database/models/conversation-state.model';
 import { InjectModel } from '@nestjs/sequelize';
 import { TRIAL_MAX_MESSAGE_PER_CONVERSATION_COUNT } from '../lib/utils/slack-constants';
 import { Md } from 'slack-block-builder';
-import { encryptForLogs } from '../lib/utils/encryption';
+import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    channelId: string,
+    threadTs?: string,
+    domain?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (threadTs) {
+      const slackUrl = getSlackMessageUrl({
+        slackDomain: domain || '',
+        channelId,
+        messageExternalId: threadTs
+      });
+
+      enhancedPreviousMessages.push({
+        role: 'system',
+        content: `Slack thread URL: ${slackUrl}\nPlease include this link in the description or comment whenever you create or update any resource—issues, tickets, records, or cases—across all integrations (Jira, GitHub, HubSpot, Salesforce, Notion, Linear, and beyond).`
+      });
path: src/llm/llm.service.ts
user: login: PranavKeshav24
body: I tested the change that you suggested, it works as expected on Jira, Github, and Hubspot. 
created_at: 2025-05-16T09:42:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,28 +26,48 @@ import { ConversationState } from '../database/models/conversation-state.model';
 import { InjectModel } from '@nestjs/sequelize';
 import { TRIAL_MAX_MESSAGE_PER_CONVERSATION_COUNT } from '../lib/utils/slack-constants';
 import { Md } from 'slack-block-builder';
-import { encryptForLogs } from '../lib/utils/encryption';
+import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    channelId: string,
+    threadTs?: string,
+    domain?: string
path: src/llm/llm.service.ts
user: login: PranavKeshav24
body: I've made the change
created_at: 2025-05-16T09:45:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,28 +26,48 @@ import { ConversationState } from '../database/models/conversation-state.model';
 import { InjectModel } from '@nestjs/sequelize';
 import { TRIAL_MAX_MESSAGE_PER_CONVERSATION_COUNT } from '../lib/utils/slack-constants';
 import { Md } from 'slack-block-builder';
-import { encryptForLogs } from '../lib/utils/encryption';
+import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    channelId: string,
+    threadTs?: string,
+    domain?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (threadTs) {
+      const slackUrl = getSlackMessageUrl({
+        slackDomain: domain || '',
+        channelId,
+        messageExternalId: threadTs
+      });
path: src/llm/llm.service.ts
user: login: PranavKeshav24
body: That makes sense. I've updated the check. 
created_at: 2025-05-16T09:46:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,28 +26,48 @@ import { ConversationState } from '../database/models/conversation-state.model';
 import { InjectModel } from '@nestjs/sequelize';
 import { TRIAL_MAX_MESSAGE_PER_CONVERSATION_COUNT } from '../lib/utils/slack-constants';
 import { Md } from 'slack-block-builder';
-import { encryptForLogs } from '../lib/utils/encryption';
+import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
path: src/llm/llm.service.ts
user: login: PranavKeshav24
body: Omitted it in the latest commit. 
created_at: 2025-05-16T09:46:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,28 +26,48 @@ import { ConversationState } from '../database/models/conversation-state.model';
 import { InjectModel } from '@nestjs/sequelize';
 import { TRIAL_MAX_MESSAGE_PER_CONVERSATION_COUNT } from '../lib/utils/slack-constants';
 import { Md } from 'slack-block-builder';
-import { encryptForLogs } from '../lib/utils/encryption';
+import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
path: src/llm/llm.service.ts
user: login: PranavKeshav24
body: It is now async. 
created_at: 2025-05-16T09:48:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,28 +26,48 @@ import { ConversationState } from '../database/models/conversation-state.model';
 import { InjectModel } from '@nestjs/sequelize';
 import { TRIAL_MAX_MESSAGE_PER_CONVERSATION_COUNT } from '../lib/utils/slack-constants';
 import { Md } from 'slack-block-builder';
-import { encryptForLogs } from '../lib/utils/encryption';
+import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
path: src/llm/llm.service.ts
user: login: batflarrow
body: What async operation are we performing here?
created_at: 2025-05-16T10:09:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,28 +26,48 @@ import { ConversationState } from '../database/models/conversation-state.model';
 import { InjectModel } from '@nestjs/sequelize';
 import { TRIAL_MAX_MESSAGE_PER_CONVERSATION_COUNT } from '../lib/utils/slack-constants';
 import { Md } from 'slack-block-builder';
-import { encryptForLogs } from '../lib/utils/encryption';
+import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
path: src/llm/llm.service.ts
user: login: PranavKeshav24
body: async is used here for interface unity, not because the body is asynchronous.
created_at: 2025-05-16T10:22:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,28 +26,48 @@ import { ConversationState } from '../database/models/conversation-state.model';
 import { InjectModel } from '@nestjs/sequelize';
 import { TRIAL_MAX_MESSAGE_PER_CONVERSATION_COUNT } from '../lib/utils/slack-constants';
 import { Md } from 'slack-block-builder';
-import { encryptForLogs } from '../lib/utils/encryption';
+import { encrypt } from '../lib/utils/encryption';
 import { formatToOpenAITool } from '@langchain/openai';
 import { isEqual } from 'lodash';
 import { SOFT_RETENTION_DAYS } from '../lib/constants';
+import { getSlackMessageUrl } from '@quix/lib/utils/slack';
+import { SlackWorkspace } from '@quix/database/models';
 import slackify = require('slackify-markdown');
 
 @Injectable()
 export class LlmService {
   private readonly logger = new Logger(LlmService.name);
   constructor(
+    @InjectModel(SlackWorkspace)
+    private readonly slackWorkspaceModel: typeof SlackWorkspace,
     private readonly llmProvider: LlmProviderService,
     private readonly tool: ToolService,
     @InjectModel(ConversationState)
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
path: src/llm/llm.service.ts
user: login: PranavKeshav24
body: I'll go ahead and omit it.

created_at: 2025-05-16T10:22:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,12 +43,28 @@ export class LlmService {
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    channelId: string,
+    threadTs?: string,
+    slackWorkspaceDomain?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (slackWorkspaceDomain) {
+      const slackUrl = getSlackMessageUrl({
+        slackDomain: slackWorkspaceDomain || '',
path: src/llm/llm.service.ts
user: login: batflarrow
body: Falsy check is already present above
```suggestion
        slackDomain: slackWorkspaceDomain,
```
created_at: 2025-05-16T11:00:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,12 +43,28 @@ export class LlmService {
     private readonly conversationStateModel: typeof ConversationState
   ) {}
 
-  private enhanceMessagesWithToolContext(
+  private async enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
-  ): LLMContext[] {
+    lastToolCalls: ConversationState['last_tool_calls'],
+    channelId: string,
+    threadTs?: string,
+    slackWorkspaceDomain?: string
+  ): Promise<LLMContext[]> {
     const enhancedPreviousMessages = [...previousMessages];
 
+    if (slackWorkspaceDomain) {
+      const slackUrl = getSlackMessageUrl({
+        slackDomain: slackWorkspaceDomain || '',
path: src/llm/llm.service.ts
user: login: PranavKeshav24
body: Yes, the additional check is not required again since we are already checking it in the if condition. I'll update it.
created_at: 2025-05-16T11:11:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,10 +45,26 @@ export class LlmService {
 
   private enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
+    lastToolCalls: ConversationState['last_tool_calls'],
+    channelId: string,
+    threadTs?: string,
+    slackWorkspaceDomain?: string
path: src/llm/llm.service.ts
user: login: batflarrow
body: Convert the parameters to an object. If consecutive arguemnts are of the same primitive data type then it's very easy to cause bugs. I could pass channelId to thread ts by mistake and typescript would allow it
created_at: 2025-05-16T11:23:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,10 +45,26 @@ export class LlmService {
 
   private enhanceMessagesWithToolContext(
     previousMessages: LLMContext[],
-    lastToolCalls: ConversationState['last_tool_calls']
+    lastToolCalls: ConversationState['last_tool_calls'],
+    channelId: string,
+    threadTs?: string,
+    slackWorkspaceDomain?: string
path: src/llm/llm.service.ts
user: login: PranavKeshav24
body: Correct, that is true, since both are strings if we currently pass channelId to thread by mistake, it would still work, which is not as expected. I'll go ahead and wrap those arguments into a ToolContextParams object and update it where ever we call it.
created_at: 2025-05-16T11:37:43Z
author_association: CONTRIBUTOR

PR comments:
user: login: PranavKeshav24
created_at: 2025-05-15T04:52:44Z
body: > I don't think it would be helpfull to include the link of the DM, let's add the slack channel link to the LLM context only in cases when Quix is mentioned in the thread and not invoked from a DM.

1. Whenever Quix is mentioned in the thread, we add the slack channel link to the LLM context
2. Whenever Quix is invoked from a DM, we do not add the link to the LLM context. 

Is that correct?
author_association: CONTRIBUTOR
user: login: PranavKeshav24
created_at: 2025-05-15T14:52:16Z
body: Implemented the changes to resolve the review comments in the latest commits and continued with the present implementation of having the slack channel link being added to the LLM context in both DMs and slack threads as discussed offline. 
author_association: CONTRIBUTOR
user: login: PranavKeshav24
created_at: 2025-05-16T10:24:10Z
body: I checked out to the branch in my vs code, pulled the latest changes and rebased it with the main branch. I do not see any conflicts there.
author_association: CONTRIBUTOR
user: login: batflarrow
created_at: 2025-05-16T10:26:40Z
body: > I checked out to the branch in my vs code, pulled the latest changes and rebased it with the main branch. I do not see any conflicts there.

Check the diff of this PR. In the main branch, we are using encryptForLogs, but the current branch is using encrypt. 
By merge conflicts, I meant that some changes were wrongly reverted during the merge of the main branch into this PR.
https://github.com/clearfeed/quix/blob/1db9c108e74bb5cd9a56495dc049a25076f5a585/src/llm/llm.service.ts#L29
author_association: CONTRIBUTOR
user: login: PranavKeshav24
created_at: 2025-05-16T11:41:47Z
body: I've resolved the comment and tested the changes. 
author_association: CONTRIBUTOR