Report citations for Agent call stacks (#5199)
This commit is contained in:
parent
15a84d5121
commit
c76576a9da
@ -1,4 +1,4 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment, useState, useEffect } from "react";
|
||||||
import { decode as HTMLDecode } from "he";
|
import { decode as HTMLDecode } from "he";
|
||||||
import truncate from "truncate";
|
import truncate from "truncate";
|
||||||
import ModalWrapper from "@/components/ModalWrapper";
|
import ModalWrapper from "@/components/ModalWrapper";
|
||||||
@ -29,19 +29,51 @@ const CIRCLE_ICONS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a circle with a source type icon inside.
|
* 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
|
||||||
* @param {number} [props.size] - Circle diameter in px
|
* @param {number} [props.size] - Circle diameter in px
|
||||||
* @param {number} [props.iconSize] - Icon size in px
|
* @param {number} [props.iconSize] - Icon size in px
|
||||||
|
* @param {string} [props.url] - Optional URL to fetch favicon from
|
||||||
*/
|
*/
|
||||||
export function SourceTypeCircle({ type = "file", size = 22, iconSize = 12 }) {
|
export function SourceTypeCircle({
|
||||||
|
type = "file",
|
||||||
|
size = 22,
|
||||||
|
iconSize = 12,
|
||||||
|
url = null,
|
||||||
|
}) {
|
||||||
const Icon = CIRCLE_ICONS[type] || CIRCLE_ICONS.file;
|
const Icon = CIRCLE_ICONS[type] || CIRCLE_ICONS.file;
|
||||||
|
const [imgError, setImgError] = useState(false);
|
||||||
|
|
||||||
|
let faviconUrl = null;
|
||||||
|
if (type === "link" && url) {
|
||||||
|
try {
|
||||||
|
const hostname = new URL(url).hostname;
|
||||||
|
faviconUrl = `https://www.google.com/s2/favicons?domain=${hostname}&sz=64`;
|
||||||
|
} catch {
|
||||||
|
faviconUrl = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setImgError(false);
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="bg-white light:bg-slate-100 rounded-full flex items-center justify-center"
|
className="bg-white light:bg-slate-100 rounded-full flex items-center justify-center overflow-hidden"
|
||||||
style={{ width: size, height: size }}
|
style={{ width: size, height: size }}
|
||||||
>
|
>
|
||||||
<Icon size={iconSize} weight="bold" className="text-black" />
|
{faviconUrl && !imgError ? (
|
||||||
|
<img
|
||||||
|
src={faviconUrl}
|
||||||
|
alt="favicon"
|
||||||
|
style={{ width: size, height: size }}
|
||||||
|
className="object-cover"
|
||||||
|
onError={() => setImgError(true)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon size={iconSize} weight="bold" className="text-black" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -107,7 +139,12 @@ export default function Citations({ sources = [] }) {
|
|||||||
className="absolute top-0 size-[22px] rounded-full border-2 border-zinc-800 light:border-white"
|
className="absolute top-0 size-[22px] rounded-full border-2 border-zinc-800 light:border-white"
|
||||||
style={{ left: `${idx * 17}px`, zIndex: 3 - idx }}
|
style={{ left: `${idx * 17}px`, zIndex: 3 - idx }}
|
||||||
>
|
>
|
||||||
<SourceTypeCircle type={info.icon} size={18} iconSize={10} />
|
<SourceTypeCircle
|
||||||
|
type={info.icon}
|
||||||
|
size={18}
|
||||||
|
iconSize={10}
|
||||||
|
url={info.href}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -196,7 +196,8 @@ export default memo(
|
|||||||
prevProps.message === nextProps.message &&
|
prevProps.message === nextProps.message &&
|
||||||
prevProps.isLastMessage === nextProps.isLastMessage &&
|
prevProps.isLastMessage === nextProps.isLastMessage &&
|
||||||
prevProps.chatId === nextProps.chatId &&
|
prevProps.chatId === nextProps.chatId &&
|
||||||
JSON.stringify(prevProps.metrics) === JSON.stringify(nextProps.metrics)
|
JSON.stringify(prevProps.metrics) === JSON.stringify(nextProps.metrics) &&
|
||||||
|
JSON.stringify(prevProps.sources) === JSON.stringify(nextProps.sources)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@ -13,7 +13,12 @@ export default function SourceItem({ source, onClick }) {
|
|||||||
className="flex flex-col gap-[2px] items-start w-full text-left hover:opacity-75 transition-opacity"
|
className="flex flex-col gap-[2px] items-start w-full text-left hover:opacity-75 transition-opacity"
|
||||||
>
|
>
|
||||||
<div className="flex gap-[6px] items-start w-full">
|
<div className="flex gap-[6px] items-start w-full">
|
||||||
<SourceTypeCircle type={info.icon} size={16} iconSize={10} />
|
<SourceTypeCircle
|
||||||
|
type={info.icon}
|
||||||
|
size={16}
|
||||||
|
iconSize={10}
|
||||||
|
url={info.href}
|
||||||
|
/>
|
||||||
<p className="flex-1 font-medium text-sm text-white light:text-slate-900 leading-[15px] truncate">
|
<p className="flex-1 font-medium text-sm text-white light:text-slate-900 leading-[15px] truncate">
|
||||||
{source.title}
|
{source.title}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -138,6 +138,18 @@ export default function handleSocketResponse(socket, event, setChatHistory) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === "citations") {
|
||||||
|
if (!data.content.citations) return prev;
|
||||||
|
return prev.map((msg) =>
|
||||||
|
msg.uuid === uuid
|
||||||
|
? {
|
||||||
|
...msg,
|
||||||
|
sources: [...(msg.sources || []), ...data.content.citations],
|
||||||
|
}
|
||||||
|
: msg
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (type === "textResponseChunk") {
|
if (type === "textResponseChunk") {
|
||||||
return prev
|
return prev
|
||||||
.map((msg) =>
|
.map((msg) =>
|
||||||
|
|||||||
@ -37,6 +37,13 @@ class AIbitat {
|
|||||||
channels = new Map();
|
channels = new Map();
|
||||||
functions = new Map();
|
functions = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buffer for citations collected during tool execution.
|
||||||
|
* Citations are flushed to the frontend when the response is finalized.
|
||||||
|
* @type {Array<{id: string, title: string, text: string, chunkSource?: string, score?: number}>}
|
||||||
|
*/
|
||||||
|
_pendingCitations = [];
|
||||||
|
|
||||||
constructor(props = {}) {
|
constructor(props = {}) {
|
||||||
const {
|
const {
|
||||||
chats = [],
|
chats = [],
|
||||||
@ -76,6 +83,41 @@ class AIbitat {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add citation(s) to be reported when the response is finalized.
|
||||||
|
* Citations are buffered and flushed with the correct message UUID.
|
||||||
|
* @param {{id: string, title: string, text: string, chunkSource?: string, score?: number}|Array<{id: string, title: string, text: string, chunkSource?: string, score?: number}>} citations - Citation object or array of citation objects
|
||||||
|
*/
|
||||||
|
addCitation(citations) {
|
||||||
|
if (!citations) return;
|
||||||
|
if (Array.isArray(citations))
|
||||||
|
this._pendingCitations.push(...citations.filter(Boolean));
|
||||||
|
else if (typeof citations === "object")
|
||||||
|
this._pendingCitations.push(citations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush all pending citations to the frontend with the given message UUID.
|
||||||
|
* Called automatically when the agent response is finalized.
|
||||||
|
* Note: Does not clear citations - they are cleared by chat-history plugin after persisting.
|
||||||
|
* @param {string} messageUuid - The UUID of the message to attach citations to
|
||||||
|
*/
|
||||||
|
flushCitations(messageUuid) {
|
||||||
|
if (!messageUuid || this._pendingCitations.length === 0) return;
|
||||||
|
this.socket?.send?.("reportStreamEvent", {
|
||||||
|
type: "citations",
|
||||||
|
uuid: messageUuid,
|
||||||
|
citations: this._pendingCitations,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all pending citations. Called after citations have been persisted.
|
||||||
|
*/
|
||||||
|
clearCitations() {
|
||||||
|
this._pendingCitations = [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new agent to the AIbitat.
|
* Add a new agent to the AIbitat.
|
||||||
*
|
*
|
||||||
@ -671,11 +713,13 @@ ${this.getHistory({ to: route.to })
|
|||||||
);
|
);
|
||||||
|
|
||||||
const finalStream = await provider.stream(messages, [], eventHandler);
|
const finalStream = await provider.stream(messages, [], eventHandler);
|
||||||
|
const finalUuid = finalStream?.uuid || v4();
|
||||||
eventHandler?.("reportStreamEvent", {
|
eventHandler?.("reportStreamEvent", {
|
||||||
type: "usageMetrics",
|
type: "usageMetrics",
|
||||||
uuid: finalStream?.uuid || v4(),
|
uuid: finalUuid,
|
||||||
metrics: provider.getUsage(),
|
metrics: provider.getUsage(),
|
||||||
});
|
});
|
||||||
|
this?.flushCitations?.(finalUuid);
|
||||||
const finalResponse =
|
const finalResponse =
|
||||||
finalStream?.textResponse ||
|
finalStream?.textResponse ||
|
||||||
"I reached the maximum number of tool calls allowed for a single response. Here is what I have so far based on the tools I was able to run.";
|
"I reached the maximum number of tool calls allowed for a single response. Here is what I have so far based on the tools I was able to run.";
|
||||||
@ -744,6 +788,7 @@ ${this.getHistory({ to: route.to })
|
|||||||
uuid: directOutputUUID,
|
uuid: directOutputUUID,
|
||||||
metrics: provider.getUsage(),
|
metrics: provider.getUsage(),
|
||||||
});
|
});
|
||||||
|
this?.flushCitations?.(directOutputUUID);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -764,11 +809,13 @@ ${this.getHistory({ to: route.to })
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const responseUuid = completionStream?.uuid || v4();
|
||||||
eventHandler?.("reportStreamEvent", {
|
eventHandler?.("reportStreamEvent", {
|
||||||
type: "usageMetrics",
|
type: "usageMetrics",
|
||||||
uuid: completionStream?.uuid || v4(),
|
uuid: responseUuid,
|
||||||
metrics: provider.getUsage(),
|
metrics: provider.getUsage(),
|
||||||
});
|
});
|
||||||
|
this?.flushCitations?.(responseUuid);
|
||||||
return completionStream?.textResponse;
|
return completionStream?.textResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -817,6 +864,7 @@ ${this.getHistory({ to: route.to })
|
|||||||
uuid: msgUUID,
|
uuid: msgUUID,
|
||||||
metrics: provider.getUsage(),
|
metrics: provider.getUsage(),
|
||||||
});
|
});
|
||||||
|
this?.flushCitations?.(msgUUID);
|
||||||
return (
|
return (
|
||||||
finalCompletion?.textResponse ||
|
finalCompletion?.textResponse ||
|
||||||
"I reached the maximum number of tool calls allowed for a single response. Here is what I have so far based on the tools I was able to run."
|
"I reached the maximum number of tool calls allowed for a single response. Here is what I have so far based on the tools I was able to run."
|
||||||
@ -874,6 +922,7 @@ ${this.getHistory({ to: route.to })
|
|||||||
uuid: msgUUID,
|
uuid: msgUUID,
|
||||||
metrics: provider.getUsage(),
|
metrics: provider.getUsage(),
|
||||||
});
|
});
|
||||||
|
this?.flushCitations?.(msgUUID);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -900,6 +949,7 @@ ${this.getHistory({ to: route.to })
|
|||||||
uuid: msgUUID,
|
uuid: msgUUID,
|
||||||
metrics: provider.getUsage(),
|
metrics: provider.getUsage(),
|
||||||
});
|
});
|
||||||
|
this?.flushCitations?.(msgUUID);
|
||||||
return completion?.textResponse;
|
return completion?.textResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,18 +44,20 @@ const chatHistory = {
|
|||||||
_store: async function (aibitat, { prompt, response } = {}) {
|
_store: async function (aibitat, { prompt, response } = {}) {
|
||||||
const invocation = aibitat.handlerProps.invocation;
|
const invocation = aibitat.handlerProps.invocation;
|
||||||
const metrics = aibitat.provider?.getUsage?.() ?? {};
|
const metrics = aibitat.provider?.getUsage?.() ?? {};
|
||||||
|
const citations = aibitat._pendingCitations ?? [];
|
||||||
await WorkspaceChats.new({
|
await WorkspaceChats.new({
|
||||||
workspaceId: Number(invocation.workspace_id),
|
workspaceId: Number(invocation.workspace_id),
|
||||||
prompt,
|
prompt,
|
||||||
response: {
|
response: {
|
||||||
text: response,
|
text: response,
|
||||||
sources: [],
|
sources: citations,
|
||||||
type: "chat",
|
type: "chat",
|
||||||
metrics,
|
metrics,
|
||||||
},
|
},
|
||||||
user: { id: invocation?.user_id || null },
|
user: { id: invocation?.user_id || null },
|
||||||
threadId: invocation?.thread_id || null,
|
threadId: invocation?.thread_id || null,
|
||||||
});
|
});
|
||||||
|
aibitat.clearCitations?.();
|
||||||
},
|
},
|
||||||
_storeSpecial: async function (
|
_storeSpecial: async function (
|
||||||
aibitat,
|
aibitat,
|
||||||
@ -63,11 +65,13 @@ const chatHistory = {
|
|||||||
) {
|
) {
|
||||||
const invocation = aibitat.handlerProps.invocation;
|
const invocation = aibitat.handlerProps.invocation;
|
||||||
const metrics = aibitat.provider?.getUsage?.() ?? {};
|
const metrics = aibitat.provider?.getUsage?.() ?? {};
|
||||||
|
const citations = aibitat._pendingCitations ?? [];
|
||||||
|
const existingSources = options?.sources ?? [];
|
||||||
await WorkspaceChats.new({
|
await WorkspaceChats.new({
|
||||||
workspaceId: Number(invocation.workspace_id),
|
workspaceId: Number(invocation.workspace_id),
|
||||||
prompt,
|
prompt,
|
||||||
response: {
|
response: {
|
||||||
sources: options?.sources ?? [],
|
sources: [...existingSources, ...citations],
|
||||||
// when we have a _storeSpecial called the options param can include a storedResponse() function
|
// when we have a _storeSpecial called the options param can include a storedResponse() function
|
||||||
// that will override the text property to store extra information in, depending on the special type of chat.
|
// that will override the text property to store extra information in, depending on the special type of chat.
|
||||||
text: options.hasOwnProperty("storedResponse")
|
text: options.hasOwnProperty("storedResponse")
|
||||||
@ -79,6 +83,7 @@ const chatHistory = {
|
|||||||
user: { id: invocation?.user_id || null },
|
user: { id: invocation?.user_id || null },
|
||||||
threadId: invocation?.thread_id || null,
|
threadId: invocation?.thread_id || null,
|
||||||
});
|
});
|
||||||
|
aibitat.clearCitations?.();
|
||||||
options?.postSave();
|
options?.postSave();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -136,6 +136,15 @@ const docSummarizer = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Report citation for the document being summarized
|
||||||
|
this.super.addCitation?.({
|
||||||
|
id: docInfo.document_id,
|
||||||
|
title: document.title || filename,
|
||||||
|
text: document.content,
|
||||||
|
chunkSource: null,
|
||||||
|
score: null,
|
||||||
|
});
|
||||||
|
|
||||||
const { TokenManager } = require("../../../helpers/tiktoken");
|
const { TokenManager } = require("../../../helpers/tiktoken");
|
||||||
if (
|
if (
|
||||||
new TokenManager(this.super.model).countFromString(
|
new TokenManager(this.super.model).countFromString(
|
||||||
|
|||||||
@ -111,6 +111,36 @@ const webBrowsing = {
|
|||||||
return `${str.slice(0, length)}...${str.slice(-length)}`;
|
return `${str.slice(0, length)}...${str.slice(-length)}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report citations for an array of search results.
|
||||||
|
* Uses title, link, and snippet directly from result data.
|
||||||
|
* @param {Array<{title?: string, link?: string, snippet?: string}>} results - Search results to report as citations
|
||||||
|
*/
|
||||||
|
reportSearchResultsCitations: function (results) {
|
||||||
|
if (!Array.isArray(results)) return;
|
||||||
|
const citations = [];
|
||||||
|
for (const result of results) {
|
||||||
|
const fallbackUrl =
|
||||||
|
result.link ||
|
||||||
|
result.url ||
|
||||||
|
result.website ||
|
||||||
|
result.product_link ||
|
||||||
|
result.patent_link ||
|
||||||
|
result.link_clean;
|
||||||
|
|
||||||
|
citations.push({
|
||||||
|
id: result.link || fallbackUrl,
|
||||||
|
title: result.title || fallbackUrl,
|
||||||
|
text: result.snippet || result.description || result.text || "",
|
||||||
|
chunkSource: result.link
|
||||||
|
? `link://${result.link}`
|
||||||
|
: `link://${fallbackUrl}`,
|
||||||
|
score: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.super.addCitation?.(citations);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use SerpApi
|
* Use SerpApi
|
||||||
* SerpApi supports dozens of search engines across the major platforms including Google, DuckDuckGo, Bing, eBay, Amazon, Baidu, Yandex, and more.
|
* SerpApi supports dozens of search engines across the major platforms including Google, DuckDuckGo, Bing, eBay, Amazon, Baidu, Yandex, and more.
|
||||||
@ -362,6 +392,7 @@ const webBrowsing = {
|
|||||||
if (data.length === 0)
|
if (data.length === 0)
|
||||||
return `No information was found online for the search query.`;
|
return `No information was found online for the search query.`;
|
||||||
|
|
||||||
|
this.reportSearchResultsCitations(data);
|
||||||
const result = JSON.stringify(data);
|
const result = JSON.stringify(data);
|
||||||
this.super.introspect(
|
this.super.introspect(
|
||||||
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
||||||
@ -436,6 +467,7 @@ const webBrowsing = {
|
|||||||
if (data.length === 0)
|
if (data.length === 0)
|
||||||
return `No information was found online for the search query.`;
|
return `No information was found online for the search query.`;
|
||||||
|
|
||||||
|
this.reportSearchResultsCitations(data);
|
||||||
const result = JSON.stringify(data);
|
const result = JSON.stringify(data);
|
||||||
this.super.introspect(
|
this.super.introspect(
|
||||||
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
||||||
@ -504,6 +536,7 @@ const webBrowsing = {
|
|||||||
if (data.length === 0)
|
if (data.length === 0)
|
||||||
return `No information was found online for the search query.`;
|
return `No information was found online for the search query.`;
|
||||||
|
|
||||||
|
this.reportSearchResultsCitations(data);
|
||||||
const result = JSON.stringify(data);
|
const result = JSON.stringify(data);
|
||||||
this.super.introspect(
|
this.super.introspect(
|
||||||
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
||||||
@ -559,6 +592,7 @@ const webBrowsing = {
|
|||||||
if (searchResponse.length === 0)
|
if (searchResponse.length === 0)
|
||||||
return `No information was found online for the search query.`;
|
return `No information was found online for the search query.`;
|
||||||
|
|
||||||
|
this.reportSearchResultsCitations(searchResponse);
|
||||||
const result = JSON.stringify(searchResponse);
|
const result = JSON.stringify(searchResponse);
|
||||||
this.super.introspect(
|
this.super.introspect(
|
||||||
`${this.caller}: I found ${searchResponse.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
`${this.caller}: I found ${searchResponse.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
||||||
@ -643,6 +677,7 @@ const webBrowsing = {
|
|||||||
if (data.length === 0)
|
if (data.length === 0)
|
||||||
return `No information was found online for the search query.`;
|
return `No information was found online for the search query.`;
|
||||||
|
|
||||||
|
this.reportSearchResultsCitations(data);
|
||||||
const result = JSON.stringify(data);
|
const result = JSON.stringify(data);
|
||||||
this.super.introspect(
|
this.super.introspect(
|
||||||
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
||||||
@ -715,6 +750,7 @@ const webBrowsing = {
|
|||||||
if (data.length === 0)
|
if (data.length === 0)
|
||||||
return `No information was found online for the search query.`;
|
return `No information was found online for the search query.`;
|
||||||
|
|
||||||
|
this.reportSearchResultsCitations(data);
|
||||||
const result = JSON.stringify(data);
|
const result = JSON.stringify(data);
|
||||||
this.super.introspect(
|
this.super.introspect(
|
||||||
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
||||||
@ -778,6 +814,7 @@ const webBrowsing = {
|
|||||||
if (data.length === 0)
|
if (data.length === 0)
|
||||||
return `No information was found online for the search query.`;
|
return `No information was found online for the search query.`;
|
||||||
|
|
||||||
|
this.reportSearchResultsCitations(data);
|
||||||
const result = JSON.stringify(data);
|
const result = JSON.stringify(data);
|
||||||
this.super.introspect(
|
this.super.introspect(
|
||||||
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
||||||
@ -785,6 +822,26 @@ const webBrowsing = {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
_duckDuckGoEngine: async function (query) {
|
_duckDuckGoEngine: async function (query) {
|
||||||
|
/**
|
||||||
|
* Extract the actual destination URL from a DuckDuckGo redirect link.
|
||||||
|
* DDG links look like: //duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.com&rut=...
|
||||||
|
* @param {string} ddgLink - The DuckDuckGo redirect link
|
||||||
|
* @returns {string} The actual destination URL
|
||||||
|
*/
|
||||||
|
function extractUrl(ddgLink) {
|
||||||
|
if (!ddgLink) return ddgLink;
|
||||||
|
try {
|
||||||
|
const fullUrl = ddgLink.startsWith("//")
|
||||||
|
? `https:${ddgLink}`
|
||||||
|
: ddgLink;
|
||||||
|
const url = new URL(fullUrl);
|
||||||
|
const actualUrl = url.searchParams.get("uddg");
|
||||||
|
return actualUrl ? decodeURIComponent(actualUrl) : ddgLink;
|
||||||
|
} catch {
|
||||||
|
return ddgLink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.super.introspect(
|
this.super.introspect(
|
||||||
`${this.caller}: Using DuckDuckGo to search for "${
|
`${this.caller}: Using DuckDuckGo to search for "${
|
||||||
query.length > 100 ? `${query.slice(0, 100)}...` : query
|
query.length > 100 ? `${query.slice(0, 100)}...` : query
|
||||||
@ -823,11 +880,11 @@ const webBrowsing = {
|
|||||||
);
|
);
|
||||||
const title = titleMatch ? titleMatch[1].trim() : "";
|
const title = titleMatch ? titleMatch[1].trim() : "";
|
||||||
|
|
||||||
// Extract URL
|
// Extract URL and clean DDG redirect
|
||||||
const urlMatch = result.match(
|
const urlMatch = result.match(
|
||||||
/<a[^>]*class="result__a"[^>]*href="([^"]*)">/
|
/<a[^>]*class="result__a"[^>]*href="([^"]*)">/
|
||||||
);
|
);
|
||||||
const link = urlMatch ? urlMatch[1] : "";
|
const link = extractUrl(urlMatch ? urlMatch[1] : "");
|
||||||
|
|
||||||
// Extract snippet
|
// Extract snippet
|
||||||
const snippetMatch = result.match(
|
const snippetMatch = result.match(
|
||||||
@ -846,6 +903,7 @@ const webBrowsing = {
|
|||||||
return `No information was found online for the search query.`;
|
return `No information was found online for the search query.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.reportSearchResultsCitations(data);
|
||||||
const result = JSON.stringify(data);
|
const result = JSON.stringify(data);
|
||||||
this.super.introspect(
|
this.super.introspect(
|
||||||
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
||||||
@ -913,6 +971,7 @@ const webBrowsing = {
|
|||||||
if (data.length === 0)
|
if (data.length === 0)
|
||||||
return `No information was found online for the search query.`;
|
return `No information was found online for the search query.`;
|
||||||
|
|
||||||
|
this.reportSearchResultsCitations(data);
|
||||||
const result = JSON.stringify(data);
|
const result = JSON.stringify(data);
|
||||||
this.super.introspect(
|
this.super.introspect(
|
||||||
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
|
||||||
|
|||||||
@ -55,6 +55,33 @@ const webScraping = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report a URL citation to be displayed in the chat UI.
|
||||||
|
* @param {string} url - The URL that was accessed
|
||||||
|
* @param {string} content - The content retrieved from the URL
|
||||||
|
*/
|
||||||
|
reportUrlCitation: function (url, content) {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
this.super.addCitation?.({
|
||||||
|
id: url,
|
||||||
|
title: urlObj.hostname + urlObj.pathname,
|
||||||
|
text: content,
|
||||||
|
chunkSource: `link://${url}`,
|
||||||
|
score: null,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// URL parsing failed, still add citation without parsed title
|
||||||
|
this.super.addCitation?.({
|
||||||
|
id: url,
|
||||||
|
title: url,
|
||||||
|
text: content,
|
||||||
|
chunkSource: `link://${url}`,
|
||||||
|
score: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scrape a website and summarize the content based on objective if the content is too large.
|
* Scrape a website and summarize the content based on objective if the content is too large.
|
||||||
* Objective is the original objective & task that user give to the agent, url is the url of the website to be scraped.
|
* Objective is the original objective & task that user give to the agent, url is the url of the website to be scraped.
|
||||||
@ -83,6 +110,7 @@ const webScraping = {
|
|||||||
throw new Error("There was no content to be collected or read.");
|
throw new Error("There was no content to be collected or read.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.reportUrlCitation(url, content);
|
||||||
const { TokenManager } = require("../../../helpers/tiktoken");
|
const { TokenManager } = require("../../../helpers/tiktoken");
|
||||||
const tokenEstimate = new TokenManager(
|
const tokenEstimate = new TokenManager(
|
||||||
this.super.model
|
this.super.model
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user