diff --git a/templates/components/engines/typescript/agent/tools/interpreter.ts b/templates/components/engines/typescript/agent/tools/interpreter.ts index d34aa6c167bca6a3a677ca3f57a06fac50de9150..cad7a0a0f474bd001fe3d732a4327435b98a6a3c 100644 --- a/templates/components/engines/typescript/agent/tools/interpreter.ts +++ b/templates/components/engines/typescript/agent/tools/interpreter.ts @@ -15,7 +15,7 @@ export type InterpreterToolParams = { fileServerURLPrefix?: string; }; -export type InterpreterToolOuput = { +export type InterpreterToolOutput = { isError: boolean; logs: Logs; extraResult: InterpreterExtraResult[]; @@ -88,7 +88,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> { return this.codeInterpreter; } - public async codeInterpret(code: string): Promise<InterpreterToolOuput> { + public async codeInterpret(code: string): Promise<InterpreterToolOutput> { console.log( `\n${"=".repeat(50)}\n> Running following AI-generated code:\n${code}\n${"=".repeat(50)}`, ); @@ -96,7 +96,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> { const exec = await interpreter.notebook.execCell(code); if (exec.error) console.error("[Code Interpreter error]", exec.error); const extraResult = await this.getExtraResult(exec.results[0]); - const result: InterpreterToolOuput = { + const result: InterpreterToolOutput = { isError: !!exec.error, logs: exec.logs, extraResult, @@ -104,7 +104,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> { return result; } - async call(input: InterpreterParameter): Promise<InterpreterToolOuput> { + async call(input: InterpreterParameter): Promise<InterpreterToolOutput> { const result = await this.codeInterpret(input.code); await this.codeInterpreter?.close(); return result; diff --git a/templates/types/streaming/express/src/controllers/stream-helper.ts b/templates/types/streaming/express/src/controllers/stream-helper.ts index 9f1a8864bfa63e06e8e622a641075cd34ac604d8..ffc5dfc58c8e6fb0d705f05b2689bf02609a6970 100644 --- a/templates/types/streaming/express/src/controllers/stream-helper.ts +++ b/templates/types/streaming/express/src/controllers/stream-helper.ts @@ -17,6 +17,22 @@ export function appendImageData(data: StreamData, imageUrl?: string) { }); } +function getNodeUrl(metadata: Metadata) { + const url = metadata["URL"]; + if (url) return url; + const fileName = metadata["file_name"]; + if (!process.env.FILESERVER_URL_PREFIX) { + console.warn( + "FILESERVER_URL_PREFIX is not set. File URLs will not be generated.", + ); + return undefined; + } + if (fileName) { + return `${process.env.FILESERVER_URL_PREFIX}/data/${fileName}`; + } + return undefined; +} + export function appendSourceData( data: StreamData, sourceNodes?: NodeWithScore<Metadata>[], @@ -29,6 +45,7 @@ export function appendSourceData( ...node.node.toMutableJSON(), id: node.node.id_, score: node.score ?? null, + url: getNodeUrl(node.node.metadata), })), }, }); diff --git a/templates/types/streaming/fastapi/app/api/routers/chat.py b/templates/types/streaming/fastapi/app/api/routers/chat.py index a23cc44096c8f1b277cd2dddc4ab5035511512d2..aad9f6e8ffb2e9861c7cc2ddfca3942dbbef88c8 100644 --- a/templates/types/streaming/fastapi/app/api/routers/chat.py +++ b/templates/types/streaming/fastapi/app/api/routers/chat.py @@ -1,3 +1,5 @@ +import os +import logging from pydantic import BaseModel from typing import List, Any, Optional, Dict, Tuple from fastapi import APIRouter, Depends, HTTPException, Request, status @@ -11,6 +13,7 @@ from aiostream import stream chat_router = r = APIRouter() +logger = logging.getLogger("uvicorn") class _Message(BaseModel): role: MessageRole @@ -38,14 +41,27 @@ class _SourceNodes(BaseModel): metadata: Dict[str, Any] score: Optional[float] text: str + url: Optional[str] @classmethod def from_source_node(cls, source_node: NodeWithScore): + metadata = source_node.node.metadata + url = metadata.get("URL") + + if not url: + file_name = metadata.get("file_name") + url_prefix = os.getenv("FILESERVER_URL_PREFIX") + if not url_prefix: + logger.warning("Warning: FILESERVER_URL_PREFIX not set in environment variables") + if file_name and url_prefix: + url = f"{url_prefix}/data/{file_name}" + return cls( id=source_node.node.node_id, - metadata=source_node.node.metadata, + metadata=metadata, score=source_node.score, text=source_node.node.text, # type: ignore + url=url ) @classmethod diff --git a/templates/types/streaming/nextjs/app/api/chat/stream-helper.ts b/templates/types/streaming/nextjs/app/api/chat/stream-helper.ts index 9f1a8864bfa63e06e8e622a641075cd34ac604d8..ffc5dfc58c8e6fb0d705f05b2689bf02609a6970 100644 --- a/templates/types/streaming/nextjs/app/api/chat/stream-helper.ts +++ b/templates/types/streaming/nextjs/app/api/chat/stream-helper.ts @@ -17,6 +17,22 @@ export function appendImageData(data: StreamData, imageUrl?: string) { }); } +function getNodeUrl(metadata: Metadata) { + const url = metadata["URL"]; + if (url) return url; + const fileName = metadata["file_name"]; + if (!process.env.FILESERVER_URL_PREFIX) { + console.warn( + "FILESERVER_URL_PREFIX is not set. File URLs will not be generated.", + ); + return undefined; + } + if (fileName) { + return `${process.env.FILESERVER_URL_PREFIX}/data/${fileName}`; + } + return undefined; +} + export function appendSourceData( data: StreamData, sourceNodes?: NodeWithScore<Metadata>[], @@ -29,6 +45,7 @@ export function appendSourceData( ...node.node.toMutableJSON(), id: node.node.id_, score: node.score ?? null, + url: getNodeUrl(node.node.metadata), })), }, }); diff --git a/templates/types/streaming/nextjs/app/components/ui/chat/chat-sources.tsx b/templates/types/streaming/nextjs/app/components/ui/chat/chat-sources.tsx index 893541b09bb5cd0cf1f07d9a5bd0d4341a293f52..eb51b9aa80178ed80d427af3ce036195e11b227a 100644 --- a/templates/types/streaming/nextjs/app/components/ui/chat/chat-sources.tsx +++ b/templates/types/streaming/nextjs/app/components/ui/chat/chat-sources.tsx @@ -2,12 +2,10 @@ import { Check, Copy } from "lucide-react"; import { useMemo } from "react"; import { Button } from "../button"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "../hover-card"; -import { getStaticFileDataUrl } from "../lib/url"; -import { SourceData, SourceNode } from "./index"; +import { SourceData } from "./index"; import { useCopyToClipboard } from "./use-copy-to-clipboard"; import PdfDialog from "./widgets/PdfDialog"; -const DATA_SOURCE_FOLDER = "data"; const SCORE_THRESHOLD = 0.3; function SourceNumberButton({ index }: { index: number }) { @@ -18,46 +16,11 @@ function SourceNumberButton({ index }: { index: number }) { ); } -enum NODE_TYPE { - URL, - FILE, - UNKNOWN, -} - type NodeInfo = { id: string; - type: NODE_TYPE; - path?: string; url?: string; }; -function getNodeInfo(node: SourceNode): NodeInfo { - if (typeof node.metadata["URL"] === "string") { - const url = node.metadata["URL"]; - return { - id: node.id, - type: NODE_TYPE.URL, - path: url, - url, - }; - } - if (typeof node.metadata["file_path"] === "string") { - const fileName = node.metadata["file_name"] as string; - const filePath = `${DATA_SOURCE_FOLDER}/${fileName}`; - return { - id: node.id, - type: NODE_TYPE.FILE, - path: node.metadata["file_path"], - url: getStaticFileDataUrl(filePath), - }; - } - - return { - id: node.id, - type: NODE_TYPE.UNKNOWN, - }; -} - export function ChatSources({ data }: { data: SourceData }) { const sources: NodeInfo[] = useMemo(() => { // aggregate nodes by url or file_path (get the highest one by score) @@ -67,8 +30,11 @@ export function ChatSources({ data }: { data: SourceData }) { .filter((node) => (node.score ?? 1) > SCORE_THRESHOLD) .sort((a, b) => (b.score ?? 1) - (a.score ?? 1)) .forEach((node) => { - const nodeInfo = getNodeInfo(node); - const key = nodeInfo.path ?? nodeInfo.id; // use id as key for UNKNOWN type + const nodeInfo = { + id: node.id, + url: node.url, + }; + const key = nodeInfo.url ?? nodeInfo.id; // use id as key for UNKNOWN type if (!nodesByPath[key]) { nodesByPath[key] = nodeInfo; } @@ -84,13 +50,12 @@ export function ChatSources({ data }: { data: SourceData }) { <span className="font-semibold">Sources:</span> <div className="inline-flex gap-1 items-center"> {sources.map((nodeInfo: NodeInfo, index: number) => { - if (nodeInfo.path?.endsWith(".pdf")) { + if (nodeInfo.url?.endsWith(".pdf")) { return ( <PdfDialog key={nodeInfo.id} documentId={nodeInfo.id} url={nodeInfo.url!} - path={nodeInfo.path} trigger={<SourceNumberButton index={index} />} /> ); @@ -116,16 +81,16 @@ export function ChatSources({ data }: { data: SourceData }) { function NodeInfo({ nodeInfo }: { nodeInfo: NodeInfo }) { const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 1000 }); - if (nodeInfo.type !== NODE_TYPE.UNKNOWN) { + if (nodeInfo.url) { // this is a node generated by the web loader or file loader, // add a link to view its URL and a button to copy the URL to the clipboard return ( <div className="flex items-center my-2"> <a className="hover:text-blue-900" href={nodeInfo.url} target="_blank"> - <span>{nodeInfo.path}</span> + <span>{nodeInfo.url}</span> </a> <Button - onClick={() => copyToClipboard(nodeInfo.path!)} + onClick={() => copyToClipboard(nodeInfo.url!)} size="icon" variant="ghost" className="h-12 w-12 shrink-0" diff --git a/templates/types/streaming/nextjs/app/components/ui/chat/index.ts b/templates/types/streaming/nextjs/app/components/ui/chat/index.ts index 106f6294bd5138b4636da6582fc4370f027eb9d8..cb7e9272d8a6c0ea3025935d627fcf1b41bfc622 100644 --- a/templates/types/streaming/nextjs/app/components/ui/chat/index.ts +++ b/templates/types/streaming/nextjs/app/components/ui/chat/index.ts @@ -21,6 +21,7 @@ export type SourceNode = { metadata: Record<string, unknown>; score?: number; text: string; + url?: string; }; export type SourceData = { diff --git a/templates/types/streaming/nextjs/app/components/ui/chat/widgets/PdfDialog.tsx b/templates/types/streaming/nextjs/app/components/ui/chat/widgets/PdfDialog.tsx index 00274546c2132ac9e4a3f76b1f827fc129abb664..42c2ad47d09b19c138acbe39525acad8abb6cb28 100644 --- a/templates/types/streaming/nextjs/app/components/ui/chat/widgets/PdfDialog.tsx +++ b/templates/types/streaming/nextjs/app/components/ui/chat/widgets/PdfDialog.tsx @@ -12,7 +12,6 @@ import { export interface PdfDialogProps { documentId: string; - path: string; url: string; trigger: React.ReactNode; } @@ -26,13 +25,13 @@ export default function PdfDialog(props: PdfDialogProps) { <div className="space-y-2"> <DrawerTitle>PDF Content</DrawerTitle> <DrawerDescription> - File path:{" "} + File URL:{" "} <a className="hover:text-blue-900" href={props.url} target="_blank" > - {props.path} + {props.url} </a> </DrawerDescription> </div> diff --git a/templates/types/streaming/nextjs/app/components/ui/lib/url.ts b/templates/types/streaming/nextjs/app/components/ui/lib/url.ts deleted file mode 100644 index 5e2c90e598045cbbfad2dc7ae5624b53ebfe9f92..0000000000000000000000000000000000000000 --- a/templates/types/streaming/nextjs/app/components/ui/lib/url.ts +++ /dev/null @@ -1,11 +0,0 @@ -const staticFileAPI = "/api/files"; - -export const getStaticFileDataUrl = (filePath: string) => { - const isUsingBackend = !!process.env.NEXT_PUBLIC_CHAT_API; - const fileUrl = `${staticFileAPI}/${filePath}`; - if (isUsingBackend) { - const backendOrigin = new URL(process.env.NEXT_PUBLIC_CHAT_API!).origin; - return `${backendOrigin}${fileUrl}`; - } - return fileUrl; -};