Better citations for gmail, gcal, and outlook
This commit is contained in:
parent
fdc585b832
commit
676f305927
@ -12,6 +12,9 @@ import {
|
|||||||
LinkSimple,
|
LinkSimple,
|
||||||
GitlabLogo,
|
GitlabLogo,
|
||||||
} from "@phosphor-icons/react";
|
} 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 { toPercentString } from "@/utils/numbers";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSourcesSidebar } from "../../SourcesSidebar";
|
import { useSourcesSidebar } from "../../SourcesSidebar";
|
||||||
@ -28,6 +31,14 @@ const CIRCLE_ICONS = {
|
|||||||
paperlessNgx: FileText,
|
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.
|
* 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
|
* @param {"file"|"link"|"youtube"|"github"|"gitlab"|"confluence"|"drupalwiki"|"obsidian"|"paperlessNgx"} props.type
|
||||||
@ -42,6 +53,7 @@ export function SourceTypeCircle({
|
|||||||
url = null,
|
url = null,
|
||||||
}) {
|
}) {
|
||||||
const Icon = CIRCLE_ICONS[type] || CIRCLE_ICONS.file;
|
const Icon = CIRCLE_ICONS[type] || CIRCLE_ICONS.file;
|
||||||
|
const customImage = CIRCLE_IMAGES[type];
|
||||||
const [imgError, setImgError] = useState(false);
|
const [imgError, setImgError] = useState(false);
|
||||||
|
|
||||||
let faviconUrl = null;
|
let faviconUrl = null;
|
||||||
@ -71,6 +83,13 @@ export function SourceTypeCircle({
|
|||||||
className="object-cover"
|
className="object-cover"
|
||||||
onError={() => setImgError(true)}
|
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" />
|
<Icon size={iconSize} weight="bold" className="text-black" />
|
||||||
)}
|
)}
|
||||||
@ -262,6 +281,11 @@ const supportedSources = [
|
|||||||
"youtube://",
|
"youtube://",
|
||||||
"obsidian://",
|
"obsidian://",
|
||||||
"paperless-ngx://",
|
"paperless-ngx://",
|
||||||
|
"gmail-thread://",
|
||||||
|
"gmail-attachment://",
|
||||||
|
"google-calendar://",
|
||||||
|
"outlook-thread://",
|
||||||
|
"outlook-attachment://",
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -342,6 +366,30 @@ export function parseChunkSource({ title = "", chunks = [] }) {
|
|||||||
icon = "paperlessNgx";
|
icon = "paperlessNgx";
|
||||||
break;
|
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:
|
default:
|
||||||
text = url.host + url.pathname;
|
text = url.host + url.pathname;
|
||||||
icon = "link";
|
icon = "link";
|
||||||
|
|||||||
@ -87,6 +87,14 @@ module.exports.GmailReadThread = {
|
|||||||
`${this.caller}: Successfully read thread with ${thread.messageCount} messages`
|
`${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 (
|
return (
|
||||||
`Thread: "${thread.subject}"\n` +
|
`Thread: "${thread.subject}"\n` +
|
||||||
`Thread ID: ${thread.id}\n` +
|
`Thread ID: ${thread.id}\n` +
|
||||||
|
|||||||
@ -80,7 +80,7 @@ module.exports.GCalGetEvent = {
|
|||||||
.join("\n")
|
.join("\n")
|
||||||
: " (none)";
|
: " (none)";
|
||||||
|
|
||||||
return (
|
const eventDetails =
|
||||||
`Event Details:\n` +
|
`Event Details:\n` +
|
||||||
`Title: ${event.title}\n` +
|
`Title: ${event.title}\n` +
|
||||||
`Event ID: ${event.eventId}\n` +
|
`Event ID: ${event.eventId}\n` +
|
||||||
@ -93,8 +93,16 @@ module.exports.GCalGetEvent = {
|
|||||||
`Owned by me: ${event.isOwnedByMe ? "Yes" : "No"}\n` +
|
`Owned by me: ${event.isOwnedByMe ? "Yes" : "No"}\n` +
|
||||||
`Guests:\n${guestList}\n` +
|
`Guests:\n${guestList}\n` +
|
||||||
`Created: ${new Date(event.dateCreated).toLocaleString()}\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) {
|
} catch (e) {
|
||||||
this.super.handlerProps.log(`gcal-get-event error: ${e.message}`);
|
this.super.handlerProps.log(`gcal-get-event error: ${e.message}`);
|
||||||
this.super.introspect(`Error: ${e.message}`);
|
this.super.introspect(`Error: ${e.message}`);
|
||||||
|
|||||||
@ -65,34 +65,42 @@ module.exports.GCalGetEventsForDay = {
|
|||||||
`${this.caller}: Found ${eventCount} event(s) for ${date}`
|
`${this.caller}: Found ${eventCount} event(s) for ${date}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (eventCount === 0) {
|
if (eventCount === 0) return `No events scheduled for ${date}.`;
|
||||||
return `No events scheduled for ${date}.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const summary = events
|
const summaries = [];
|
||||||
.map((event, i) => {
|
const citations = [];
|
||||||
let timeStr;
|
events.forEach((event, i) => {
|
||||||
if (event.isAllDayEvent) {
|
let timeStr;
|
||||||
timeStr = "All day";
|
if (event.isAllDayEvent) {
|
||||||
} else {
|
timeStr = "All day";
|
||||||
const start = new Date(event.startTime).toLocaleTimeString(
|
} else {
|
||||||
[],
|
const start = new Date(event.startTime).toLocaleTimeString(
|
||||||
{ hour: "2-digit", minute: "2-digit" }
|
[],
|
||||||
);
|
{ 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 end = new Date(event.endTime).toLocaleTimeString([], {
|
||||||
.join("\n\n");
|
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}`;
|
return `Events for ${date} (${eventCount} total):\n\n${summary}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.super.handlerProps.log(
|
this.super.handlerProps.log(
|
||||||
|
|||||||
@ -98,22 +98,44 @@ module.exports.GCalGetEvents = {
|
|||||||
return `No events found between ${startDate} and ${endDate}${query ? ` matching "${query}"` : ""}.`;
|
return `No events found between ${startDate} and ${endDate}${query ? ` matching "${query}"` : ""}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary = events
|
const summaries = [];
|
||||||
.map((event, i) => {
|
const citations = [];
|
||||||
let timeStr;
|
events.forEach((event, i) => {
|
||||||
if (event.isAllDayEvent) {
|
let timeStr;
|
||||||
timeStr = `All day (${new Date(event.startDate).toLocaleDateString()})`;
|
if (event.isAllDayEvent) {
|
||||||
} else {
|
timeStr = `All day (${new Date(event.startDate).toLocaleDateString()})`;
|
||||||
timeStr = `${new Date(event.startTime).toLocaleString()} - ${new Date(event.endTime).toLocaleString()}`;
|
} else {
|
||||||
}
|
const startTime = new Date(event.startTime);
|
||||||
return (
|
const endTime = new Date(event.endTime);
|
||||||
`${i + 1}. "${event.title}"\n` +
|
const dateStr = startTime.toLocaleDateString();
|
||||||
` ${timeStr}\n` +
|
const startTimeStr = startTime.toLocaleTimeString([], {
|
||||||
` ID: ${event.eventId}` +
|
hour: "2-digit",
|
||||||
(event.location ? `\n Location: ${event.location}` : "")
|
minute: "2-digit",
|
||||||
);
|
});
|
||||||
})
|
const endTimeStr = endTime.toLocaleTimeString([], {
|
||||||
.join("\n\n");
|
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)`;
|
let response = `Found ${totalEvents} event(s)`;
|
||||||
if (returnedEvents < totalEvents) {
|
if (returnedEvents < totalEvents) {
|
||||||
|
|||||||
@ -186,33 +186,44 @@ module.exports.GCalGetUpcomingEvents = {
|
|||||||
return `No events scheduled for ${label}${query ? ` matching "${query}"` : ""}.`;
|
return `No events scheduled for ${label}${query ? ` matching "${query}"` : ""}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary = events
|
const summaries = [];
|
||||||
.map((event, i) => {
|
const citations = [];
|
||||||
let timeStr;
|
events.forEach((event, i) => {
|
||||||
if (event.isAllDayEvent) {
|
let timeStr;
|
||||||
timeStr = `All day (${new Date(event.startDate).toLocaleDateString()})`;
|
if (event.isAllDayEvent) {
|
||||||
} else {
|
timeStr = `All day (${new Date(event.startDate).toLocaleDateString()})`;
|
||||||
const startTime = new Date(event.startTime);
|
} else {
|
||||||
const endTime = new Date(event.endTime);
|
const startTime = new Date(event.startTime);
|
||||||
const dateStr = startTime.toLocaleDateString();
|
const endTime = new Date(event.endTime);
|
||||||
const startTimeStr = startTime.toLocaleTimeString([], {
|
const dateStr = startTime.toLocaleDateString();
|
||||||
hour: "2-digit",
|
const startTimeStr = startTime.toLocaleTimeString([], {
|
||||||
minute: "2-digit",
|
hour: "2-digit",
|
||||||
});
|
minute: "2-digit",
|
||||||
const endTimeStr = endTime.toLocaleTimeString([], {
|
});
|
||||||
hour: "2-digit",
|
const endTimeStr = endTime.toLocaleTimeString([], {
|
||||||
minute: "2-digit",
|
hour: "2-digit",
|
||||||
});
|
minute: "2-digit",
|
||||||
timeStr = `${dateStr} ${startTimeStr} - ${endTimeStr}`;
|
});
|
||||||
}
|
timeStr = `${dateStr} ${startTimeStr} - ${endTimeStr}`;
|
||||||
return (
|
}
|
||||||
`${i + 1}. "${event.title}"\n` +
|
const eventDetails =
|
||||||
` ${timeStr}\n` +
|
`${i + 1}. "${event.title}"\n` +
|
||||||
` ID: ${event.eventId}` +
|
` ${timeStr}\n` +
|
||||||
(event.location ? `\n Location: ${event.location}` : "")
|
` ID: ${event.eventId}` +
|
||||||
);
|
(event.location ? `\n Location: ${event.location}` : "");
|
||||||
})
|
|
||||||
.join("\n\n");
|
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}`;
|
let response = `Events for ${label}`;
|
||||||
if (returnedEvents < totalEvents) {
|
if (returnedEvents < totalEvents) {
|
||||||
|
|||||||
@ -95,6 +95,15 @@ module.exports.OutlookReadThread = {
|
|||||||
`${this.caller}: Successfully read thread with ${thread.messageCount} messages`
|
`${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 (
|
return (
|
||||||
`Thread: "${thread.subject}"\n` +
|
`Thread: "${thread.subject}"\n` +
|
||||||
`Conversation ID: ${thread.conversationId}\n` +
|
`Conversation ID: ${thread.conversationId}\n` +
|
||||||
@ -107,6 +116,14 @@ module.exports.OutlookReadThread = {
|
|||||||
return handleSkillError(this, "outlook-read-thread", e);
|
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}`;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user