mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-03 20:41:07 +02:00
fix(copilot): make Copilot aware of native Slack and query selected channels (#627)
When Slack is connected natively (desktop/cURL auth, not Composio), the Copilot now routes Slack requests to the native slack skill instead of composio-integration, surfaces the user's selected channels in the system prompt, and steers catch-up queries to 'agent-slack message list --oldest' rather than unreads/search (which return empty under desktop-imported auth). Instruction cache is invalidated on slack:setConfig and knowledgeSources:upsert so changes take effect.
This commit is contained in:
parent
c38ddef93f
commit
2f926f8dc0
3 changed files with 88 additions and 10 deletions
|
|
@ -1054,6 +1054,9 @@ export function setupIpcHandlers() {
|
||||||
'slack:setConfig': async (_event, args) => {
|
'slack:setConfig': async (_event, args) => {
|
||||||
const repo = container.resolve<ISlackConfigRepo>('slackConfigRepo');
|
const repo = container.resolve<ISlackConfigRepo>('slackConfigRepo');
|
||||||
await repo.setConfig({ enabled: args.enabled, workspaces: args.workspaces });
|
await repo.setConfig({ enabled: args.enabled, workspaces: args.workspaces });
|
||||||
|
// Connecting/disconnecting Slack changes the Copilot's routing (native
|
||||||
|
// `slack` skill vs. Composio), so rebuild its cached instructions.
|
||||||
|
invalidateCopilotInstructionsCache();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
'slack:cliStatus': async () => {
|
'slack:cliStatus': async () => {
|
||||||
|
|
@ -1227,6 +1230,9 @@ export function setupIpcHandlers() {
|
||||||
'knowledgeSources:upsert': async (_event, args) => {
|
'knowledgeSources:upsert': async (_event, args) => {
|
||||||
const config = knowledgeSourcesRepo.upsertSource(args);
|
const config = knowledgeSourcesRepo.upsertSource(args);
|
||||||
if (args.provider === 'slack') {
|
if (args.provider === 'slack') {
|
||||||
|
// The Copilot prompt lists the selected Slack channels, so refresh it
|
||||||
|
// whenever the channel selection changes.
|
||||||
|
invalidateCopilotInstructionsCache();
|
||||||
triggerSlackKnowledgeSync();
|
triggerSlackKnowledgeSync();
|
||||||
void syncSlackKnowledgeSources().catch(error => {
|
void syncSlackKnowledgeSources().catch(error => {
|
||||||
console.error('[SlackKnowledge] Immediate sync after settings update failed:', error);
|
console.error('[SlackKnowledge] Immediate sync after settings update failed:', error);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import { isConfigured as isComposioConfigured } from "../../composio/client.js";
|
||||||
import { CURATED_TOOLKITS } from "@x/shared/dist/composio.js";
|
import { CURATED_TOOLKITS } from "@x/shared/dist/composio.js";
|
||||||
import container from "../../di/container.js";
|
import container from "../../di/container.js";
|
||||||
import type { ICodeModeConfigRepo } from "../../code-mode/repo.js";
|
import type { ICodeModeConfigRepo } from "../../code-mode/repo.js";
|
||||||
|
import type { ISlackConfigRepo } from "../../slack/repo.js";
|
||||||
|
import { knowledgeSourcesRepo } from "../../knowledge/sources/repo.js";
|
||||||
|
|
||||||
const runtimeContextPrompt = getRuntimeContextPrompt(getRuntimeContext());
|
const runtimeContextPrompt = getRuntimeContextPrompt(getRuntimeContext());
|
||||||
|
|
||||||
|
|
@ -12,7 +14,7 @@ const runtimeContextPrompt = getRuntimeContextPrompt(getRuntimeContext());
|
||||||
* Generate dynamic instructions section for Composio integrations.
|
* Generate dynamic instructions section for Composio integrations.
|
||||||
* Lists connected toolkits and explains the meta-tool discovery flow.
|
* Lists connected toolkits and explains the meta-tool discovery flow.
|
||||||
*/
|
*/
|
||||||
async function getComposioToolsPrompt(): Promise<string> {
|
async function getComposioToolsPrompt(slackConnected: boolean = false): Promise<string> {
|
||||||
if (!(await isComposioConfigured())) {
|
if (!(await isComposioConfigured())) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
@ -22,28 +24,54 @@ async function getComposioToolsPrompt(): Promise<string> {
|
||||||
? `**Currently connected:** ${connectedToolkits.map(slug => CURATED_TOOLKITS.find(t => t.slug === slug)?.displayName ?? slug).join(', ')}`
|
? `**Currently connected:** ${connectedToolkits.map(slug => CURATED_TOOLKITS.find(t => t.slug === slug)?.displayName ?? slug).join(', ')}`
|
||||||
: `**No services connected yet.** Load the \`composio-integration\` skill to help the user connect one.`;
|
: `**No services connected yet.** Load the \`composio-integration\` skill to help the user connect one.`;
|
||||||
|
|
||||||
|
// Slack is connected natively, so exclude it from the Composio catch-all.
|
||||||
|
const slackException = slackConnected
|
||||||
|
? ` Exception: **Slack is connected natively** — use the \`slack\` skill for Slack, not Composio.`
|
||||||
|
: '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
## Composio Integrations
|
## Composio Integrations
|
||||||
|
|
||||||
${connectedSection}
|
${connectedSection}
|
||||||
|
|
||||||
Load the \`composio-integration\` skill when the user asks to interact with any third-party service. NEVER say "I can't access [service]" without loading the skill and trying Composio first.
|
Load the \`composio-integration\` skill when the user asks to interact with any third-party service. NEVER say "I can't access [service]" without loading the skill and trying Composio first.${slackException}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildStaticInstructions(composioEnabled: boolean, catalog: string, codeModeEnabled: boolean = true): string {
|
function buildStaticInstructions(composioEnabled: boolean, catalog: string, codeModeEnabled: boolean = true, slackConnected: boolean = false, slackChannelsHint: string = ''): string {
|
||||||
// Conditionally include Composio-related instruction sections
|
// Conditionally include Composio-related instruction sections
|
||||||
const emailDraftSuffix = composioEnabled
|
const emailDraftSuffix = composioEnabled
|
||||||
? ` Do NOT load this skill for reading, fetching, or checking emails — use the \`composio-integration\` skill for that instead.`
|
? ` Do NOT load this skill for reading, fetching, or checking emails — use the \`composio-integration\` skill for that instead.`
|
||||||
: ` Do NOT load this skill for reading, fetching, or checking emails.`;
|
: ` Do NOT load this skill for reading, fetching, or checking emails.`;
|
||||||
|
|
||||||
|
// When Slack is connected natively (desktop/cURL auth, not Composio), keep it
|
||||||
|
// out of the Composio routing examples so the Copilot doesn't try to connect
|
||||||
|
// it through Composio and wrongly report it as unavailable.
|
||||||
|
const composioServiceExamples = slackConnected
|
||||||
|
? 'Gmail, GitHub, LinkedIn, Notion, Google Sheets, Jira, etc.'
|
||||||
|
: 'Gmail, GitHub, Slack, LinkedIn, Notion, Google Sheets, Jira, etc.';
|
||||||
|
|
||||||
const thirdPartyBlock = composioEnabled
|
const thirdPartyBlock = composioEnabled
|
||||||
? `\n**Third-Party Services:** When users ask to interact with any external service (Gmail, GitHub, Slack, LinkedIn, Notion, Google Sheets, Jira, etc.) — reading emails, listing issues, sending messages, fetching profiles — load the \`composio-integration\` skill first. Do NOT look in local \`gmail_sync/\` or \`calendar_sync/\` folders for live data.\n`
|
? `\n**Third-Party Services:** When users ask to interact with any external service (${composioServiceExamples}) — reading emails, listing issues, sending messages, fetching profiles — load the \`composio-integration\` skill first. Do NOT look in local \`gmail_sync/\` or \`calendar_sync/\` folders for live data.\n`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Slack is connected directly in Rowboat (agent-slack CLI), independent of
|
||||||
|
// Composio. Route every Slack request to the native \`slack\` skill so the
|
||||||
|
// Copilot never claims Slack isn't connected or sends it through Composio.
|
||||||
|
const slackChannelsLine = slackChannelsHint
|
||||||
|
? ` The user has selected these Slack channels to follow: ${slackChannelsHint}. For broad "what's on my Slack / catch me up / anything new" requests, query THESE channels directly with \`agent-slack message list "#channel" --workspace <url> --oldest <unix-seconds> --limit 100 --resolve-users\` (use \`--oldest\`/\`--latest\` to scope to today/yesterday). Do NOT rely on \`search messages\` or \`unreads\` to answer catch-up questions — they frequently return empty with desktop-imported auth even when channels have messages; direct \`message list\` is authoritative.`
|
||||||
|
: '';
|
||||||
|
const slackBlock = slackConnected
|
||||||
|
? `\n**Slack (connected):** Slack is connected directly in Rowboat (via the agent-slack CLI, not Composio). For ANY Slack request — summarizing or reading messages, catching up on channels or DMs, searching, listing users, or sending a message — your FIRST action MUST be \`loadSkill('slack')\`, then use the \`agent-slack\` commands it documents via \`executeCommand\` (the selected workspaces are in \`config/slack.json\`). NEVER tell the user Slack isn't connected, and NEVER route Slack through the \`composio-integration\` skill.${slackChannelsLine}\n`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const slackToolPriority = slackConnected
|
||||||
|
? ` For Slack specifically, load the \`slack\` skill and use the agent-slack CLI — Slack is connected natively, not via Composio.`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const toolPriority = composioEnabled
|
const toolPriority = composioEnabled
|
||||||
? `For third-party services (GitHub, Gmail, Slack, etc.), load the \`composio-integration\` skill. For capabilities Composio doesn't cover (web search, file scraping, audio), use MCP tools via the \`mcp-integration\` skill.`
|
? `For third-party services (GitHub, Gmail, etc.), load the \`composio-integration\` skill.${slackToolPriority} For capabilities Composio doesn't cover (web search, file scraping, audio), use MCP tools via the \`mcp-integration\` skill.`
|
||||||
: `For capabilities like web search, file scraping, and audio, use MCP tools via the \`mcp-integration\` skill.`;
|
: `For capabilities like web search, file scraping, and audio, use MCP tools via the \`mcp-integration\` skill.${slackToolPriority}`;
|
||||||
|
|
||||||
const slackToolsLine = composioEnabled
|
const slackToolsLine = composioEnabled
|
||||||
? `- \`slack-checkConnection\`, \`slack-listAvailableTools\`, \`slack-executeAction\` - Slack integration (requires Slack to be connected via Composio). Use \`slack-listAvailableTools\` first to discover available tool slugs, then \`slack-executeAction\` to execute them.\n`
|
? `- \`slack-checkConnection\`, \`slack-listAvailableTools\`, \`slack-executeAction\` - Slack integration (requires Slack to be connected via Composio). Use \`slack-listAvailableTools\` first to discover available tool slugs, then \`slack-executeAction\` to execute them.\n`
|
||||||
|
|
@ -76,7 +104,7 @@ Rowboat is an agentic assistant for everyday work - emails, meetings, projects,
|
||||||
|
|
||||||
**Email Drafting:** When users ask you to **draft** or **compose** emails (e.g., "draft a follow-up to Monica", "write an email to John about the project"), load the \`draft-emails\` skill first.${emailDraftSuffix}
|
**Email Drafting:** When users ask you to **draft** or **compose** emails (e.g., "draft a follow-up to Monica", "write an email to John about the project"), load the \`draft-emails\` skill first.${emailDraftSuffix}
|
||||||
|
|
||||||
${thirdPartyBlock}**Meeting Prep:** When users ask you to prepare for a meeting, prep for a call, or brief them on attendees, load the \`meeting-prep\` skill first. It provides structured guidance for gathering context about attendees from the knowledge base and creating useful meeting briefs.
|
${thirdPartyBlock}${slackBlock}**Meeting Prep:** When users ask you to prepare for a meeting, prep for a call, or brief them on attendees, load the \`meeting-prep\` skill first. It provides structured guidance for gathering context about attendees from the knowledge base and creating useful meeting briefs.
|
||||||
|
|
||||||
**Create Presentations:** When users ask you to create a presentation, slide deck, pitch deck, or PDF slides, load the \`create-presentations\` skill first. It provides structured guidance for generating PDF presentations using context from the knowledge base.
|
**Create Presentations:** When users ask you to create a presentation, slide deck, pitch deck, or PDF slides, load the \`create-presentations\` skill first. It provides structured guidance for generating PDF presentations using context from the knowledge base.
|
||||||
|
|
||||||
|
|
@ -332,14 +360,41 @@ export async function buildCopilotInstructions(): Promise<string> {
|
||||||
} catch {
|
} catch {
|
||||||
// repo unavailable — default to disabled
|
// repo unavailable — default to disabled
|
||||||
}
|
}
|
||||||
|
let slackConnected = false;
|
||||||
|
let slackChannelsHint = '';
|
||||||
|
try {
|
||||||
|
const slackRepo = container.resolve<ISlackConfigRepo>('slackConfigRepo');
|
||||||
|
const slackConfig = await slackRepo.getConfig();
|
||||||
|
slackConnected = slackConfig.enabled && slackConfig.workspaces.length > 0;
|
||||||
|
} catch {
|
||||||
|
// repo unavailable — default to not connected
|
||||||
|
}
|
||||||
|
if (slackConnected) {
|
||||||
|
try {
|
||||||
|
// Surface the channels the user selected for sync so the Copilot
|
||||||
|
// queries those directly instead of relying on workspace-wide search.
|
||||||
|
const slackSource = knowledgeSourcesRepo.getConfig().sources
|
||||||
|
.find(source => source.provider === 'slack' && source.enabled);
|
||||||
|
const channels = (slackSource?.scopes ?? []).filter(scope => scope.type === 'channel');
|
||||||
|
slackChannelsHint = channels
|
||||||
|
.map(scope => {
|
||||||
|
const raw = scope.name || scope.id;
|
||||||
|
const display = raw.startsWith('#') ? raw : `#${raw}`;
|
||||||
|
return scope.workspaceUrl ? `${display} (${scope.workspaceUrl})` : display;
|
||||||
|
})
|
||||||
|
.join(', ');
|
||||||
|
} catch {
|
||||||
|
// knowledge sources unavailable — fall back to no channel hint
|
||||||
|
}
|
||||||
|
}
|
||||||
const excludeIds: string[] = [];
|
const excludeIds: string[] = [];
|
||||||
if (!composioEnabled) excludeIds.push('composio-integration');
|
if (!composioEnabled) excludeIds.push('composio-integration');
|
||||||
if (!codeModeEnabled) excludeIds.push('code-with-agents');
|
if (!codeModeEnabled) excludeIds.push('code-with-agents');
|
||||||
const catalog = excludeIds.length > 0
|
const catalog = excludeIds.length > 0
|
||||||
? buildSkillCatalog({ excludeIds })
|
? buildSkillCatalog({ excludeIds })
|
||||||
: skillCatalog;
|
: skillCatalog;
|
||||||
const baseInstructions = buildStaticInstructions(composioEnabled, catalog, codeModeEnabled);
|
const baseInstructions = buildStaticInstructions(composioEnabled, catalog, codeModeEnabled, slackConnected, slackChannelsHint);
|
||||||
const composioPrompt = await getComposioToolsPrompt();
|
const composioPrompt = await getComposioToolsPrompt(slackConnected);
|
||||||
cachedInstructions = composioPrompt
|
cachedInstructions = composioPrompt
|
||||||
? baseInstructions + '\n' + composioPrompt
|
? baseInstructions + '\n' + composioPrompt
|
||||||
: baseInstructions;
|
: baseInstructions;
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,27 @@ You interact with Slack by running **agent-slack** commands through \`executeCom
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Check Connection
|
## 1. Check Connection & Selected Channels
|
||||||
|
|
||||||
Before any Slack operation, read \`config/slack.json\` from the workspace root. If \`enabled\` is \`false\` or the \`workspaces\` array is empty, simply tell the user: "Slack is not enabled. You can enable it in the Connectors settings." Do not attempt any agent-slack commands.
|
Before any Slack operation, read \`config/slack.json\` from the workspace root. If \`enabled\` is \`false\` or the \`workspaces\` array is empty, simply tell the user: "Slack is not enabled. You can enable it in the Connectors settings." Do not attempt any agent-slack commands.
|
||||||
|
|
||||||
If enabled, use the workspace URLs from the config for all commands.
|
If enabled, use the workspace URLs from the config for all commands.
|
||||||
|
|
||||||
|
**Which channels the user follows:** The user selects specific channels to sync in \`config/knowledge_sources.json\`. Read that file and find the source with \`"provider": "slack"\`; its \`scopes\` array (entries with \`"type": "channel"\`) lists the selected channels (each has a \`name\` like \`#general\` and an optional \`workspaceUrl\`). For broad "what's on my Slack / catch me up / anything new" requests where the user did NOT name a channel, query these selected channels directly — do not guess or run workspace-wide search.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1b. Catching Up ("what's new", "today", "yesterday")
|
||||||
|
|
||||||
|
For catch-up questions, list recent messages from each selected channel and filter by time with \`--oldest\` / \`--latest\` (Unix-epoch seconds):
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
# Everything in #general since the start of today (compute the epoch for 00:00 local)
|
||||||
|
agent-slack message list "#general" --workspace https://team.slack.com --oldest 1718668800 --limit 100 --resolve-users
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Do NOT use \`agent-slack unreads\` or \`agent-slack search messages\` to answer catch-up questions.** With desktop-imported auth those endpoints frequently return empty even when channels clearly have messages. Direct \`message list\` against the selected channels is the authoritative source. Run one \`message list\` per selected channel (batch them in a single \`executeCommand\` with \`;\` separators), then summarize across channels. Always pass \`--resolve-users\` so author names are readable.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Core Commands
|
## 2. Core Commands
|
||||||
|
|
@ -41,6 +56,8 @@ agent-slack message react remove "<target>" <emoji> --ts <ts>
|
||||||
|
|
||||||
### Search
|
### Search
|
||||||
|
|
||||||
|
Note: search is best for finding a *specific* message by keyword. It can return empty under desktop-imported auth, so never conclude "there's nothing on Slack" from an empty search — fall back to \`message list\` on the selected channels (see section 1b).
|
||||||
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
agent-slack search messages "query text" --limit 20
|
agent-slack search messages "query text" --limit 20
|
||||||
agent-slack search messages "query" --channel "#channel-name" --user "@username"
|
agent-slack search messages "query" --channel "#channel-name" --user "@username"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue