125 |
126 | );
127 | }
128 |
--------------------------------------------------------------------------------
/degreegurucrawler/degreegurucrawler/middlewares.py:
--------------------------------------------------------------------------------
1 | # Define here the models for your spider middleware
2 | #
3 | # See documentation in:
4 | # https://docs.scrapy.org/en/latest/topics/spider-middleware.html
5 |
6 | from scrapy import signals
7 |
8 | # useful for handling different item types with a single interface
9 | from itemadapter import is_item, ItemAdapter
10 |
11 |
12 | class DegreegurucrawlerSpiderMiddleware:
13 | # Not all methods need to be defined. If a method is not defined,
14 | # scrapy acts as if the spider middleware does not modify the
15 | # passed objects.
16 |
17 | @classmethod
18 | def from_crawler(cls, crawler):
19 | # This method is used by Scrapy to create your spiders.
20 | s = cls()
21 | crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
22 | return s
23 |
24 | def process_spider_input(self, response, spider):
25 | # Called for each response that goes through the spider
26 | # middleware and into the spider.
27 |
28 | # Should return None or raise an exception.
29 | return None
30 |
31 | def process_spider_output(self, response, result, spider):
32 | # Called with the results returned from the Spider, after
33 | # it has processed the response.
34 |
35 | # Must return an iterable of Request, or item objects.
36 | for i in result:
37 | yield i
38 |
39 | def process_spider_exception(self, response, exception, spider):
40 | # Called when a spider or process_spider_input() method
41 | # (from other spider middleware) raises an exception.
42 |
43 | # Should return either None or an iterable of Request or item objects.
44 | pass
45 |
46 | def process_start_requests(self, start_requests, spider):
47 | # Called with the start requests of the spider, and works
48 | # similarly to the process_spider_output() method, except
49 | # that it doesn’t have a response associated.
50 |
51 | # Must return only requests (not items).
52 | for r in start_requests:
53 | yield r
54 |
55 | def spider_opened(self, spider):
56 | spider.logger.info("Spider opened: %s" % spider.name)
57 |
58 |
59 | class DegreegurucrawlerDownloaderMiddleware:
60 | # Not all methods need to be defined. If a method is not defined,
61 | # scrapy acts as if the downloader middleware does not modify the
62 | # passed objects.
63 |
64 | @classmethod
65 | def from_crawler(cls, crawler):
66 | # This method is used by Scrapy to create your spiders.
67 | s = cls()
68 | crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
69 | return s
70 |
71 | def process_request(self, request, spider):
72 | # Called for each request that goes through the downloader
73 | # middleware.
74 |
75 | # Must either:
76 | # - return None: continue processing this request
77 | # - or return a Response object
78 | # - or return a Request object
79 | # - or raise IgnoreRequest: process_exception() methods of
80 | # installed downloader middleware will be called
81 | return None
82 |
83 | def process_response(self, request, response, spider):
84 | # Called with the response returned from the downloader.
85 |
86 | # Must either;
87 | # - return a Response object
88 | # - return a Request object
89 | # - or raise IgnoreRequest
90 | return response
91 |
92 | def process_exception(self, request, exception, spider):
93 | # Called when a download handler or a process_request()
94 | # (from other downloader middleware) raises an exception.
95 |
96 | # Must either:
97 | # - return None: continue processing this exception
98 | # - return a Response object: stops process_exception() chain
99 | # - return a Request object: stops process_exception() chain
100 | pass
101 |
102 | def spider_opened(self, spider):
103 | spider.logger.info("Spider opened: %s" % spider.name)
104 |
--------------------------------------------------------------------------------
/src/app/api/guru/route.tsx:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 |
3 | import { Ratelimit } from "@upstash/ratelimit";
4 | import { Redis } from "@upstash/redis";
5 |
6 | import { Message as VercelChatMessage, StreamingTextResponse } from "ai";
7 |
8 | import { AIMessage, ChatMessage, HumanMessage } from "@langchain/core/messages";
9 | import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
10 | import { createRetrieverTool } from "langchain/tools/retriever";
11 | import { AgentExecutor, createOpenAIFunctionsAgent } from "langchain/agents";
12 | import {
13 | ChatPromptTemplate,
14 | MessagesPlaceholder,
15 | } from "@langchain/core/prompts";
16 |
17 | import { UpstashVectorStore } from "@/app/vectorstore/UpstashVectorStore";
18 |
19 | export const runtime = "edge";
20 |
21 | const redis = Redis.fromEnv();
22 |
23 | const ratelimit = new Ratelimit({
24 | redis: redis,
25 | limiter: Ratelimit.slidingWindow(1, "10 s"),
26 | });
27 |
28 | const convertVercelMessageToLangChainMessage = (message: VercelChatMessage) => {
29 | if (message.role === "user") {
30 | return new HumanMessage(message.content);
31 | } else if (message.role === "assistant") {
32 | return new AIMessage(message.content);
33 | } else {
34 | return new ChatMessage(message.content, message.role);
35 | }
36 | };
37 |
38 | export async function POST(req: NextRequest) {
39 | try {
40 | const ip = req.ip ?? "127.0.0.1";
41 | const { success } = await ratelimit.limit(ip);
42 |
43 | if (!success) {
44 | const textEncoder = new TextEncoder();
45 | const customString =
46 | "Oops! It seems you've reached the rate limit. Please try again later.";
47 |
48 | const transformStream = new ReadableStream({
49 | async start(controller) {
50 | controller.enqueue(textEncoder.encode(customString));
51 | controller.close();
52 | },
53 | });
54 | return new StreamingTextResponse(transformStream);
55 | }
56 |
57 | const body = await req.json();
58 |
59 | /**
60 | * We represent intermediate steps as system messages for display purposes,
61 | * but don't want them in the chat history.
62 | */
63 | const messages = (body.messages ?? []).filter(
64 | (message: VercelChatMessage) =>
65 | message.role === "user" || message.role === "assistant",
66 | );
67 | const returnIntermediateSteps = false;
68 | const previousMessages = messages
69 | .slice(0, -1)
70 | .map(convertVercelMessageToLangChainMessage);
71 | const currentMessageContent = messages[messages.length - 1].content;
72 |
73 | const chatModel = new ChatOpenAI({
74 | modelName: "gpt-3.5-turbo-1106",
75 | temperature: 0.2,
76 | // IMPORTANT: Must "streaming: true" on OpenAI to enable final output streaming below.
77 | streaming: true,
78 | }, {
79 | apiKey: process.env.OPENAI_API_KEY,
80 | organization: process.env.OPENAI_ORGANIZATION
81 | });
82 |
83 | /**
84 | * Create vector store and retriever
85 | */
86 | const vectorstore = await new UpstashVectorStore(new OpenAIEmbeddings());
87 | const retriever = vectorstore.asRetriever(
88 | {
89 | k: 6,
90 | searchType: "mmr",
91 | searchKwargs: {
92 | fetchK: 20,
93 | lambda: 0.5
94 | },
95 | verbose: false
96 | },
97 | );
98 |
99 | /**
100 | * Wrap the retriever in a tool to present it to the agent in a
101 | * usable form.
102 | */
103 | const tool = createRetrieverTool(retriever, {
104 | name: "search_latest_knowledge",
105 | description: "Searches and returns up-to-date general information.",
106 | });
107 |
108 | /**
109 | * Based on https://smith.langchain.com/hub/hwchase17/openai-functions-agent
110 | *
111 | * This default prompt for the OpenAI functions agent has a placeholder
112 | * where chat messages get inserted as "chat_history".
113 | *
114 | * You can customize this prompt yourself!
115 | */
116 |
117 | const AGENT_SYSTEM_TEMPLATE = `
118 | You are an artificial intelligence university bot named DegreeGuru, programmed to respond to inquiries about Stanford in a highly systematic and data-driven manner.
119 |
120 | Begin your answers with a formal greeting and sign off with a closing statement about promoting knowledge.
121 |
122 | Your responses should be precise and factual, with an emphasis on using the context provided and providing links from the context whenever posible. If some link does not look like it belongs to stanford, don't use the link and the information in your response.
123 |
124 | Don't repeat yourself in your responses even if some information is repeated in the context.
125 |
126 | Reply with apologies and tell the user that you don't know the answer only when you are faced with a question whose answer is not available in the context.
127 | `;
128 |
129 | const prompt = ChatPromptTemplate.fromMessages([
130 | ["system", AGENT_SYSTEM_TEMPLATE],
131 | new MessagesPlaceholder("chat_history"),
132 | ["human", "{input}"],
133 | new MessagesPlaceholder("agent_scratchpad"),
134 | ]);
135 |
136 | const agent = await createOpenAIFunctionsAgent({
137 | llm: chatModel,
138 | tools: [tool],
139 | prompt,
140 | });
141 |
142 | const agentExecutor = new AgentExecutor({
143 | agent,
144 | tools: [tool],
145 | // Set this if you want to receive all intermediate steps in the output of .invoke().
146 | returnIntermediateSteps,
147 | });
148 |
149 | if (!returnIntermediateSteps) {
150 | /**
151 | * Agent executors also allow you to stream back all generated tokens and steps
152 | * from their runs.
153 | *
154 | * This contains a lot of data, so we do some filtering of the generated log chunks
155 | * and only stream back the final response.
156 | *
157 | * This filtering is easiest with the OpenAI functions or tools agents, since final outputs
158 | * are log chunk values from the model that contain a string instead of a function call object.
159 | *
160 | * See: https://js.langchain.com/docs/modules/agents/how_to/streaming#streaming-tokens
161 | */
162 | const logStream = await agentExecutor.streamLog({
163 | input: currentMessageContent,
164 | chat_history: previousMessages,
165 | });
166 |
167 | const textEncoder = new TextEncoder();
168 | const transformStream = new ReadableStream({
169 | async start(controller) {
170 | for await (const chunk of logStream) {
171 | if (chunk.ops?.length > 0 && chunk.ops[0].op === "add") {
172 | const addOp = chunk.ops[0];
173 | if (
174 | addOp.path.startsWith("/logs/ChatOpenAI") &&
175 | typeof addOp.value === "string" &&
176 | addOp.value.length
177 | ) {
178 | controller.enqueue(textEncoder.encode(addOp.value));
179 | }
180 | }
181 | }
182 | controller.close();
183 | },
184 | });
185 |
186 | return new StreamingTextResponse(transformStream);
187 | } else {
188 | /**
189 | * Intermediate steps are the default outputs with the executor's `.stream()` method.
190 | * We could also pick them out from `streamLog` chunks.
191 | * They are generated as JSON objects, so streaming them is a bit more complicated.
192 | */
193 | const result = await agentExecutor.invoke({
194 | input: currentMessageContent,
195 | chat_history: previousMessages,
196 | });
197 |
198 | const urls = JSON.parse(
199 | `[${result.intermediateSteps[0]?.observation.replaceAll("}\n\n{", "}, {")}]`,
200 | ).map((source: { url: any }) => source.url);
201 |
202 | return NextResponse.json(
203 | {
204 | _no_streaming_response_: true,
205 | output: result.output,
206 | sources: urls,
207 | },
208 | { status: 200 },
209 | );
210 | }
211 | } catch (e: any) {
212 | console.log(e.message);
213 | return NextResponse.json({ error: e.message }, { status: 500 });
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DegreeGuru
2 |
3 | ## Build a RAG Chatbot using Vercel AI SDK, Langchain, Upstash Vector and OpenAI
4 |
5 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fupstash%2Fdegreeguru&env=UPSTASH_REDIS_REST_URL,UPSTASH_REDIS_REST_TOKEN,UPSTASH_VECTOR_REST_URL,UPSTASH_VECTOR_REST_TOKEN,OPENAI_API_KEY&demo-title=DegreeGuru%20Demo&demo-description=A%20Demo%20Showcasing%20the%20DegreeGuru%20App&demo-url=https%3A%2F%2Fdegreeguru.vercel.app%2F&demo-image=https%3A%2F%2Fupstash.com%2Ficons%2Ffavicon-32x32.png)
6 |
7 | 
8 |
9 | > [!NOTE]
10 | > **This project is a Community Project.**
11 | >
12 | > The project is maintained and supported by the community. Upstash may contribute but does not officially support or assume responsibility for it.
13 |
14 | **DegreeGuru** is a project designed to teach you making your own AI RAG chatbot on any custom data. Some of our favorite features:
15 |
16 | - 🕷️ Built-in crawler that scrapes the website you point it to, automatically making this data available for the AI
17 | - ⚡ Fast answers using Upstash Vector and real-time data streaming
18 | - 🛡️ Includes rate limiting to prevent API abuse
19 |
20 | This chatbot is trained on data from Stanford University as an example, but is totally domain agnostic. We've created this project so you can turn it into a chatbot with your very own data by simply modifying the `crawler.yaml` file.
21 |
22 | ## Overview
23 |
24 | 1. [Stack](#stack)
25 | 2. [Quickstart](#quickstart)
26 | 1. [Crawler](#crawler)
27 | 2. [ChatBot](#chatbot)
28 | 3. [Conclusion](#conclusion)
29 | 4. [Shortcomings](#shortcomings)
30 |
31 | ## Stack
32 |
33 | - Crawler: [scrapy](https://scrapy.org/)
34 | - Chatbot App: [Next.js](https://nextjs.org/)
35 | - Vector DB: [Upstash](https://upstash.com/)
36 | - LLM Orchestration: [Langchain.js](https://js.langchain.com)
37 | - Generative Model: [OpenAI](https://openai.com/), [gpt-3.5-turbo-1106](https://platform.openai.com/docs/models)
38 | - Embedding Model: [OpenAI](https://openai.com/), [text-embedding-ada-002](https://platform.openai.com/docs/guides/embeddings)
39 | - Text Streaming: [Vercel AI](https://vercel.com/ai)
40 | - Rate Limiting: [Upstash](https://upstash.com/)
41 |
42 | ## Quickstart
43 |
44 | For local development, we recommend forking this project and cloning the forked repository to your local machine by running the following command:
45 |
46 | ```
47 | git clone git@github.com:[YOUR_GITHUB_ACCOUNT]/DegreeGuru.git
48 | ```
49 |
50 | This project contains two primary components: the crawler and the chatbot. First, we'll take a look at how the crawler extracts information from any website you point it to. This data is automatically stored in an Upstash Vector database. If you already have a vector database available, the crawling stage can be skipped.
51 |
52 | ### Step 1: Crawler
53 |
54 | 
55 |
56 | The crawler is developed using Python, by [initializing a Scrapy project](https://docs.scrapy.org/en/latest/intro/tutorial.html#creating-a-project) and implementing a [custom spider](https://github.com/upstash/degreeguru/blob/master/degreegurucrawler/degreegurucrawler/spiders/configurable.py). The spider is equipped with [the `parse_page` function](https://github.com/upstash/degreeguru/blob/master/degreegurucrawler/degreegurucrawler/spiders/configurable.py#L42), invoked each time the spider visits a webpage. This callback function splits the text on the webpage into chunks, generates vector embeddings for each chunk, and upserts those vectors into your Upstash Vector Database. Each vector stored in our database includes the original text and website URL as metadata.
57 |
58 |
59 |
60 | To run the crawler, follow these steps:
61 |
62 | > [!TIP]
63 | > If you have docker installed, you can skip the "Configure Environment Variables" and "Install Required Python Libraries" sections. Instead you can simply update the environment variables in [docker-compose.yml](https://github.com/upstash/DegreeGuru/blob/master/degreegurucrawler/docker-compose.yml) and run `docker-compose up`. This will create a container running our crawler. Don't forget to configure the crawler as explained in the following sections!
64 |
65 |
66 |
67 | Configure Environment Variables
68 | Before we can run our crawler, we need to configure environment variables. They let us securely store sensitive information, such as the API keys we need to communicate with OpenAI or Upstash Vector.
69 |
70 | If you don't already have an Upstash Vector Database, create one [here](https://console.upstash.com/vector) and set 1536 as the vector dimensions. We set 1536 here because that is the amount needed by the embedding model we will use.
71 |
72 | 
73 |
74 | The following environment variables should be set:
75 |
76 | ```
77 | # Upstash Vector credentials retrieved here: https://console.upstash.com/vector
78 | UPSTASH_VECTOR_REST_URL=****
79 | UPSTASH_VECTOR_REST_TOKEN=****
80 |
81 | # OpenAI key retrieved here: https://platform.openai.com/api-keys
82 | OPENAI_API_KEY=****
83 | ```
84 |
85 |
86 |
87 |
88 | Install Required Python Libraries
89 |
90 | To install the libraries, we suggest setting up a virtual Python environment. Before starting the installation, navigate to the `degreegurucrawler` directory.
91 |
92 | To setup a virtual environment, first install `virtualenv` package:
93 |
94 | ```bash
95 | pip install virtualenv
96 | ```
97 |
98 | Then, create a new virtual environment and activate it:
99 |
100 | ```bash
101 | # create environment
102 | python3 -m venv venv
103 |
104 | # activate environment
105 | source venv/bin/activate
106 | ```
107 |
108 | Finally, use [the `requirements.txt`](https://github.com/upstash/degreeguru/blob/master/degreegurucrawler/requirements.txt) to install the required libraries:
109 |
110 | ```bash
111 | pip install -r requirements.txt
112 | ```
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | After setting these environment variables, we are almost ready to run the crawler. The subsequent step involves configuring the crawler itself, primarily accomplished through the `crawler.yaml` file located in the `degreegurucrawler/utils` directory. Additionally, it is imperative to address a crucial setting within the `settings.py` file.
121 |
122 |
123 | Configuring the crawler in `crawler.yaml`
124 |
125 | The crawler.yaml has two main sections: `crawler` and `index`:
126 |
127 | ```yaml
128 | crawler:
129 | start_urls:
130 | - https://www.some.domain.com
131 | link_extractor:
132 | allow: '.*some\.domain.*'
133 | deny:
134 | - "#"
135 | - '\?'
136 | - about
137 | index:
138 | openAI_embedding_model: text-embedding-ada-002
139 | text_splitter:
140 | chunk_size: 1000
141 | chunk_overlap: 100
142 | ```
143 |
144 | In the `crawler` section, there are two subsections:
145 |
146 | - `start_urls`: the entrypoints our crawler will start searching from
147 | - `link_extractor`: a dictionary passed as arguments to [`scrapy.linkextractors.LinkExtractor`](https://docs.scrapy.org/en/latest/topics/link-extractors.html). Some important parameters are:
148 | - `allow`: Only extracts links matching the given regex(s)
149 | - `allow_domains`: Only extract links matching the given domain(s)
150 | - `deny`: Deny links matching the given regex(s)
151 |
152 | In the `index` section, there are two subsections:
153 |
154 | - `openAI_embedding_model`: The embedding model to use
155 | - `test_splitter`: a dictionary passed as arguments to [`langchain.text_splitter.RecursiveCharacterTextSplitter`](https://api.python.langchain.com/en/latest/text_splitter/langchain.text_splitter.RecursiveCharacterTextSplitter.html)
156 |
157 |
158 |
159 |
160 | Configuring crawl depth via `settings.py`
161 |
162 | `settings.py` file has an important setting called `DEPTH_LIMIT` which determines how many consecutive links our spider can crawl. A high value lets our crawler visit the deepest corners of a website, taking longer to finish with possibly diminishing returns. A low value could end the crawl before extracting relevant information.
163 |
164 | If pages are skipped due to the `DEPTH_LIMIT`, Scrapy logs those skipped URLs for us. Because this usually causes a lot of logs, we've disabled this option in our project. If you'd like to keep it enabled, remove [the `"scrapy.spidermiddlewares.depth"` from the `disable_loggers` in `degreegurucrawler/spider/configurable.py` file](https://github.com/upstash/degreeguru/blob/master/degreegurucrawler/degreegurucrawler/spiders/configurable.py#L22).
165 |
166 |
167 |
168 |
169 |
170 | That's it! 🎉 We've configured our crawler and are ready to run it using the following command:
171 |
172 | ```
173 | scrapy crawl configurable --logfile degreegurucrawl.log
174 | ```
175 |
176 | Note that running this might take time. You can monitor the progress by looking at the log file `degreegurucrawl.log` or the metrics of your Upstash Vector Database dashboard as shown below.
177 |
178 | 
179 |
180 | > [!TIP]
181 | > If you want to do a dry run (without creating embeddings or a vector database), simply comment out [the line where we pass the `callback` parameter to the `Rule` object in `ConfigurableSpider`](https://github.com/upstash/degreeguru/blob/master/degreegurucrawler/degreegurucrawler/spiders/configurable.py#L38)
182 |
183 | ### Step 2: Chatbot
184 |
185 | In this section, we'll explore how to chat with the data we've just crawled and stored in our vector database. Here's an overview of what this will look like architecturally:
186 |
187 | 
188 |
189 | Before we can run the chatbot locally, we need to set the environment variables as shown in the [`.env.local.example`](https://github.com/upstash/degreeguru/blob/master/.env.local.example) file. Rename this file and remove the `.example` ending, leaving us with `.env.local`.
190 |
191 | Your `.env.local` file should look like this:
192 | ```
193 | # Redis tokens retrieved here: https://console.upstash.com/
194 | UPSTASH_REDIS_REST_URL=
195 | UPSTASH_REDIS_REST_TOKEN=
196 |
197 | # Vector database tokens retrieved here: https://console.upstash.com/vector
198 | UPSTASH_VECTOR_REST_URL=
199 | UPSTASH_VECTOR_REST_TOKEN=
200 |
201 | # OpenAI key retrieved here: https://platform.openai.com/api-keys
202 | OPENAI_API_KEY=
203 | ```
204 |
205 | The first four variables are provided by Upstash, you can visit the commented links for the place to retrieve these tokens. You can find the vector database tokens here:
206 |
207 | 
208 |
209 | The `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` are needed for rate-limiting based on IP address. In order to get these secrets, go to Upstash dashboard and create a Redis database.
210 |
211 | 
212 |
213 | Finally, set the `OPENAI_API_KEY` environment variable you can get [here](https://platform.openai.com/api-keys) which allows us to vectorize user queries and generate responses.
214 |
215 | That's the setup done! 🎉 We've configured our crawler, set up all neccessary environment variables are after running `npm install` to install all local packages needed to run the app, we can start our chatbot using the command:
216 |
217 | ```bash
218 | npm run dev
219 | ```
220 |
221 | Visit `http://localhost:3000` to see your chatbot live in action!
222 |
223 | ### Step 3: Optional tweaking
224 |
225 | You can use this chatbot in two different modes:
226 |
227 | - Streaming Mode: model responses are streamed to the web application in real-time as the model generates them. Interaction with the app is more fluid.
228 | - Non-Streaming Mode: Model responses are shown to the user once entirely generated. In this mode, DegreeGuru can explicitly provide the URLs of the web pages it uses as context.
229 |
230 |
231 | Changing streaming mode
232 |
233 | To turn streaming on/off, navigate to `src/app/route/guru` and open the `route.tsx` file. Setting [`returnIntermediateSteps`](https://github.com/upstash/degreeguru/blob/master/src/app/api/guru/route.tsx#L64) to `true` disables streaming, setting it to `false` enables streaming.
234 |
235 |
236 |
237 | To customize the chatbot further, you can update the [AGENT_SYSTEM_TEMPLATE in your route.tsx file](https://github.com/upstash/DegreeGuru/blob/master/src/app/api/guru/route.tsx#L101) to better match your specific use case.
238 |
239 |
240 |
241 | ## Conclusion
242 |
243 | Congratulations on setting up your own AI chatbot! We hope you learned a lot by following along and seeing how the different parts of this app, namely the crawler, vector database, and LLM, play together. A major focus in developing this project was on its user-friendly design and adaptable settings to make this project perfect for your use case.
244 |
245 | ## Limitations
246 |
247 | The above implementation works great for a variety of use cases. There are a few limitations I'd like to mention:
248 |
249 | - Because the Upstash LangChain integration is a work-in-progress, the [`UpstashVectorStore`](https://github.com/upstash/degreeguru/blob/master/src/app/vectorstore/UpstashVectorStore.js) used with LangChain currently only implements the `similaritySearchVectorWithScore` method needed for our agent. Once we're done developing our native LangChain integration, we'll update this project accordingly.
250 | - When the non-streaming mode is enabled, the message history can cause an error after the user enters another query.
251 | - Our sources are available as URLs in the Upstash Vector Database, but we cannot show the sources explicitly when streaming. Instead, we provide the links to the chatbot as context and expect the bot to include the links in the response.
252 |
--------------------------------------------------------------------------------