mirror of
https://github.com/Mintplex-Labs/anything-llm
synced 2026-04-25 17:15:37 +02:00
Better citations for gmail, gcal, and outlook
This commit is contained in:
@@ -12,6 +12,9 @@ import {
|
||||
LinkSimple,
|
||||
GitlabLogo,
|
||||
} from "@phosphor-icons/react";
|
||||
import GmailLogo from "@/pages/Admin/Agents/GMailSkillPanel/gmail.png";
|
||||
import GoogleCalendarLogo from "@/pages/Admin/Agents/GoogleCalendarSkillPanel/google-calendar.png";
|
||||
import OutlookLogo from "@/pages/Admin/Agents/OutlookSkillPanel/outlook.png";
|
||||
import { toPercentString } from "@/utils/numbers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSourcesSidebar } from "../../SourcesSidebar";
|
||||
@@ -28,6 +31,14 @@ const CIRCLE_ICONS = {
|
||||
paperlessNgx: FileText,
|
||||
};
|
||||
|
||||
const CIRCLE_IMAGES = {
|
||||
gmailThread: GmailLogo,
|
||||
gmailAttachment: GmailLogo,
|
||||
googleCalendar: GoogleCalendarLogo,
|
||||
outlookThread: OutlookLogo,
|
||||
outlookAttachment: OutlookLogo,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a circle with a source type icon inside, or a favicon if URL is provided.
|
||||
* @param {"file"|"link"|"youtube"|"github"|"gitlab"|"confluence"|"drupalwiki"|"obsidian"|"paperlessNgx"} props.type
|
||||
@@ -42,6 +53,7 @@ export function SourceTypeCircle({
|
||||
url = null,
|
||||
}) {
|
||||
const Icon = CIRCLE_ICONS[type] || CIRCLE_ICONS.file;
|
||||
const customImage = CIRCLE_IMAGES[type];
|
||||
const [imgError, setImgError] = useState(false);
|
||||
|
||||
let faviconUrl = null;
|
||||
@@ -71,6 +83,13 @@ export function SourceTypeCircle({
|
||||
className="object-cover"
|
||||
onError={() => setImgError(true)}
|
||||
/>
|
||||
) : customImage ? (
|
||||
<img
|
||||
src={customImage}
|
||||
alt={type}
|
||||
style={{ width: iconSize, height: iconSize }}
|
||||
className="object-contain"
|
||||
/>
|
||||
) : (
|
||||
<Icon size={iconSize} weight="bold" className="text-black" />
|
||||
)}
|
||||
@@ -262,6 +281,11 @@ const supportedSources = [
|
||||
"youtube://",
|
||||
"obsidian://",
|
||||
"paperless-ngx://",
|
||||
"gmail-thread://",
|
||||
"gmail-attachment://",
|
||||
"google-calendar://",
|
||||
"outlook-thread://",
|
||||
"outlook-attachment://",
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -342,6 +366,30 @@ export function parseChunkSource({ title = "", chunks = [] }) {
|
||||
icon = "paperlessNgx";
|
||||
break;
|
||||
|
||||
case "gmail-thread://":
|
||||
text = title;
|
||||
icon = "gmailThread";
|
||||
break;
|
||||
case "gmail-attachment://":
|
||||
text = title;
|
||||
icon = "gmailAttachment";
|
||||
break;
|
||||
|
||||
case "google-calendar://":
|
||||
text = title;
|
||||
icon = "googleCalendar";
|
||||
break;
|
||||
|
||||
case "outlook-thread://":
|
||||
text = title;
|
||||
icon = "outlookThread";
|
||||
break;
|
||||
|
||||
case "outlook-attachment://":
|
||||
text = title;
|
||||
icon = "outlookAttachment";
|
||||
break;
|
||||
|
||||
default:
|
||||
text = url.host + url.pathname;
|
||||
icon = "link";
|
||||
|
||||
@@ -87,6 +87,14 @@ module.exports.GmailReadThread = {
|
||||
`${this.caller}: Successfully read thread with ${thread.messageCount} messages`
|
||||
);
|
||||
|
||||
this.super.addCitation?.({
|
||||
id: `gmail-thread-${thread.id}`,
|
||||
title: thread.subject,
|
||||
text: messagesFormatted,
|
||||
chunkSource: `gmail-thread://${thread.permalink}`,
|
||||
score: null,
|
||||
});
|
||||
|
||||
return (
|
||||
`Thread: "${thread.subject}"\n` +
|
||||
`Thread ID: ${thread.id}\n` +
|
||||
|
||||
@@ -80,7 +80,7 @@ module.exports.GCalGetEvent = {
|
||||
.join("\n")
|
||||
: " (none)";
|
||||
|
||||
return (
|
||||
const eventDetails =
|
||||
`Event Details:\n` +
|
||||
`Title: ${event.title}\n` +
|
||||
`Event ID: ${event.eventId}\n` +
|
||||
@@ -93,8 +93,16 @@ module.exports.GCalGetEvent = {
|
||||
`Owned by me: ${event.isOwnedByMe ? "Yes" : "No"}\n` +
|
||||
`Guests:\n${guestList}\n` +
|
||||
`Created: ${new Date(event.dateCreated).toLocaleString()}\n` +
|
||||
`Last Updated: ${new Date(event.lastUpdated).toLocaleString()}`
|
||||
);
|
||||
`Last Updated: ${new Date(event.lastUpdated).toLocaleString()}`;
|
||||
|
||||
this.super.addCitation?.({
|
||||
id: `google-calendar-${event.eventId}`,
|
||||
title: event.title,
|
||||
text: eventDetails,
|
||||
chunkSource: `google-calendar://${event.eventId}`,
|
||||
score: null,
|
||||
});
|
||||
return eventDetails;
|
||||
} catch (e) {
|
||||
this.super.handlerProps.log(`gcal-get-event error: ${e.message}`);
|
||||
this.super.introspect(`Error: ${e.message}`);
|
||||
|
||||
@@ -65,34 +65,42 @@ module.exports.GCalGetEventsForDay = {
|
||||
`${this.caller}: Found ${eventCount} event(s) for ${date}`
|
||||
);
|
||||
|
||||
if (eventCount === 0) {
|
||||
return `No events scheduled for ${date}.`;
|
||||
}
|
||||
if (eventCount === 0) return `No events scheduled for ${date}.`;
|
||||
|
||||
const summary = events
|
||||
.map((event, i) => {
|
||||
let timeStr;
|
||||
if (event.isAllDayEvent) {
|
||||
timeStr = "All day";
|
||||
} else {
|
||||
const start = new Date(event.startTime).toLocaleTimeString(
|
||||
[],
|
||||
{ hour: "2-digit", minute: "2-digit" }
|
||||
);
|
||||
const end = new Date(event.endTime).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
timeStr = `${start} - ${end}`;
|
||||
}
|
||||
return (
|
||||
`${i + 1}. "${event.title}" (${timeStr})\n` +
|
||||
` ID: ${event.eventId}` +
|
||||
(event.location ? `\n Location: ${event.location}` : "")
|
||||
const summaries = [];
|
||||
const citations = [];
|
||||
events.forEach((event, i) => {
|
||||
let timeStr;
|
||||
if (event.isAllDayEvent) {
|
||||
timeStr = "All day";
|
||||
} else {
|
||||
const start = new Date(event.startTime).toLocaleTimeString(
|
||||
[],
|
||||
{ hour: "2-digit", minute: "2-digit" }
|
||||
);
|
||||
})
|
||||
.join("\n\n");
|
||||
const end = new Date(event.endTime).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
timeStr = `${start} - ${end}`;
|
||||
}
|
||||
const eventDetails =
|
||||
`${i + 1}. "${event.title}" (${timeStr})\n` +
|
||||
` ID: ${event.eventId}` +
|
||||
(event.location ? `\n Location: ${event.location}` : "");
|
||||
|
||||
summaries.push(eventDetails);
|
||||
citations.push({
|
||||
id: `google-calendar-${event.eventId}`,
|
||||
title: event.title,
|
||||
text: eventDetails,
|
||||
chunkSource: `google-calendar://${event.eventId}`,
|
||||
score: null,
|
||||
});
|
||||
});
|
||||
|
||||
const summary = summaries.join("\n\n");
|
||||
citations.forEach((c) => this.super.addCitation?.(c));
|
||||
return `Events for ${date} (${eventCount} total):\n\n${summary}`;
|
||||
} catch (e) {
|
||||
this.super.handlerProps.log(
|
||||
|
||||
@@ -98,22 +98,44 @@ module.exports.GCalGetEvents = {
|
||||
return `No events found between ${startDate} and ${endDate}${query ? ` matching "${query}"` : ""}.`;
|
||||
}
|
||||
|
||||
const summary = events
|
||||
.map((event, i) => {
|
||||
let timeStr;
|
||||
if (event.isAllDayEvent) {
|
||||
timeStr = `All day (${new Date(event.startDate).toLocaleDateString()})`;
|
||||
} else {
|
||||
timeStr = `${new Date(event.startTime).toLocaleString()} - ${new Date(event.endTime).toLocaleString()}`;
|
||||
}
|
||||
return (
|
||||
`${i + 1}. "${event.title}"\n` +
|
||||
` ${timeStr}\n` +
|
||||
` ID: ${event.eventId}` +
|
||||
(event.location ? `\n Location: ${event.location}` : "")
|
||||
);
|
||||
})
|
||||
.join("\n\n");
|
||||
const summaries = [];
|
||||
const citations = [];
|
||||
events.forEach((event, i) => {
|
||||
let timeStr;
|
||||
if (event.isAllDayEvent) {
|
||||
timeStr = `All day (${new Date(event.startDate).toLocaleDateString()})`;
|
||||
} else {
|
||||
const startTime = new Date(event.startTime);
|
||||
const endTime = new Date(event.endTime);
|
||||
const dateStr = startTime.toLocaleDateString();
|
||||
const startTimeStr = startTime.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
const endTimeStr = endTime.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
timeStr = `${dateStr} ${startTimeStr} - ${endTimeStr}`;
|
||||
}
|
||||
const eventDetails =
|
||||
`${i + 1}. "${event.title}"\n` +
|
||||
` ${timeStr}\n` +
|
||||
` ID: ${event.eventId}` +
|
||||
(event.location ? `\n Location: ${event.location}` : "");
|
||||
|
||||
summaries.push(eventDetails);
|
||||
citations.push({
|
||||
id: `google-calendar-${event.eventId}`,
|
||||
title: event.title,
|
||||
text: eventDetails,
|
||||
chunkSource: `google-calendar://${event.eventId}`,
|
||||
score: null,
|
||||
});
|
||||
});
|
||||
|
||||
const summary = summaries.join("\n\n");
|
||||
citations.forEach((c) => this.super.addCitation?.(c));
|
||||
|
||||
let response = `Found ${totalEvents} event(s)`;
|
||||
if (returnedEvents < totalEvents) {
|
||||
|
||||
@@ -186,33 +186,44 @@ module.exports.GCalGetUpcomingEvents = {
|
||||
return `No events scheduled for ${label}${query ? ` matching "${query}"` : ""}.`;
|
||||
}
|
||||
|
||||
const summary = events
|
||||
.map((event, i) => {
|
||||
let timeStr;
|
||||
if (event.isAllDayEvent) {
|
||||
timeStr = `All day (${new Date(event.startDate).toLocaleDateString()})`;
|
||||
} else {
|
||||
const startTime = new Date(event.startTime);
|
||||
const endTime = new Date(event.endTime);
|
||||
const dateStr = startTime.toLocaleDateString();
|
||||
const startTimeStr = startTime.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
const endTimeStr = endTime.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
timeStr = `${dateStr} ${startTimeStr} - ${endTimeStr}`;
|
||||
}
|
||||
return (
|
||||
`${i + 1}. "${event.title}"\n` +
|
||||
` ${timeStr}\n` +
|
||||
` ID: ${event.eventId}` +
|
||||
(event.location ? `\n Location: ${event.location}` : "")
|
||||
);
|
||||
})
|
||||
.join("\n\n");
|
||||
const summaries = [];
|
||||
const citations = [];
|
||||
events.forEach((event, i) => {
|
||||
let timeStr;
|
||||
if (event.isAllDayEvent) {
|
||||
timeStr = `All day (${new Date(event.startDate).toLocaleDateString()})`;
|
||||
} else {
|
||||
const startTime = new Date(event.startTime);
|
||||
const endTime = new Date(event.endTime);
|
||||
const dateStr = startTime.toLocaleDateString();
|
||||
const startTimeStr = startTime.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
const endTimeStr = endTime.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
timeStr = `${dateStr} ${startTimeStr} - ${endTimeStr}`;
|
||||
}
|
||||
const eventDetails =
|
||||
`${i + 1}. "${event.title}"\n` +
|
||||
` ${timeStr}\n` +
|
||||
` ID: ${event.eventId}` +
|
||||
(event.location ? `\n Location: ${event.location}` : "");
|
||||
|
||||
summaries.push(eventDetails);
|
||||
citations.push({
|
||||
id: `google-calendar-${event.eventId}`,
|
||||
title: event.title,
|
||||
text: eventDetails,
|
||||
chunkSource: `google-calendar://${event.eventId}`,
|
||||
score: null,
|
||||
});
|
||||
});
|
||||
|
||||
const summary = summaries.join("\n\n");
|
||||
citations.forEach((c) => this.super.addCitation?.(c));
|
||||
|
||||
let response = `Events for ${label}`;
|
||||
if (returnedEvents < totalEvents) {
|
||||
|
||||
@@ -95,6 +95,15 @@ module.exports.OutlookReadThread = {
|
||||
`${this.caller}: Successfully read thread with ${thread.messageCount} messages`
|
||||
);
|
||||
|
||||
// Report citation for the thread (without attachments)
|
||||
this.super.addCitation?.({
|
||||
id: `outlook-thread-${thread.conversationId}`,
|
||||
title: thread.subject,
|
||||
text: `Subject: "${thread.subject}"\n\n${messagesFormatted}`,
|
||||
chunkSource: `outlook-thread://${this._generatePermalink(thread.conversationId)}`,
|
||||
score: null,
|
||||
});
|
||||
|
||||
return (
|
||||
`Thread: "${thread.subject}"\n` +
|
||||
`Conversation ID: ${thread.conversationId}\n` +
|
||||
@@ -107,6 +116,14 @@ module.exports.OutlookReadThread = {
|
||||
return handleSkillError(this, "outlook-read-thread", e);
|
||||
}
|
||||
},
|
||||
_generatePermalink: function (conversationId) {
|
||||
if (!conversationId) return null;
|
||||
let encodedId = encodeURIComponent(conversationId);
|
||||
// For outlook, this needs to be specifically encoded
|
||||
// as the webpage does not respect it like traditional URL encoding
|
||||
encodedId = encodedId.replace(/-/g, "%2F");
|
||||
return `https://outlook.live.com/mail/inbox/id/${encodedId}`;
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user