enabled whatsapp interface
This commit is contained in:
parent
4a01429e4a
commit
cdc3fcab74
|
@ -61,13 +61,28 @@ const ConfigSchema = z.object({
|
|||
rolePermissions: z.record(z.string(), z.array(z.string())),
|
||||
});
|
||||
|
||||
// Load user configuration data from file
|
||||
// Mutable exports that will be updated
|
||||
export let userConfigs: UserConfig[] = [];
|
||||
export let rolePermissions: Record<string, string[]> = {};
|
||||
|
||||
// Function to load config
|
||||
function loadConfig() {
|
||||
try {
|
||||
const userConfigPath = pathInDataDir("user-config.json");
|
||||
const rawData = fs.readFileSync(userConfigPath, "utf-8");
|
||||
const parsedData = JSON.parse(rawData);
|
||||
|
||||
// Validate the parsed JSON using the Zod schema
|
||||
const configData = ConfigSchema.parse(parsedData);
|
||||
|
||||
// Export the validated data
|
||||
export const { users: userConfigs, rolePermissions } = configData;
|
||||
// Update the exported variables
|
||||
userConfigs = configData.users;
|
||||
rolePermissions = configData.rolePermissions;
|
||||
} catch (error) {
|
||||
console.error("Error loading config:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial load
|
||||
loadConfig();
|
||||
|
||||
// Setup auto-reload every minute
|
||||
setInterval(loadConfig, 60 * 1000);
|
||||
|
|
|
@ -32,15 +32,16 @@ export class MessageProcessor {
|
|||
});
|
||||
}
|
||||
|
||||
private checkpointMessageString = "🔄 Chat context has been reset.";
|
||||
|
||||
public async processMessage(message: Message): Promise<void> {
|
||||
const userId = message.author.id;
|
||||
const channelId = message.channelId || userId; // Use message.id if channelId is not available
|
||||
|
||||
// Check if the message is a stop message
|
||||
if (["stop", "reset"].includes(message.content.toLowerCase())) {
|
||||
message.platform !== "whatsapp" &&
|
||||
(await message.send({
|
||||
content: "---setting this point as the start---",
|
||||
content: this.checkpointMessageString,
|
||||
}));
|
||||
// Clear maps
|
||||
const hashes = this.channelIdHashMap.get(channelId) ?? [];
|
||||
|
@ -98,8 +99,7 @@ export class MessageProcessor {
|
|||
let stopIndex = -1;
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
if (
|
||||
history[i].content === "---setting this point as the start---" ||
|
||||
history[i].content.replaceAll("!", "").trim() === "stop"
|
||||
history[i].content === this.checkpointMessageString
|
||||
) {
|
||||
stopIndex = i;
|
||||
break;
|
||||
|
@ -179,6 +179,10 @@ export class MessageProcessor {
|
|||
.map((e) => JSON.stringify(e))
|
||||
.join("\n");
|
||||
|
||||
console.log("Embeds", embeds?.length);
|
||||
console.log("Files", files?.length);
|
||||
console.log("Attachments", msg?.attachments?.length);
|
||||
|
||||
// Transcribe voice messages
|
||||
const voiceMessagesPromises = (msg.attachments || [])
|
||||
.filter(
|
||||
|
@ -197,6 +201,11 @@ export class MessageProcessor {
|
|||
|
||||
const voiceMessages = await Promise.all(voiceMessagesPromises);
|
||||
|
||||
console.log("Voice Messages", voiceMessages);
|
||||
|
||||
const images = (msg.attachments || [])
|
||||
.filter((a) => a.mediaType?.includes("image"))
|
||||
|
||||
// Process context message if any
|
||||
let contextMessage = null;
|
||||
if (msg.threadId) {
|
||||
|
@ -238,7 +247,20 @@ export class MessageProcessor {
|
|||
|
||||
const aiMessage: OpenAI.Chat.ChatCompletionMessageParam = {
|
||||
role,
|
||||
content: contextAsJson,
|
||||
content: (images.length ? [
|
||||
...images.map(img => {
|
||||
return {
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: img.base64 || img.url,
|
||||
},
|
||||
}
|
||||
}),
|
||||
{
|
||||
type: "text",
|
||||
text: contextAsJson,
|
||||
}
|
||||
] : contextAsJson) as string,
|
||||
name:
|
||||
user?.name ||
|
||||
msg.author.username.replace(/\s+/g, "_").substring(0, 64),
|
||||
|
@ -260,7 +282,7 @@ export class MessageProcessor {
|
|||
|
||||
// Collect hashes
|
||||
history.forEach((msg) => {
|
||||
const hash = this.generateHash(msg.content);
|
||||
const hash = this.generateHash(typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content));
|
||||
channelHashes.push(hash);
|
||||
});
|
||||
|
||||
|
@ -376,9 +398,13 @@ ${summaryContent}
|
|||
queueEntry.runningTools = true;
|
||||
}
|
||||
// Indicate running tools
|
||||
if (this.sentMessage?.platformAdapter.config.indicators.processing) {
|
||||
if (this.sentMessage) {
|
||||
await this.sentMessage.edit({ content: `Running ${fnc.name}...` });
|
||||
} else await message.send({ content: `Running ${fnc.name}...` });
|
||||
} else {
|
||||
this.sentMessage = await message.send({ content: `Running ${fnc.name}...` })
|
||||
}
|
||||
}
|
||||
})
|
||||
.on("message", (m) => {
|
||||
if (
|
||||
|
@ -434,7 +460,7 @@ ${summaryContent}
|
|||
|
||||
private generateHash(input: string): string {
|
||||
const hash = createHash("sha256");
|
||||
hash.update(input);
|
||||
hash.update(typeof input === "string" ? input : JSON.stringify(input));
|
||||
return hash.digest("hex");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
DMChannel,
|
||||
} from "discord.js";
|
||||
import { UserConfig, userConfigs } from "../config";
|
||||
import { get_transcription } from "../tools/ask"; // Add this import
|
||||
|
||||
export class DiscordAdapter implements PlatformAdapter {
|
||||
private client: Client;
|
||||
|
@ -254,6 +255,33 @@ export class DiscordAdapter implements PlatformAdapter {
|
|||
// Expose getMessageInterface method
|
||||
public getMessageInterface = this.createMessageInterface;
|
||||
|
||||
public async handleMediaAttachment(attachment: Attachment) {
|
||||
if (!attachment.url) return { mediaType: 'other' as const };
|
||||
|
||||
const response = await fetch(attachment.url);
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
if (attachment.contentType?.includes('image')) {
|
||||
const base64 = `data:${attachment.contentType};base64,${Buffer.from(buffer).toString('base64')}`;
|
||||
return {
|
||||
base64,
|
||||
mediaType: 'image' as const
|
||||
};
|
||||
}
|
||||
|
||||
if (attachment.contentType?.includes('audio')) {
|
||||
// Create temporary file for transcription
|
||||
const tempFile = new File([buffer], 'audio', { type: attachment.contentType });
|
||||
const transcription = await get_transcription(tempFile);
|
||||
return {
|
||||
transcription,
|
||||
mediaType: 'audio' as const
|
||||
};
|
||||
}
|
||||
|
||||
return { mediaType: 'other' as const };
|
||||
}
|
||||
|
||||
private async convertMessage(
|
||||
discordMessage: DiscordMessage
|
||||
): Promise<StdMessage> {
|
||||
|
@ -263,10 +291,19 @@ export class DiscordAdapter implements PlatformAdapter {
|
|||
config: this.getUserById(discordMessage.author.id),
|
||||
};
|
||||
|
||||
const attachments: Attachment[] = discordMessage.attachments.map(
|
||||
(attachment) => ({
|
||||
const attachments: Attachment[] = await Promise.all(
|
||||
discordMessage.attachments.map(async (attachment) => {
|
||||
const stdAttachment: Attachment = {
|
||||
url: attachment.url,
|
||||
contentType: attachment.contentType || undefined,
|
||||
};
|
||||
|
||||
const processedMedia = await this.handleMediaAttachment(stdAttachment);
|
||||
if (processedMedia.base64) stdAttachment.base64 = processedMedia.base64;
|
||||
if (processedMedia.transcription) stdAttachment.transcription = processedMedia.transcription;
|
||||
stdAttachment.mediaType = processedMedia.mediaType;
|
||||
|
||||
return stdAttachment;
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -4,6 +4,18 @@ import { startEventsServer } from "./events";
|
|||
import { Message } from "./message";
|
||||
import { WhatsAppAdapter } from "./whatsapp";
|
||||
|
||||
// Add debounce utility function
|
||||
function debounce<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout;
|
||||
return (...args: Parameters<T>) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func(...args), wait) as any;
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize Discord Adapter and Processor
|
||||
export const discordAdapter = new DiscordAdapter();
|
||||
|
||||
|
@ -17,9 +29,13 @@ export function startInterfaces() {
|
|||
discordAdapter.onMessage(async (message) => {
|
||||
await discordProcessor.processMessage(message);
|
||||
});
|
||||
whatsappAdapter.onMessage(async (message) => {
|
||||
|
||||
// Debounce WhatsApp messages with 500ms delay
|
||||
const debouncedWhatsAppProcessor = debounce(async (message) => {
|
||||
await whatsappProcessor.processMessage(message);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
whatsappAdapter.onMessage(debouncedWhatsAppProcessor);
|
||||
startEventsServer();
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,9 @@ export interface Attachment {
|
|||
contentType?: string;
|
||||
data?: Buffer | string;
|
||||
type?: string;
|
||||
mediaType?: 'image' | 'audio' | 'other';
|
||||
base64?: string;
|
||||
transcription?: string;
|
||||
}
|
||||
|
||||
export interface Embed {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { UserConfig } from "../config";
|
||||
import { Message, User } from "./message";
|
||||
import { Attachment, Message, User } from "./message";
|
||||
|
||||
export interface FetchOptions {
|
||||
limit?: number;
|
||||
|
@ -25,4 +25,9 @@ export interface PlatformAdapter {
|
|||
processing: boolean;
|
||||
};
|
||||
};
|
||||
handleMediaAttachment?(attachment: Attachment): Promise<{
|
||||
base64?: string;
|
||||
transcription?: string;
|
||||
mediaType: 'image' | 'audio' | 'other';
|
||||
}>;
|
||||
}
|
||||
|
|
|
@ -12,20 +12,17 @@ import {
|
|||
MessageMedia,
|
||||
} from "whatsapp-web.js";
|
||||
import { UserConfig, userConfigs } from "../config";
|
||||
import { eventManager } from "./events";
|
||||
// import { eventManager } from "./events";
|
||||
import { return_current_listeners } from "../tools/events";
|
||||
import Fuse from "fuse.js";
|
||||
|
||||
// const allowedUsers = ["pooja", "raj"];
|
||||
const allowedUsers: string[] = [];
|
||||
import { get_transcription } from "../tools/ask"; // Add this import
|
||||
|
||||
export class WhatsAppAdapter implements PlatformAdapter {
|
||||
private client: WAClient;
|
||||
private botUserId: string = "918884016724@c.us";
|
||||
|
||||
public config = {
|
||||
indicators: {
|
||||
typing: false,
|
||||
typing: true,
|
||||
processing: false,
|
||||
},
|
||||
};
|
||||
|
@ -39,6 +36,11 @@ export class WhatsAppAdapter implements PlatformAdapter {
|
|||
console.log("WhatsApp Client is ready!");
|
||||
});
|
||||
|
||||
this.client.on("qr", (qr) => {
|
||||
console.log("QR Code received. Please scan with WhatsApp:");
|
||||
console.log(qr);
|
||||
});
|
||||
|
||||
this.client.initialize();
|
||||
} catch (error) {
|
||||
console.log(`Failed to initialize WhatsApp client: `, error);
|
||||
|
@ -62,6 +64,8 @@ export class WhatsAppAdapter implements PlatformAdapter {
|
|||
|
||||
public onMessage(callback: (message: StdMessage) => void): void {
|
||||
this.client.on("message_create", async (waMessage: WAMessage) => {
|
||||
|
||||
|
||||
// emit internal event only if text message and there is an active listener
|
||||
const listeners = return_current_listeners();
|
||||
if (
|
||||
|
@ -69,16 +73,16 @@ export class WhatsAppAdapter implements PlatformAdapter {
|
|||
!waMessage.fromMe &&
|
||||
listeners.find((l) => l.eventId.includes("whatsapp"))
|
||||
) {
|
||||
const contact = await this.client.getContactById(waMessage.from);
|
||||
eventManager.emit("got_whatsapp_message", {
|
||||
sender_id: waMessage.from,
|
||||
sender_contact_name:
|
||||
contact.name || contact.shortName || contact.pushname || "NA",
|
||||
timestamp: waMessage.timestamp,
|
||||
content: waMessage.body,
|
||||
profile_image_url: await contact.getProfilePicUrl(),
|
||||
is_group_message: contact.isGroup.toString(),
|
||||
});
|
||||
// const contact = await this.client.getContactById(waMessage.from);
|
||||
// eventManager.emit("got_whatsapp_message", {
|
||||
// sender_id: waMessage.from,
|
||||
// sender_contact_name:
|
||||
// contact.name || contact.shortName || contact.pushname || "NA",
|
||||
// timestamp: waMessage.timestamp,
|
||||
// content: waMessage.body,
|
||||
// profile_image_url: await contact.getProfilePicUrl(),
|
||||
// is_group_message: contact.isGroup.toString(),
|
||||
// });
|
||||
}
|
||||
|
||||
// user must exist in userConfigs
|
||||
|
@ -88,14 +92,15 @@ export class WhatsAppAdapter implements PlatformAdapter {
|
|||
return;
|
||||
}
|
||||
|
||||
// user must be in allowedUsers
|
||||
if (!allowedUsers.includes(usr.name)) {
|
||||
// console.log(`User not allowed: ${usr.name}`);
|
||||
return;
|
||||
}
|
||||
// // user must be in allowedUsers
|
||||
// if (!allowedUsers.includes(usr.name)) {
|
||||
// console.log(`User not allowed: ${usr.name}`, allowedUsers);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Ignore messages sent by the bot
|
||||
if (waMessage.fromMe) return;
|
||||
|
||||
const message = await this.convertMessage(waMessage);
|
||||
|
||||
callback(message);
|
||||
|
@ -125,7 +130,7 @@ export class WhatsAppAdapter implements PlatformAdapter {
|
|||
}
|
||||
|
||||
public getBotId(): string {
|
||||
return this.botUserId;
|
||||
return this.client.info.wid.user;
|
||||
}
|
||||
|
||||
public async createMessageInterface(userId: string): Promise<StdMessage> {
|
||||
|
@ -280,6 +285,32 @@ export class WhatsAppAdapter implements PlatformAdapter {
|
|||
// Expose this method so it can be accessed elsewhere
|
||||
public getMessageInterface = this.createMessageInterface;
|
||||
|
||||
public async handleMediaAttachment(attachment: Attachment) {
|
||||
if (!attachment.data) return { mediaType: 'other' as const };
|
||||
|
||||
const buffer = Buffer.from(attachment.data as string, 'base64');
|
||||
|
||||
if (attachment.type?.includes('image')) {
|
||||
const base64 = `data:${attachment.contentType};base64,${buffer.toString('base64')}`;
|
||||
return {
|
||||
base64,
|
||||
mediaType: 'image' as const
|
||||
};
|
||||
}
|
||||
|
||||
if (attachment.type?.includes('audio')) {
|
||||
// Create temporary file for transcription
|
||||
const tempFile = new File([buffer], 'audio', { type: attachment.contentType });
|
||||
const transcription = await get_transcription(tempFile);
|
||||
return {
|
||||
transcription,
|
||||
mediaType: 'audio' as const
|
||||
};
|
||||
}
|
||||
|
||||
return { mediaType: 'other' as const };
|
||||
}
|
||||
|
||||
private async convertMessage(waMessage: WAMessage): Promise<StdMessage> {
|
||||
const contact = await waMessage.getContact();
|
||||
|
||||
|
@ -292,14 +323,25 @@ export class WhatsAppAdapter implements PlatformAdapter {
|
|||
// Convert attachments
|
||||
let attachments: Attachment[] = [];
|
||||
if (waMessage.hasMedia) {
|
||||
console.log("Downloading media...");
|
||||
const media = await waMessage.downloadMedia();
|
||||
|
||||
attachments.push({
|
||||
url: "", // WhatsApp does not provide a direct URL to the media
|
||||
const attachment: Attachment = {
|
||||
url: "",
|
||||
data: media.data,
|
||||
contentType: media.mimetype,
|
||||
type: waMessage.type,
|
||||
});
|
||||
type: waMessage.type
|
||||
};
|
||||
|
||||
console.log("Processing media attachment...");
|
||||
|
||||
const processedMedia = await this.handleMediaAttachment(attachment);
|
||||
console.log("Processed media attachment:", processedMedia.transcription);
|
||||
if (processedMedia.base64) attachment.base64 = processedMedia.base64;
|
||||
if (processedMedia.transcription) attachment.transcription = processedMedia.transcription;
|
||||
attachment.mediaType = processedMedia.mediaType;
|
||||
|
||||
attachments.push(attachment);
|
||||
}
|
||||
|
||||
const stdMessage: StdMessage = {
|
||||
|
@ -358,7 +400,7 @@ export class WhatsAppAdapter implements PlatformAdapter {
|
|||
user.identities.some(
|
||||
(identity) =>
|
||||
identity.platform === "whatsapp" &&
|
||||
identity.id === contact.id._serialized
|
||||
identity.id === contact.id.user
|
||||
)
|
||||
);
|
||||
return userConfig ? userConfig.roles : ["user"];
|
||||
|
@ -400,7 +442,11 @@ export class WhatsAppAdapter implements PlatformAdapter {
|
|||
sendTyping: async () => {
|
||||
// WhatsApp Web API does not support sending typing indicators directly
|
||||
// You may leave this as a no-op
|
||||
const chat = await this.client.getChatById(waMessage.from)
|
||||
await chat.sendStateTyping()
|
||||
|
||||
},
|
||||
attachments,
|
||||
};
|
||||
|
||||
return stdMessage;
|
||||
|
@ -540,6 +586,8 @@ export class WhatsAppAdapter implements PlatformAdapter {
|
|||
sendTyping: async () => {
|
||||
// WhatsApp Web API does not support sending typing indicators directly
|
||||
// You may leave this as a no-op
|
||||
const chat = await this.client.getChatById(sentWAMessage.from)
|
||||
await chat.sendStateTyping()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -19,6 +19,9 @@ import { memory_manager_guide, memory_manager_init } from "./memory-manager";
|
|||
// Paths to the JSON files
|
||||
const ACTIONS_FILE_PATH = pathInDataDir("actions.json");
|
||||
|
||||
// Add this constant at the top with other constants
|
||||
const MIN_SCHEDULE_INTERVAL_SECONDS = 600; // 10 minutes in seconds
|
||||
|
||||
// Define schema for creating an action
|
||||
export const CreateActionParams = z.object({
|
||||
actionId: z
|
||||
|
@ -347,6 +350,13 @@ export async function create_action(
|
|||
let { actionId, description, schedule, instruction, tool_names } =
|
||||
parsed.data;
|
||||
|
||||
// Validate schedule frequency
|
||||
if (!validateScheduleFrequency(schedule)) {
|
||||
return {
|
||||
error: "❌ Schedule frequency cannot be less than 10 minutes. Please adjust the schedule."
|
||||
};
|
||||
}
|
||||
|
||||
// Get the userId from contextMessage
|
||||
const userId: string = contextMessage.author.id;
|
||||
|
||||
|
@ -484,6 +494,13 @@ export async function update_action(
|
|||
notify,
|
||||
} = parsed.data;
|
||||
|
||||
// Validate schedule frequency
|
||||
if (!validateScheduleFrequency(schedule)) {
|
||||
return {
|
||||
error: "❌ Schedule frequency cannot be less than 10 minutes. Please adjust the schedule."
|
||||
};
|
||||
}
|
||||
|
||||
// Get the userId from contextMessage
|
||||
const userId: string = contextMessage.author.id;
|
||||
|
||||
|
@ -534,16 +551,25 @@ const action_tools: (
|
|||
schema: CreateActionParams,
|
||||
description: `Creates a new action.
|
||||
|
||||
**IMPORTANT SCHEDULING LIMITATION:**
|
||||
Actions CANNOT be scheduled more frequently than once every 10 minutes. This is a hard system limitation that cannot be overridden.
|
||||
- For delays: Minimum delay is 600 seconds (10 minutes)
|
||||
- For cron: Must have at least 10 minutes between executions
|
||||
|
||||
**Example:**
|
||||
- **User:** "Send a summary email in 10 minutes"
|
||||
- **User:** "Send a summary email every 10 minutes"
|
||||
- **Action ID:** "send_summary_email"
|
||||
- **Description:** "Sends a summary email after a delay."
|
||||
- **Schedule:** { type: "delay", time: 600 }
|
||||
- **Description:** "Sends a summary email periodically"
|
||||
- **Schedule:** { type: "cron", time: "*/10 * * * *" }
|
||||
- **Instruction:** "Compose and send a summary email to the user."
|
||||
- **Required Tools:** ["email_service"]
|
||||
|
||||
**Notes:**
|
||||
- Supported scheduling types: 'delay' (in seconds), 'cron' (cron expressions).
|
||||
**Invalid Examples:**
|
||||
❌ Every 5 minutes: "*/5 * * * *"
|
||||
❌ Delay of 300 seconds
|
||||
❌ Multiple times within 10 minutes
|
||||
|
||||
The system will automatically reject any schedule that attempts to run more frequently than every 10 minutes.
|
||||
`,
|
||||
}),
|
||||
// zodFunction({
|
||||
|
@ -710,5 +736,41 @@ Use the data provided above to fulfill the user's request.
|
|||
}
|
||||
}
|
||||
|
||||
// Replace the existing validateCronFrequency function with this more comprehensive one
|
||||
function validateScheduleFrequency(schedule: { type: "delay" | "cron"; time: number | string }): boolean {
|
||||
try {
|
||||
if (schedule.type === "delay") {
|
||||
const delaySeconds = schedule.time as number;
|
||||
return delaySeconds >= MIN_SCHEDULE_INTERVAL_SECONDS;
|
||||
} else if (schedule.type === "cron") {
|
||||
const cronExpression = schedule.time as string;
|
||||
const intervals = cronExpression.split(' ');
|
||||
|
||||
// Check minutes field (first position)
|
||||
const minutesPart = intervals[0];
|
||||
if (minutesPart === '*') return false;
|
||||
if (minutesPart.includes('/')) {
|
||||
const step = parseInt(minutesPart.split('/')[1]);
|
||||
if (step < 10) return false;
|
||||
}
|
||||
// Convert specific minute values to ensure they're at least 10 minutes apart
|
||||
if (!minutesPart.includes('/')) {
|
||||
const minutes = minutesPart.split(',').map(Number);
|
||||
if (minutes.length > 1) {
|
||||
minutes.sort((a, b) => a - b);
|
||||
for (let i = 1; i < minutes.length; i++) {
|
||||
if (minutes[i] - minutes[i - 1] < 10) return false;
|
||||
}
|
||||
if ((60 - minutes[minutes.length - 1] + minutes[0]) < 10) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize by loading actions from file when the module is loaded
|
||||
loadActionsFromFile();
|
||||
|
|
58
tools/ask.ts
58
tools/ask.ts
|
@ -133,12 +133,12 @@ export async function ask({
|
|||
tools,
|
||||
seed,
|
||||
json,
|
||||
image_url,
|
||||
image_urls, // Changed from image_url to image_urls array
|
||||
}: {
|
||||
model?: string;
|
||||
prompt: string;
|
||||
message?: string;
|
||||
image_url?: string;
|
||||
image_urls?: string[]; // Changed to array of strings
|
||||
name?: string;
|
||||
tools?: RunnableToolFunctionWithParse<any>[];
|
||||
seed?: string;
|
||||
|
@ -166,6 +166,22 @@ export async function ask({
|
|||
// Retrieve existing message history
|
||||
const history = getMessageHistory(seed);
|
||||
|
||||
let messageContent: any = message;
|
||||
if (image_urls && image_urls.length > 0) {
|
||||
messageContent = [
|
||||
{
|
||||
type: "text",
|
||||
text: message,
|
||||
},
|
||||
...image_urls.map((url) => ({
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: url,
|
||||
},
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
// Combine system prompt with message history and new user message
|
||||
messages = [
|
||||
{
|
||||
|
@ -175,24 +191,11 @@ export async function ask({
|
|||
...history,
|
||||
{
|
||||
role: "user",
|
||||
content: image_url
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message,
|
||||
},
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: image_url,
|
||||
},
|
||||
},
|
||||
]
|
||||
: message,
|
||||
content: messageContent,
|
||||
name,
|
||||
},
|
||||
];
|
||||
image_url && console.log("got image:", image_url?.slice(0, 20));
|
||||
image_urls && console.log("got images:", image_urls.length);
|
||||
} else if (seed && !message) {
|
||||
// If seed is provided but no new message, just retrieve history
|
||||
const history = getMessageHistory(seed);
|
||||
|
@ -205,22 +208,25 @@ export async function ask({
|
|||
];
|
||||
} else if (!seed && message) {
|
||||
// If no seed but message is provided, send system prompt and user message without history
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: image_url
|
||||
? [
|
||||
let messageContent: any = message;
|
||||
if (image_urls && image_urls.length > 0) {
|
||||
messageContent = [
|
||||
{
|
||||
type: "text",
|
||||
text: message,
|
||||
},
|
||||
{
|
||||
...image_urls.map((url) => ({
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: image_url,
|
||||
url: url,
|
||||
},
|
||||
},
|
||||
]
|
||||
: message,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: messageContent,
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,13 +17,13 @@ const CommunicationManagerSchema = z.object({
|
|||
.describe(
|
||||
"The platform you prefer to use, you can leave this empty to default to the current user's platform."
|
||||
),
|
||||
prefered_recipient_details: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
user_id: z.string().optional(),
|
||||
})
|
||||
.optional()
|
||||
.describe("Give these details only if you have them."),
|
||||
// prefered_recipient_details: z
|
||||
// .object({
|
||||
// name: z.string().optional(),
|
||||
// user_id: z.string().optional(),
|
||||
// })
|
||||
// .optional()
|
||||
// .describe("Give these details only if you have them."),
|
||||
});
|
||||
|
||||
export type CommunicationManager = z.infer<typeof CommunicationManagerSchema>;
|
||||
|
@ -70,7 +70,7 @@ export async function communication_manager(
|
|||
{
|
||||
request,
|
||||
prefered_platform,
|
||||
prefered_recipient_details,
|
||||
// prefered_recipient_details,
|
||||
}: CommunicationManager,
|
||||
context_message: Message
|
||||
) {
|
||||
|
@ -78,27 +78,61 @@ export async function communication_manager(
|
|||
memory_manager_init(context_message, "communications_manager")
|
||||
);
|
||||
|
||||
const prompt = `You are a Communication Manager Tool.
|
||||
const prompt = `You are a Communication Manager Tool responsible for routing messages to the correct recipients.
|
||||
|
||||
Your task is to route messages to the correct recipient.
|
||||
CONTEXT INFORMATION:
|
||||
1. Current User (Sender): ${context_message.author.config?.name}
|
||||
2. Current Platform: ${context_message.platform}
|
||||
3. WhatsApp Access: ${context_message.getUserRoles().includes("creator")}
|
||||
4. Available Platforms: discord, whatsapp, email
|
||||
|
||||
---
|
||||
STEP-BY-STEP PROCESS:
|
||||
1. First, identify the recipient(s) from the request
|
||||
2. Then, check if recipient exists in this list of known users:
|
||||
${JSON.stringify(userConfigs, null, 2)}
|
||||
|
||||
3. If recipient not found in above list:
|
||||
- Use search_user tool to find them
|
||||
- Wait for search results before proceeding
|
||||
|
||||
4. Platform Selection:
|
||||
- If prefered_platform is specified, use that
|
||||
- If not specified, use current platform: ${context_message.platform}
|
||||
- For WhatsApp, verify you have creator access first
|
||||
|
||||
TOOLS AVAILABLE:
|
||||
- search_user: Find user details by name
|
||||
- send_message_to: Send message on discord/whatsapp
|
||||
- send_email: Send emails (requires verified email address)
|
||||
- memory_manager: Store user preferences and contact names
|
||||
|
||||
${memory_manager_guide("communications_manager", context_message.author.id)}
|
||||
|
||||
You can use the \`memory_manager\` tool to remember user preferences, such as what the user calls certain contacts, to help you route messages better.
|
||||
MESSAGE DELIVERY GUIDELINES:
|
||||
Act as a professional assistant delivering messages between people. Consider:
|
||||
|
||||
---
|
||||
1. Relationship Context:
|
||||
- Professional for workplace communications
|
||||
- Casual for friends and family
|
||||
- Respectful for all contexts
|
||||
|
||||
**Default Platform (if not mentioned):** ${context_message.platform}
|
||||
2. Message Delivery Style:
|
||||
- Frame the message naturally as an assistant would when passing along information
|
||||
- Maintain the original intent and tone of the sender
|
||||
- Add appropriate context without changing the core message
|
||||
|
||||
**Configuration of All Users:** ${JSON.stringify(userConfigs)}
|
||||
3. Natural Communication:
|
||||
- Deliver messages as if you're the assistant of the user: ${context_message.author.config?.name}.
|
||||
- Adapt your tone based on the message urgency and importance
|
||||
- Include relevant context when delivering reminders or requests
|
||||
- Keep the human element in the communication
|
||||
|
||||
**Can Access 'WhatsApp':** ${context_message.getUserRoles().includes("creator")}
|
||||
Remember: You're not just forwarding messages, you're acting as a professional assistant helping facilitate communication between people. Make your delivery natural and appropriate for each situation.
|
||||
|
||||
**Guidelines:**
|
||||
|
||||
- If the user does not mention a platform, use the same platform as the current user.
|
||||
ERROR PREVENTION:
|
||||
- Don't halucinate or invent contact details
|
||||
- Always verify platform availability before sending
|
||||
- If unsure about recipient, ask for clarification
|
||||
`;
|
||||
|
||||
const response = await ask({
|
||||
|
@ -106,9 +140,7 @@ You can use the \`memory_manager\` tool to remember user preferences, such as wh
|
|||
model: "gpt-4o-mini",
|
||||
message: `request: ${request}
|
||||
|
||||
prefered_platform: ${prefered_platform}
|
||||
|
||||
prefered_recipient_details: ${JSON.stringify(prefered_recipient_details)}`,
|
||||
prefered_platform: ${prefered_platform}`,
|
||||
tools,
|
||||
});
|
||||
|
||||
|
@ -128,13 +160,11 @@ export const communication_manager_tool = (context_message: Message) =>
|
|||
function: (args) => communication_manager(args, context_message),
|
||||
name: "communication_manager",
|
||||
schema: CommunicationManagerSchema,
|
||||
description: `Communications Manager.
|
||||
description: `Sends messages to one or more recipients across different platforms (discord, whatsapp, email).
|
||||
|
||||
This tool routes messages to the specified user on the appropriate platform.
|
||||
Input format:
|
||||
request: "send [message] to [recipient(s)]"
|
||||
prefered_platform: (optional) platform name
|
||||
|
||||
Use it to send messages to users on various platforms.
|
||||
|
||||
Provide detailed information to ensure the message reaches the correct recipient.
|
||||
|
||||
Include in your request the full message content and its context along with the recipient's details.`,
|
||||
The tool handles recipient lookup, message composition, and delivery automatically.`,
|
||||
});
|
||||
|
|
|
@ -13,10 +13,24 @@ export interface PromptAugmentationResult {
|
|||
updatedSystemPrompt?: string;
|
||||
message?: string;
|
||||
updatedTools?: RunnableToolFunctionWithParse<any>[];
|
||||
attachedImageBase64?: string;
|
||||
attachedImagesBase64?: string[]; // Changed to string array
|
||||
model: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to handle transcription of a single file or array of files
|
||||
*/
|
||||
async function handleTranscription(
|
||||
input: File | File[]
|
||||
): Promise<string | string[]> {
|
||||
if (Array.isArray(input)) {
|
||||
return Promise.all(
|
||||
input.map((file) => get_transcription(file as globalThis.File))
|
||||
);
|
||||
}
|
||||
return get_transcription(input as globalThis.File);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1) Voice Event Augmentation
|
||||
* - Possibly do transcription if an audio File is present.
|
||||
|
@ -28,27 +42,103 @@ async function voiceEventAugmentation(
|
|||
baseTools: RunnableToolFunctionWithParse<any>[] | undefined,
|
||||
contextMessage: Message
|
||||
): Promise<PromptAugmentationResult> {
|
||||
let attachedImageBase64: string | undefined;
|
||||
let attachedImagesBase64: string[] = []; // Changed to array
|
||||
|
||||
// Transcribe if there's an audio file
|
||||
if (payload?.transcription && payload.transcription instanceof File) {
|
||||
console.log("Transcribing audio for voice event listener.");
|
||||
const file = payload.transcription;
|
||||
payload.transcription = await get_transcription(file as globalThis.File);
|
||||
// Handle transcription - single file or array
|
||||
if (payload?.transcription) {
|
||||
if (
|
||||
payload.transcription instanceof File ||
|
||||
Array.isArray(payload.transcription)
|
||||
) {
|
||||
console.log("Transcribing audio(s) for voice event listener.");
|
||||
payload.transcription = await handleTranscription(payload.transcription);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for an attached image
|
||||
// Handle other_reference_data - single file or array
|
||||
const otherContextData = payload?.other_reference_data;
|
||||
if (
|
||||
otherContextData instanceof File &&
|
||||
otherContextData.type.includes("image")
|
||||
) {
|
||||
console.log("Got image in voice event payload; converting to base64...");
|
||||
const buffer = await otherContextData.arrayBuffer();
|
||||
attachedImageBase64 = `data:${otherContextData.type};base64,${Buffer.from(
|
||||
if (otherContextData) {
|
||||
if (Array.isArray(otherContextData)) {
|
||||
const results = await Promise.all(
|
||||
otherContextData.map(async (item) => {
|
||||
if (item instanceof File) {
|
||||
if (item.type.includes("audio")) {
|
||||
return await get_transcription(item as globalThis.File);
|
||||
} else if (item.type.includes("image")) {
|
||||
const buffer = await item.arrayBuffer();
|
||||
const imageBase64 = `data:${item.type};base64,${Buffer.from(
|
||||
buffer
|
||||
).toString("base64")}`;
|
||||
attachedImagesBase64.push(imageBase64); // Add to array
|
||||
return imageBase64;
|
||||
}
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
payload.other_reference_data = results;
|
||||
} else if (otherContextData instanceof File) {
|
||||
if (otherContextData.type.includes("audio")) {
|
||||
payload.other_reference_data = await get_transcription(
|
||||
otherContextData as globalThis.File
|
||||
);
|
||||
} else if (otherContextData.type.includes("image")) {
|
||||
const buffer = await otherContextData.arrayBuffer();
|
||||
const imageBase64 = `data:${otherContextData.type};base64,${Buffer.from(
|
||||
buffer
|
||||
).toString("base64")}`;
|
||||
attachedImagesBase64.push(imageBase64); // Add to array
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const files = payload?.files;
|
||||
if (files) {
|
||||
if (Array.isArray(files)) {
|
||||
const results = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
if (file instanceof File) {
|
||||
if (file.type.includes("audio")) {
|
||||
const res = await get_transcription(file as globalThis.File);
|
||||
return `transcript of file: ${file.name}:
|
||||
|
||||
${res}
|
||||
`
|
||||
} else if (file.type.includes("image")) {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const imageBase64 = `data:${file.type};base64,${Buffer.from(
|
||||
buffer
|
||||
).toString("base64")}`;
|
||||
attachedImagesBase64.push(imageBase64); // Add to array
|
||||
return `image file: ${file.name}`;
|
||||
}
|
||||
}
|
||||
return file;
|
||||
})
|
||||
);
|
||||
payload.files = results;
|
||||
} else if (files instanceof File) {
|
||||
if (files.type.includes("audio")) {
|
||||
// payload.files = await get_transcription(files as globalThis.File);
|
||||
const res = await get_transcription(files as globalThis.File);
|
||||
payload.files = `transcript of file: ${files.name}:
|
||||
|
||||
${res}
|
||||
`;
|
||||
|
||||
} else if (files.type.includes("image")) {
|
||||
const buffer = await files.arrayBuffer();
|
||||
const imageBase64 = `data:${files.type};base64,${Buffer.from(
|
||||
buffer
|
||||
).toString("base64")}`;
|
||||
attachedImagesBase64.push(imageBase64); // Add to array
|
||||
payload.files = `image file: ${files.name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// console.log("Payload: ", payload);
|
||||
// console.log("IMages: ", attachedImagesBase64);
|
||||
|
||||
let message = `
|
||||
You are in voice trigger mode.
|
||||
|
@ -72,7 +162,7 @@ Your response must be in plain text without extra formatting or Markdown.
|
|||
updatedSystemPrompt: prompt,
|
||||
message,
|
||||
updatedTools: tools,
|
||||
attachedImageBase64,
|
||||
attachedImagesBase64, // Now returning array
|
||||
model: "gpt-4o",
|
||||
};
|
||||
}
|
||||
|
@ -147,7 +237,7 @@ async function defaultAugmentation(
|
|||
): Promise<PromptAugmentationResult> {
|
||||
return {
|
||||
updatedTools: baseTools,
|
||||
attachedImageBase64: undefined,
|
||||
attachedImagesBase64: [], // Changed to empty array
|
||||
model: "gpt-4o-mini",
|
||||
};
|
||||
}
|
||||
|
@ -187,7 +277,7 @@ export async function buildPromptAndToolsForEvent(
|
|||
finalPrompt: string;
|
||||
message?: string;
|
||||
finalTools: RunnableToolFunctionWithParse<any>[] | undefined;
|
||||
attachedImage?: string;
|
||||
attachedImages?: string[];
|
||||
model?: string;
|
||||
}> {
|
||||
console.log(`Building prompt for event: ${eventId}`);
|
||||
|
@ -227,8 +317,7 @@ You are called when an event triggers. Your task is to execute the user's instru
|
|||
- **Event ID:** ${eventId}
|
||||
- **Description:** ${description}
|
||||
- **Payload:** ${JSON.stringify(payload, null, 2)}
|
||||
- **Will Auto Notify Creator of Listener:** ${
|
||||
notify
|
||||
- **Will Auto Notify Creator of Listener:** ${notify
|
||||
? "Yes, no need to send it yourself"
|
||||
: "No, you need to notify the user manually"
|
||||
}
|
||||
|
@ -257,7 +346,7 @@ You are called when an event triggers. Your task is to execute the user's instru
|
|||
const {
|
||||
additionalSystemPrompt,
|
||||
updatedTools,
|
||||
attachedImageBase64,
|
||||
attachedImagesBase64,
|
||||
updatedSystemPrompt,
|
||||
model,
|
||||
message,
|
||||
|
@ -271,7 +360,7 @@ You are called when an event triggers. Your task is to execute the user's instru
|
|||
return {
|
||||
finalPrompt: updatedSystemPrompt || finalPrompt,
|
||||
finalTools: updatedTools,
|
||||
attachedImage: attachedImageBase64,
|
||||
attachedImages: attachedImagesBase64,
|
||||
model,
|
||||
message,
|
||||
};
|
||||
|
|
|
@ -434,7 +434,7 @@ function registerListener(listener: EventListener) {
|
|||
|
||||
console.time("buildPromptAndToolsForEvent");
|
||||
// Now call the helper from the new file
|
||||
const { finalPrompt, finalTools, attachedImage, model, message } =
|
||||
const { finalPrompt, finalTools, attachedImages, model, message } =
|
||||
await buildPromptAndToolsForEvent(
|
||||
eventId,
|
||||
description,
|
||||
|
@ -457,7 +457,7 @@ function registerListener(listener: EventListener) {
|
|||
model: model,
|
||||
message,
|
||||
prompt: finalPrompt,
|
||||
image_url: attachedImage, // If there's an attached image base64
|
||||
image_urls: attachedImages, // If there's an attached image base64
|
||||
seed: `${eventId}-${listener.id}`,
|
||||
tools: finalTools,
|
||||
});
|
||||
|
|
|
@ -148,6 +148,8 @@ export function getTools(
|
|||
) {
|
||||
const userRoles = context_message.getUserRoles();
|
||||
|
||||
console.log("User roles: ", userRoles);
|
||||
|
||||
// Aggregate permissions from all roles
|
||||
const userPermissions = new Set<string>();
|
||||
userRoles.forEach((role) => {
|
||||
|
|
|
@ -13,80 +13,59 @@ export const IssueParams = z.object({
|
|||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
assigneeId: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
priority: z.number().optional(),
|
||||
labelIds: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const UpdateIssueParams = z.object({
|
||||
issueId: z.string().describe("The ID of the issue to update"),
|
||||
// Basic fields
|
||||
title: z.string().optional().describe("The issue title"),
|
||||
description: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The issue description in markdown format"),
|
||||
stateId: z.string().optional().describe("The team state/status of the issue"),
|
||||
assigneeId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The identifier of the user to assign the issue to"),
|
||||
priority: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(4)
|
||||
.optional()
|
||||
.describe(
|
||||
"The priority of the issue. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low"
|
||||
),
|
||||
addedLabelIds: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
description: z.string().optional().describe("The issue description in markdown format"),
|
||||
descriptionData: z.any().optional().describe("The issue description as a Prosemirror document"),
|
||||
priority: z.number().min(0).max(4).optional()
|
||||
.describe("The priority of the issue. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low"),
|
||||
|
||||
// Assignee and subscribers
|
||||
assigneeId: z.string().optional().describe("The identifier of the user to assign the issue to"),
|
||||
subscriberIds: z.array(z.string()).optional().describe("The identifiers of the users subscribing to this ticket"),
|
||||
|
||||
// Labels
|
||||
labelIds: z.array(z.string()).optional()
|
||||
.describe("The complete set of label IDs to set on the issue (replaces existing labels)"),
|
||||
addedLabelIds: z.array(z.string()).optional()
|
||||
.describe("The identifiers of the issue labels to be added to this issue"),
|
||||
removedLabelIds: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
"The identifiers of the issue labels to be removed from this issue"
|
||||
),
|
||||
labelIds: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
"The complete set of label IDs to set on the issue (replaces existing labels)"
|
||||
),
|
||||
autoClosedByParentClosing: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
"Whether the issue was automatically closed because its parent issue was closed"
|
||||
),
|
||||
boardOrder: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("The position of the issue in its column on the board view"),
|
||||
dueDate: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The date at which the issue is due (TimelessDate format)"),
|
||||
parentId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The identifier of the parent issue"),
|
||||
projectId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The project associated with the issue"),
|
||||
sortOrder: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("The position of the issue related to other issues"),
|
||||
subIssueSortOrder: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("The position of the issue in parent's sub-issue list"),
|
||||
teamId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The identifier of the team associated with the issue"),
|
||||
removedLabelIds: z.array(z.string()).optional()
|
||||
.describe("The identifiers of the issue labels to be removed from this issue"),
|
||||
|
||||
// Status and workflow
|
||||
stateId: z.string().optional().describe("The team state of the issue"),
|
||||
estimate: z.number().optional().describe("The estimated complexity of the issue"),
|
||||
|
||||
// Dates and scheduling
|
||||
dueDate: z.string().optional().describe("The date at which the issue is due (YYYY-MM-DD format)"),
|
||||
snoozedById: z.string().optional().describe("The identifier of the user who snoozed the issue"),
|
||||
snoozedUntilAt: z.string().optional().describe("The time until an issue will be snoozed in Triage view"),
|
||||
|
||||
// Relationships
|
||||
parentId: z.string().optional().describe("The identifier of the parent issue"),
|
||||
projectId: z.string().optional().describe("The project associated with the issue"),
|
||||
projectMilestoneId: z.string().optional().describe("The project milestone associated with the issue"),
|
||||
teamId: z.string().optional().describe("The identifier of the team associated with the issue"),
|
||||
cycleId: z.string().optional().describe("The cycle associated with the issue"),
|
||||
|
||||
// Sorting and positioning
|
||||
sortOrder: z.number().optional().describe("The position of the issue related to other issues"),
|
||||
boardOrder: z.number().optional().describe("The position of the issue in its column on the board view"),
|
||||
subIssueSortOrder: z.number().optional().describe("The position of the issue in parent's sub-issue list"),
|
||||
prioritySortOrder: z.number().optional().describe("[ALPHA] The position of the issue when ordered by priority"),
|
||||
|
||||
// Templates and automation
|
||||
lastAppliedTemplateId: z.string().optional().describe("The ID of the last template applied to the issue"),
|
||||
autoClosedByParentClosing: z.boolean().optional()
|
||||
.describe("Whether the issue was automatically closed because its parent issue was closed"),
|
||||
trashed: z.boolean().optional().describe("Whether the issue has been trashed"),
|
||||
});
|
||||
|
||||
export const GetIssueParams = z.object({
|
||||
|
@ -103,31 +82,134 @@ export const ListTeamsParams = z.object({
|
|||
limit: z.number().max(20).describe("Number of teams to return (default 3)"),
|
||||
});
|
||||
|
||||
export const AdvancedSearchIssuesParams = z.object({
|
||||
query: z.string().optional(),
|
||||
teamId: z.string().optional(),
|
||||
assigneeId: z.string().optional(),
|
||||
status: z
|
||||
.enum(["backlog", "todo", "in_progress", "done", "canceled"])
|
||||
.optional(),
|
||||
priority: z.number().min(0).max(4).optional(),
|
||||
orderBy: z
|
||||
.enum(["createdAt", "updatedAt"])
|
||||
.optional()
|
||||
.describe("Order by, defaults to updatedAt"),
|
||||
limit: z
|
||||
.number()
|
||||
.max(10)
|
||||
.describe("Number of results to return (default: 5)"),
|
||||
// Add these type definitions before the parameter schemas
|
||||
export const StringComparator = z.object({
|
||||
eq: z.string().optional(),
|
||||
neq: z.string().optional(),
|
||||
in: z.array(z.string()).optional(),
|
||||
nin: z.array(z.string()).optional(),
|
||||
contains: z.string().optional(),
|
||||
notContains: z.string().optional(),
|
||||
startsWith: z.string().optional(),
|
||||
notStartsWith: z.string().optional(),
|
||||
endsWith: z.string().optional(),
|
||||
notEndsWith: z.string().optional(),
|
||||
containsIgnoreCase: z.string().optional(),
|
||||
notContainsIgnoreCase: z.string().optional(),
|
||||
startsWithIgnoreCase: z.string().optional(),
|
||||
notStartsWithIgnoreCase: z.string().optional(),
|
||||
endsWithIgnoreCase: z.string().optional(),
|
||||
notEndsWithIgnoreCase: z.string().optional(),
|
||||
});
|
||||
|
||||
export const DateComparator = z.object({
|
||||
eq: z.string().optional(),
|
||||
neq: z.string().optional(),
|
||||
gt: z.string().optional(),
|
||||
gte: z.string().optional(),
|
||||
lt: z.string().optional(),
|
||||
lte: z.string().optional(),
|
||||
in: z.array(z.string()).optional(),
|
||||
nin: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const NumberComparator = z.object({
|
||||
eq: z.number().optional(),
|
||||
neq: z.number().optional(),
|
||||
gt: z.number().optional(),
|
||||
gte: z.number().optional(),
|
||||
lt: z.number().optional(),
|
||||
lte: z.number().optional(),
|
||||
in: z.array(z.number()).optional(),
|
||||
nin: z.array(z.number()).optional(),
|
||||
});
|
||||
|
||||
export const IdComparator = z.object({
|
||||
eq: z.string().optional(),
|
||||
neq: z.string().optional(),
|
||||
in: z.array(z.string()).optional(),
|
||||
nin: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const WorkflowStateFilter = z.object({
|
||||
createdAt: DateComparator.optional(),
|
||||
description: StringComparator.optional(),
|
||||
id: IdComparator.optional(),
|
||||
name: StringComparator.optional(),
|
||||
position: NumberComparator.optional(),
|
||||
type: StringComparator.optional(),
|
||||
updatedAt: DateComparator.optional(),
|
||||
});
|
||||
|
||||
export const AdvancedSearchIssuesParams = z.object({
|
||||
// Text search
|
||||
query: z.string().optional().describe("Search in title and description"),
|
||||
title: z.string().optional().describe("Filter by exact or partial title match"),
|
||||
description: z.string().optional().describe("Filter by description content"),
|
||||
|
||||
// Basic filters
|
||||
teamId: z.string().optional().describe("Filter by team ID"),
|
||||
assigneeId: z.string().optional().describe("Filter by assignee user ID"),
|
||||
creatorId: z.string().optional().describe("Filter by creator user ID"),
|
||||
priority: z.number().min(0).max(4).optional()
|
||||
.describe("0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low"),
|
||||
|
||||
// Status and state
|
||||
stateId: z.string().optional().describe("Filter by specific workflow state ID (simplified)"),
|
||||
|
||||
// Dates
|
||||
createdAfter: z.string().optional().describe("Issues created after this ISO datetime"),
|
||||
createdBefore: z.string().optional().describe("Issues created before this ISO datetime"),
|
||||
updatedAfter: z.string().optional().describe("Issues updated after this ISO datetime"),
|
||||
updatedBefore: z.string().optional().describe("Issues updated before this ISO datetime"),
|
||||
completedAfter: z.string().optional().describe("Issues completed after this ISO datetime"),
|
||||
completedBefore: z.string().optional().describe("Issues completed before this ISO datetime"),
|
||||
dueDate: z.string().optional().describe("Filter by due date (YYYY-MM-DD format)"),
|
||||
dueDateAfter: z.string().optional().describe("Due date after (YYYY-MM-DD format)"),
|
||||
dueDateBefore: z.string().optional().describe("Due date before (YYYY-MM-DD format)"),
|
||||
startedAfter: z.string().optional().describe("Issues started after this ISO datetime"),
|
||||
startedBefore: z.string().optional().describe("Issues started before this ISO datetime"),
|
||||
|
||||
// Relationships
|
||||
projectId: z.string().optional().describe("Filter by project ID"),
|
||||
parentId: z.string().optional().describe("Filter by parent issue ID"),
|
||||
subscriberId: z.string().optional().describe("Filter by subscriber user ID"),
|
||||
hasBlockedBy: z.boolean().optional().describe("Issues that are blocked by others"),
|
||||
hasBlocking: z.boolean().optional().describe("Issues that are blocking others"),
|
||||
hasDuplicates: z.boolean().optional().describe("Issues that have duplicates"),
|
||||
|
||||
// Labels and estimates
|
||||
labelIds: z.array(z.string()).optional().describe("Filter by one or more label IDs"),
|
||||
estimate: z.number().optional().describe("Filter by issue estimate points"),
|
||||
|
||||
// Other filters
|
||||
number: z.number().optional().describe("Filter by issue number"),
|
||||
snoozedById: z.string().optional().describe("Filter by user who snoozed the issue"),
|
||||
snoozedUntilAfter: z.string().optional().describe("Issues snoozed until after this ISO datetime"),
|
||||
snoozedUntilBefore: z.string().optional().describe("Issues snoozed until before this ISO datetime"),
|
||||
|
||||
// Result options
|
||||
orderBy: z.enum(["createdAt", "updatedAt", "priority", "dueDate"])
|
||||
.optional()
|
||||
.describe("Sort order for results"),
|
||||
limit: z.number().max(10)
|
||||
.describe("Number of results to return (default: 2, max: 10)"),
|
||||
});
|
||||
|
||||
// Modify SearchUsersParams schema to allow more specific search parameters
|
||||
export const SearchUsersParams = z.object({
|
||||
query: z.string().describe("Search query for user names"),
|
||||
email: z.string().optional().describe("Search by exact email address"),
|
||||
displayName: z.string().optional().describe("Search by display name"),
|
||||
limit: z
|
||||
.number()
|
||||
.max(10)
|
||||
.describe("Number of results to return (default: 5)"),
|
||||
});
|
||||
}).refine(
|
||||
data => (data.email && !data.displayName) || (!data.email && data.displayName),
|
||||
{
|
||||
message: "Provide either email OR displayName, not both"
|
||||
}
|
||||
);
|
||||
|
||||
// Add new Project Parameter Schemas
|
||||
export const ProjectParams = z.object({
|
||||
|
@ -207,9 +289,45 @@ export const GetProjectParams = z.object({
|
|||
});
|
||||
|
||||
export const SearchProjectsParams = z.object({
|
||||
query: z.string().describe("Search query string"),
|
||||
teamId: z.string().optional(),
|
||||
limit: z.number().max(5).describe("Number of results to return (default: 1)"),
|
||||
// Text search
|
||||
query: z.string().optional().describe("Search in project name and content"),
|
||||
name: z.string().optional().describe("Filter by exact or partial project name"),
|
||||
|
||||
// Basic filters
|
||||
teamId: z.string().optional().describe("Filter by team ID"),
|
||||
creatorId: z.string().optional().describe("Filter by creator user ID"),
|
||||
leadId: z.string().optional().describe("Filter by lead user ID"),
|
||||
priority: z.number().min(0).max(4).optional()
|
||||
.describe("0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low"),
|
||||
|
||||
// Status and state
|
||||
health: z.string().optional().describe("Filter by project health status"),
|
||||
state: z.string().optional().describe("[DEPRECATED] Filter by project state"),
|
||||
status: z.string().optional().describe("Filter by project status ID"),
|
||||
|
||||
// Dates
|
||||
startDate: z.string().optional().describe("Filter by start date"),
|
||||
targetDate: z.string().optional().describe("Filter by target date"),
|
||||
createdAfter: z.string().optional().describe("Projects created after this ISO datetime"),
|
||||
createdBefore: z.string().optional().describe("Projects created before this ISO datetime"),
|
||||
updatedAfter: z.string().optional().describe("Projects updated after this ISO datetime"),
|
||||
updatedBefore: z.string().optional().describe("Projects updated before this ISO datetime"),
|
||||
completedAfter: z.string().optional().describe("Projects completed after this ISO datetime"),
|
||||
completedBefore: z.string().optional().describe("Projects completed before this ISO datetime"),
|
||||
canceledAfter: z.string().optional().describe("Projects canceled after this ISO datetime"),
|
||||
canceledBefore: z.string().optional().describe("Projects canceled before this ISO datetime"),
|
||||
|
||||
// Relationships
|
||||
hasBlockedBy: z.boolean().optional().describe("Projects that are blocked by others"),
|
||||
hasBlocking: z.boolean().optional().describe("Projects that are blocking others"),
|
||||
hasRelated: z.boolean().optional().describe("Projects that have related items"),
|
||||
hasViolatedDependencies: z.boolean().optional().describe("Projects with violated dependencies"),
|
||||
|
||||
// Result options
|
||||
orderBy: z.enum(["createdAt", "updatedAt", "priority", "targetDate"])
|
||||
.optional()
|
||||
.describe("Sort order for results"),
|
||||
limit: z.number().max(10).describe("Number of results to return (default: 1)"),
|
||||
});
|
||||
|
||||
// Add new ListProjectsParams schema after other params
|
||||
|
@ -225,10 +343,49 @@ export const ListProjectsParams = z.object({
|
|||
.describe("Filter projects by state"),
|
||||
});
|
||||
|
||||
// Add after other parameter schemas
|
||||
export const CreateCommentParams = z.object({
|
||||
issueId: z.string().describe("The ID of the issue to comment on"),
|
||||
body: z.string().describe("The comment text in markdown format"),
|
||||
});
|
||||
|
||||
export const ListCommentsParams = z.object({
|
||||
issueId: z.string().describe("The ID of the issue to get comments from"),
|
||||
limit: z.number().max(20).describe("Number of comments to return (default: 10)"),
|
||||
});
|
||||
|
||||
// Add new document parameter schemas
|
||||
export const CreateDocumentParams = z.object({
|
||||
title: z.string().describe("The title of the document"),
|
||||
content: z.string().describe("The content of the document in markdown format"),
|
||||
icon: z.string().optional().describe("The icon of the document"),
|
||||
organizationId: z.string().optional().describe("The organization ID"),
|
||||
projectId: z.string().optional().describe("The project ID to link the document to"),
|
||||
});
|
||||
|
||||
export const UpdateDocumentParams = z.object({
|
||||
documentId: z.string().describe("The ID of the document to update"),
|
||||
title: z.string().optional().describe("The new title of the document"),
|
||||
content: z.string().optional().describe("The new content in markdown format"),
|
||||
icon: z.string().optional().describe("The new icon of the document"),
|
||||
});
|
||||
|
||||
export const GetDocumentParams = z.object({
|
||||
documentId: z.string().describe("The ID of the document to retrieve"),
|
||||
});
|
||||
|
||||
export const SearchDocumentsParams = z.object({
|
||||
query: z.string().describe("Search query string"),
|
||||
projectId: z.string().optional().describe("Filter by project ID"),
|
||||
limit: z.number().max(10).describe("Number of results to return (default: 5)"),
|
||||
});
|
||||
|
||||
interface SimpleIssue {
|
||||
id: string;
|
||||
id: string; // The internal UUID of the issue (e.g., "123e4567-e89b-12d3-a456-426614174000")
|
||||
identifier: string; // The human-readable identifier (e.g., "XCE-205")
|
||||
title: string;
|
||||
status: string;
|
||||
statusId: string;
|
||||
priority: number;
|
||||
assignee?: string;
|
||||
dueDate?: string;
|
||||
|
@ -265,15 +422,42 @@ interface SimpleProject {
|
|||
statusId?: string;
|
||||
}
|
||||
|
||||
// Add new document interface
|
||||
interface SimpleDocument {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
icon?: string;
|
||||
url: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// Add after other interfaces
|
||||
interface SimpleComment {
|
||||
id: string;
|
||||
body: string;
|
||||
user?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
function formatIssue(issue: any): SimpleIssue {
|
||||
return {
|
||||
id: issue.id,
|
||||
identifier: issue.identifier,
|
||||
title: issue.title,
|
||||
status: issue.state?.name || "Unknown",
|
||||
statusId: issue.state?.id,
|
||||
priority: issue.priority,
|
||||
assignee: issue.assignee?.name,
|
||||
dueDate: issue.dueDate,
|
||||
labels: issue.labels?.nodes?.map((l: any) => l.name) || [],
|
||||
labels: issue.labels?.nodes?.map((l: any) => ({
|
||||
name: l.name,
|
||||
id: l.id
|
||||
})) || [],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -296,6 +480,32 @@ function formatProject(project: any): SimpleProject {
|
|||
};
|
||||
}
|
||||
|
||||
// Add new document formatting function
|
||||
function formatDocument(doc: any): SimpleDocument {
|
||||
return {
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
content: doc.content,
|
||||
icon: doc.icon,
|
||||
url: doc.url,
|
||||
createdAt: doc.createdAt,
|
||||
updatedAt: doc.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
// Add after other formatting functions
|
||||
function formatComment(comment: any): SimpleComment {
|
||||
return {
|
||||
id: comment.id,
|
||||
body: comment.body,
|
||||
user: comment.user ? {
|
||||
id: comment.user.id,
|
||||
name: comment.user.name,
|
||||
} : undefined,
|
||||
createdAt: comment.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
// API Functions
|
||||
async function createIssue(
|
||||
client: LinearClient,
|
||||
|
@ -314,7 +524,29 @@ async function updateIssue(
|
|||
) {
|
||||
try {
|
||||
const { issueId, ...updateData } = params;
|
||||
return await client.updateIssue(issueId, updateData);
|
||||
|
||||
// Create a new object for the properly typed data
|
||||
const formattedData: any = { ...updateData };
|
||||
|
||||
// Convert date strings to proper format if provided
|
||||
if (formattedData.dueDate) {
|
||||
// Due date should be YYYY-MM-DD format
|
||||
formattedData.dueDate = formattedData.dueDate.split('T')[0];
|
||||
}
|
||||
|
||||
if (formattedData.snoozedUntilAt) {
|
||||
// Convert to Date object for the API
|
||||
formattedData.snoozedUntilAt = new Date(formattedData.snoozedUntilAt);
|
||||
}
|
||||
|
||||
// Remove any undefined values to avoid API errors
|
||||
Object.keys(formattedData).forEach(key => {
|
||||
if (formattedData[key] === undefined) {
|
||||
delete formattedData[key];
|
||||
}
|
||||
});
|
||||
|
||||
return await client.updateIssue(issueId, formattedData);
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
|
@ -380,21 +612,67 @@ async function advancedSearchIssues(
|
|||
) {
|
||||
try {
|
||||
const filter: any = {};
|
||||
if (params.teamId) filter.team = { id: { eq: params.teamId } };
|
||||
if (params.assigneeId) filter.assignee = { id: { eq: params.assigneeId } };
|
||||
if (params.status) filter.state = { type: { eq: params.status } };
|
||||
if (params.priority) filter.priority = { eq: params.priority };
|
||||
|
||||
// Text search filters
|
||||
if (params.query) {
|
||||
filter.or = [
|
||||
{ title: { containsIgnoreCase: params.query } },
|
||||
{ description: { containsIgnoreCase: params.query } },
|
||||
];
|
||||
}
|
||||
if (params.title) filter.title = { containsIgnoreCase: params.title };
|
||||
if (params.description) filter.description = { containsIgnoreCase: params.description };
|
||||
|
||||
// Basic filters
|
||||
if (params.teamId) filter.team = { id: { eq: params.teamId } };
|
||||
if (params.assigneeId) filter.assignee = { id: { eq: params.assigneeId } };
|
||||
if (params.creatorId) filter.creator = { id: { eq: params.creatorId } };
|
||||
if (params.priority !== undefined) filter.priority = { eq: params.priority };
|
||||
|
||||
// Status and state
|
||||
if (params.stateId) {
|
||||
filter.state = { id: { eq: params.stateId } };
|
||||
}
|
||||
|
||||
// Date filters
|
||||
if (params.createdAfter) filter.createdAt = { gt: params.createdAfter };
|
||||
if (params.createdBefore) filter.createdAt = { lt: params.createdBefore };
|
||||
if (params.updatedAfter) filter.updatedAt = { gt: params.updatedAfter };
|
||||
if (params.updatedBefore) filter.updatedAt = { lt: params.updatedBefore };
|
||||
if (params.completedAfter) filter.completedAt = { gt: params.completedAfter };
|
||||
if (params.completedBefore) filter.completedAt = { lt: params.completedBefore };
|
||||
if (params.startedAfter) filter.startedAt = { gt: params.startedAfter };
|
||||
if (params.startedBefore) filter.startedAt = { lt: params.startedBefore };
|
||||
|
||||
// Due date filters
|
||||
if (params.dueDate) filter.dueDate = { eq: params.dueDate };
|
||||
if (params.dueDateAfter) filter.dueDate = { gt: params.dueDateAfter };
|
||||
if (params.dueDateBefore) filter.dueDate = { lt: params.dueDateBefore };
|
||||
|
||||
// Relationship filters
|
||||
if (params.projectId) filter.project = { id: { eq: params.projectId } };
|
||||
if (params.parentId) filter.parent = { id: { eq: params.parentId } };
|
||||
if (params.subscriberId) filter.subscribers = { some: { id: { eq: params.subscriberId } } };
|
||||
if (params.hasBlockedBy) filter.hasBlockedByRelations = { eq: true };
|
||||
if (params.hasBlocking) filter.hasBlockingRelations = { eq: true };
|
||||
if (params.hasDuplicates) filter.hasDuplicateRelations = { eq: true };
|
||||
|
||||
// Labels
|
||||
if (params.labelIds?.length) {
|
||||
filter.labels = { some: { id: { in: params.labelIds } } };
|
||||
}
|
||||
|
||||
// Other filters
|
||||
if (params.estimate !== undefined) filter.estimate = { eq: params.estimate };
|
||||
if (params.number !== undefined) filter.number = { eq: params.number };
|
||||
if (params.snoozedById) filter.snoozedBy = { id: { eq: params.snoozedById } };
|
||||
if (params.snoozedUntilAfter) filter.snoozedUntilAt = { gt: params.snoozedUntilAfter };
|
||||
if (params.snoozedUntilBefore) filter.snoozedUntilAt = { lt: params.snoozedUntilBefore };
|
||||
|
||||
const issues = await client.issues({
|
||||
first: params.limit,
|
||||
filter,
|
||||
orderBy: params.orderBy || ("updatedAt" as any),
|
||||
orderBy: params.orderBy || "updatedAt" as any,
|
||||
});
|
||||
|
||||
return issues.nodes.map(formatIssue);
|
||||
|
@ -403,20 +681,23 @@ async function advancedSearchIssues(
|
|||
}
|
||||
}
|
||||
|
||||
// Modify searchUsers function to allow more specific search parameters
|
||||
async function searchUsers(
|
||||
client: LinearClient,
|
||||
{ query, limit }: z.infer<typeof SearchUsersParams>
|
||||
params: z.infer<typeof SearchUsersParams>
|
||||
) {
|
||||
try {
|
||||
let filter: any = {};
|
||||
|
||||
if (params.email) {
|
||||
filter = { email: { eq: params.email } };
|
||||
} else if (params.displayName) {
|
||||
filter = { displayName: { containsIgnoreCase: params.displayName } };
|
||||
}
|
||||
|
||||
const users = await client.users({
|
||||
filter: {
|
||||
or: [
|
||||
{ name: { containsIgnoreCase: query } },
|
||||
{ displayName: { containsIgnoreCase: query } },
|
||||
{ email: { containsIgnoreCase: query } },
|
||||
],
|
||||
},
|
||||
first: limit,
|
||||
filter,
|
||||
first: params.limit,
|
||||
});
|
||||
|
||||
return users.nodes.map(
|
||||
|
@ -471,25 +752,99 @@ async function getProject(
|
|||
// Modify searchProjects function to handle empty queries
|
||||
async function searchProjects(
|
||||
client: LinearClient,
|
||||
{ query, teamId, limit }: z.infer<typeof SearchProjectsParams>
|
||||
params: z.infer<typeof SearchProjectsParams>
|
||||
) {
|
||||
try {
|
||||
const searchParams: any = { first: limit };
|
||||
const filter: any = {};
|
||||
|
||||
if (teamId) {
|
||||
filter.team = { id: { eq: teamId } };
|
||||
// Text search filters
|
||||
if (params.query) {
|
||||
filter.or = [
|
||||
{ name: { containsIgnoreCase: params.query } },
|
||||
{ searchableContent: { contains: params.query } }
|
||||
];
|
||||
}
|
||||
if (params.name) {
|
||||
filter.name = { containsIgnoreCase: params.name };
|
||||
}
|
||||
|
||||
if (query) {
|
||||
filter.or = [{ name: { containsIgnoreCase: query } }];
|
||||
// Basic filters
|
||||
if (params.teamId) {
|
||||
filter.accessibleTeams = { some: { id: { eq: params.teamId } } };
|
||||
}
|
||||
if (params.creatorId) {
|
||||
filter.creator = { id: { eq: params.creatorId } };
|
||||
}
|
||||
if (params.leadId) {
|
||||
filter.lead = { id: { eq: params.leadId } };
|
||||
}
|
||||
if (params.priority !== undefined) {
|
||||
filter.priority = { eq: params.priority };
|
||||
}
|
||||
|
||||
if (Object.keys(filter).length > 0) {
|
||||
searchParams.filter = filter;
|
||||
// Status and state filters
|
||||
if (params.health) {
|
||||
filter.health = { eq: params.health };
|
||||
}
|
||||
if (params.state) {
|
||||
filter.state = { eq: params.state };
|
||||
}
|
||||
if (params.status) {
|
||||
filter.status = { id: { eq: params.status } };
|
||||
}
|
||||
|
||||
const projects = await client.projects(searchParams);
|
||||
// Date filters
|
||||
if (params.startDate) {
|
||||
filter.startDate = { eq: params.startDate };
|
||||
}
|
||||
if (params.targetDate) {
|
||||
filter.targetDate = { eq: params.targetDate };
|
||||
}
|
||||
if (params.createdAfter || params.createdBefore) {
|
||||
filter.createdAt = {
|
||||
...(params.createdAfter && { gt: params.createdAfter }),
|
||||
...(params.createdBefore && { lt: params.createdBefore })
|
||||
};
|
||||
}
|
||||
if (params.updatedAfter || params.updatedBefore) {
|
||||
filter.updatedAt = {
|
||||
...(params.updatedAfter && { gt: params.updatedAfter }),
|
||||
...(params.updatedBefore && { lt: params.updatedBefore })
|
||||
};
|
||||
}
|
||||
if (params.completedAfter || params.completedBefore) {
|
||||
filter.completedAt = {
|
||||
...(params.completedAfter && { gt: params.completedAfter }),
|
||||
...(params.completedBefore && { lt: params.completedBefore })
|
||||
};
|
||||
}
|
||||
if (params.canceledAfter || params.canceledBefore) {
|
||||
filter.canceledAt = {
|
||||
...(params.canceledAfter && { gt: params.canceledAfter }),
|
||||
...(params.canceledBefore && { lt: params.canceledBefore })
|
||||
};
|
||||
}
|
||||
|
||||
// Relationship filters
|
||||
if (params.hasBlockedBy) {
|
||||
filter.hasBlockedByRelations = { eq: true };
|
||||
}
|
||||
if (params.hasBlocking) {
|
||||
filter.hasBlockingRelations = { eq: true };
|
||||
}
|
||||
if (params.hasRelated) {
|
||||
filter.hasRelatedRelations = { eq: true };
|
||||
}
|
||||
if (params.hasViolatedDependencies) {
|
||||
filter.hasViolatedRelations = { eq: true };
|
||||
}
|
||||
|
||||
const projects = await client.projects({
|
||||
first: params.limit,
|
||||
filter,
|
||||
orderBy: params.orderBy || "updatedAt" as any,
|
||||
});
|
||||
|
||||
return projects.nodes.map(formatProject);
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
|
@ -524,6 +879,106 @@ async function listProjects(
|
|||
}
|
||||
}
|
||||
|
||||
// Add new comment API functions before the main manager function
|
||||
async function createComment(
|
||||
client: LinearClient,
|
||||
params: z.infer<typeof CreateCommentParams>
|
||||
) {
|
||||
try {
|
||||
const { issueId, body } = params;
|
||||
const comment = await client.createComment({
|
||||
issueId,
|
||||
body,
|
||||
});
|
||||
return formatComment(comment);
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function listComments(
|
||||
client: LinearClient,
|
||||
params: z.infer<typeof ListCommentsParams>
|
||||
) {
|
||||
try {
|
||||
const { issueId, limit } = params;
|
||||
const issue = await client.issue(issueId);
|
||||
const comments = await issue.comments({
|
||||
first: limit,
|
||||
orderBy: "createdAt" as any,
|
||||
});
|
||||
return comments.nodes.map(formatComment);
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new document API functions before the main manager function
|
||||
async function createDocument(
|
||||
client: LinearClient,
|
||||
params: z.infer<typeof CreateDocumentParams>
|
||||
) {
|
||||
try {
|
||||
const document = await client.createDocument(params);
|
||||
return formatDocument(document);
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateDocument(
|
||||
client: LinearClient,
|
||||
params: z.infer<typeof UpdateDocumentParams>
|
||||
) {
|
||||
try {
|
||||
const { documentId, ...updateData } = params;
|
||||
const document = await client.updateDocument(documentId, updateData);
|
||||
return formatDocument(document);
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function getDocument(
|
||||
client: LinearClient,
|
||||
{ documentId }: z.infer<typeof GetDocumentParams>
|
||||
) {
|
||||
try {
|
||||
const document = await client.document(documentId);
|
||||
return formatDocument(document);
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function searchDocuments(
|
||||
client: LinearClient,
|
||||
params: z.infer<typeof SearchDocumentsParams>
|
||||
) {
|
||||
try {
|
||||
const filter: any = {
|
||||
or: [
|
||||
{ title: { containsIgnoreCase: params.query } },
|
||||
{ content: { containsIgnoreCase: params.query } },
|
||||
],
|
||||
};
|
||||
|
||||
if (params.projectId) {
|
||||
filter.project = { id: { eq: params.projectId } };
|
||||
}
|
||||
|
||||
const documents = await client.documents({
|
||||
first: params.limit,
|
||||
filter,
|
||||
orderBy: "updatedAt" as any,
|
||||
});
|
||||
|
||||
return documents.nodes.map(formatDocument);
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Main manager function
|
||||
export const LinearManagerParams = z.object({
|
||||
request: z
|
||||
|
@ -536,11 +991,7 @@ export async function linearManager(
|
|||
{ request }: LinearManagerParams,
|
||||
context_message: Message
|
||||
) {
|
||||
console.log(
|
||||
"Context message",
|
||||
context_message.author,
|
||||
context_message.getUserRoles()
|
||||
);
|
||||
|
||||
const userConfig = context_message.author.config;
|
||||
|
||||
// console.log("User config", userConfig);
|
||||
|
@ -563,6 +1014,7 @@ export async function linearManager(
|
|||
|
||||
const client = new LinearClient({ apiKey: linearApiKey });
|
||||
|
||||
|
||||
const linear_tools: RunnableToolFunction<any>[] = [
|
||||
zodFunction({
|
||||
function: (params) => createIssue(client, params),
|
||||
|
@ -606,8 +1058,16 @@ export async function linearManager(
|
|||
function: (params) => advancedSearchIssues(client, params),
|
||||
name: "linearAdvancedSearchIssues",
|
||||
schema: AdvancedSearchIssuesParams,
|
||||
description:
|
||||
"Search for issues with advanced filters including status, assignee, and priority",
|
||||
description: `Search for issues with advanced filters including:
|
||||
- Status (backlog, todo, in_progress, done, canceled)
|
||||
- Assignee
|
||||
- Priority
|
||||
- Date ranges for:
|
||||
* Updated time
|
||||
* Created time
|
||||
* Completed time
|
||||
Use ISO datetime format (e.g., "2024-01-18T00:00:00Z") for date filters.
|
||||
Can find issues updated, created, or completed within specific time periods.`,
|
||||
}),
|
||||
zodFunction({
|
||||
function: (params) => createProject(client, params),
|
||||
|
@ -641,8 +1101,48 @@ export async function linearManager(
|
|||
description:
|
||||
"List projects in Linear, optionally filtered by team and state. Returns most recently updated projects first.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: (params) => createComment(client, params),
|
||||
name: "linearCreateComment",
|
||||
schema: CreateCommentParams,
|
||||
description: "Create a new comment on a Linear issue",
|
||||
}),
|
||||
zodFunction({
|
||||
function: (params) => listComments(client, params),
|
||||
name: "linearListComments",
|
||||
schema: ListCommentsParams,
|
||||
description: "List comments on a Linear issue",
|
||||
}),
|
||||
zodFunction({
|
||||
function: (params) => createDocument(client, params),
|
||||
name: "linearCreateDocument",
|
||||
schema: CreateDocumentParams,
|
||||
description: "Create a new document in Linear",
|
||||
}),
|
||||
zodFunction({
|
||||
function: (params) => updateDocument(client, params),
|
||||
name: "linearUpdateDocument",
|
||||
schema: UpdateDocumentParams,
|
||||
description: "Update an existing document in Linear",
|
||||
}),
|
||||
zodFunction({
|
||||
function: (params) => getDocument(client, params),
|
||||
name: "linearGetDocument",
|
||||
schema: GetDocumentParams,
|
||||
description: "Get details of a specific document",
|
||||
}),
|
||||
zodFunction({
|
||||
function: (params) => searchDocuments(client, params),
|
||||
name: "linearSearchDocuments",
|
||||
schema: SearchDocumentsParams,
|
||||
description: "Search for documents in Linear using a query string",
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
const organization = await client.organization
|
||||
const workspace = organization?.name
|
||||
|
||||
// fetch all labels available in each team
|
||||
const teams = await client.teams({ first: 10 });
|
||||
const teamLabels = await client.issueLabels();
|
||||
|
@ -654,54 +1154,65 @@ export async function linearManager(
|
|||
name: state.name,
|
||||
}));
|
||||
|
||||
const organizationContext = `Organization:
|
||||
Name: ${workspace}
|
||||
Id: ${organization?.id}
|
||||
`;
|
||||
|
||||
// Only include teams and labels in the context if they exist
|
||||
const teamsContext =
|
||||
teams.nodes.length > 0
|
||||
? `Teams:\n${teams.nodes.map((team) => ` - ${team.name}`).join("\n")}`
|
||||
? `Teams:\n${teams.nodes.map((team) => ` - ${team.name} id: ${team.id}`).join("\n")}`
|
||||
: "";
|
||||
|
||||
const labelsContext =
|
||||
teamLabels.nodes.length > 0
|
||||
? `All Labels:\n${teamLabels.nodes
|
||||
.map((label) => ` - ${label.name} (${label.color})`)
|
||||
.map((label) => ` - ${label.name} (${label.color}) id: ${label.id}`)
|
||||
.join("\n")}`
|
||||
: "";
|
||||
|
||||
const issueStateContext =
|
||||
state_values.length > 0
|
||||
? `All Issue States:\n${state_values
|
||||
.map((state) => ` - ${state.name}`)
|
||||
.map((state) => ` - ${state.name} id: ${state.id}`)
|
||||
.join("\n")}`
|
||||
: "";
|
||||
|
||||
const workspaceContext = [teamsContext, labelsContext, issueStateContext]
|
||||
const workspaceContext = [organizationContext, teamsContext, labelsContext, issueStateContext]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
|
||||
const userDetails = await client.users({ filter: { email: { eq: linearEmail } } });
|
||||
|
||||
const response = await ask({
|
||||
model: "gpt-4o-mini",
|
||||
model: "gpt-4o",
|
||||
prompt: `You are a Linear project manager.
|
||||
|
||||
Your job is to understand the user's request and manage issues, teams, and projects using the available tools.
|
||||
|
||||
Important note about Linear issue identification:
|
||||
- issueId: A UUID that uniquely identifies an issue internally (e.g., "123e4567-e89b-12d3-a456-426614174000")
|
||||
- identifier: A human-readable issue reference (e.g., "XCE-205", "ENG-123")
|
||||
When referring to issues in responses, always use the identifier format for better readability.
|
||||
|
||||
----
|
||||
${memory_manager_guide("linear_manager", context_message.author.id)}
|
||||
----
|
||||
|
||||
${
|
||||
workspaceContext
|
||||
${workspaceContext
|
||||
? `Here is some more context on current linear workspace:\n${workspaceContext}`
|
||||
: ""
|
||||
}
|
||||
|
||||
The user you are currently assisting has the following details:
|
||||
The user you are currently assisting has the following details (No need to search if the user is asking for their own related issues/projects):
|
||||
- Name: ${userConfig?.name}
|
||||
- Linear Email: ${linearEmail}
|
||||
- Linear User ID: ${userDetails.nodes[0]?.id}
|
||||
|
||||
When responding make sure to link the issues when returning the value.
|
||||
linear issue links look like: \`https://linear.app/xcelerator/issue/XCE-205\`
|
||||
Where \`XCE-205\` is the issue ID and \`xcelerator\` is the team name.
|
||||
|
||||
Where \`XCE-205\` is the identifier (not the issueId) and \`xcelerator\` is the team name.
|
||||
`,
|
||||
message: request,
|
||||
seed: `linear-${context_message.channelId}`,
|
||||
|
|
|
@ -416,7 +416,7 @@ ${memory_manager_guide("links_manager", context_message.author.id)}
|
|||
----
|
||||
`,
|
||||
message: request,
|
||||
seed: "link-${context_message.channelId}",
|
||||
seed: `link-${context_message.channelId}`,
|
||||
tools: link_tools.concat(
|
||||
memory_manager_init(context_message, "links_manager")
|
||||
) as any,
|
||||
|
|
Loading…
Reference in New Issue