Skip to content

Commit

Permalink
Merge pull request #613 from zackproser/update-rag-blog-tutorial
Browse files Browse the repository at this point in the history
Allow Paywall overrides, and Paywall the RAG blog after updating it with latest Vercel AI and OpenAI SDK changes.
  • Loading branch information
zackproser authored Jan 2, 2025
2 parents 87061e4 + ae0d66b commit 417bcfd
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 23 deletions.
41 changes: 25 additions & 16 deletions src/app/blog/langchain-pinecone-chat-with-my-blog/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,35 @@ import ConsultingCTA from '@/components/ConsultingCTA'

import { createMetadata } from '@/utils/createMetadata'

export const metadata = createMetadata({
export const articleMetadata = {
author: "Zachary Proser",
date: "2024-05-10",
title: "Build a RAG pipeline for your blog with LangChain, OpenAI and Pinecone",
description: "You can chat with my writing and ask me questions I've already answered even when I'm not around",
image: chatWithMyBlog,
slug: '/blog/langchain-pinecone-chat-with-my-blog'
});
slug: 'langchain-pinecone-chat-with-my-blog',
isPaid: true,
price: 2000,
previewLength: 250,
previewElements: 37,
paywallHeader: "Master RAG Pipelines: The Most In-Demand Gen AI Skill",
paywallBody: "Every company wants developers who can build RAG applications. Get a complete, production-ready tutorial that teaches you exactly how to build a RAG pipeline with the latest tech stack: Vercel AI SDK, OpenAI embeddings, and Pinecone vector search.",
buttonText: "Level Up Your Gen AI Skills ($20)"
};

export const metadata = createMetadata(articleMetadata);

export default (props) => <ArticleLayout metadata={metadata} {...props} />
export default (props) => <ArticleLayout metadata={articleMetadata} {...props} />

I built [a chat with my blog experience](/chat) into my site, allowing visitors to ask questions of my writing.

Here's a quick demo of it in action - or [you can try it out yourself](/chat):

<iframe
class="youtube-video"
className="youtube-video"
src="https://www.youtube.com/embed/ii4aUE-6Okk"
title="Building an AI chatbot with langchain, Pinecone.io, Jupyter notebooks and Vercel"
frameborder="0"
frameBorder="0"
allow="fullscreen;">
</iframe>

Expand Down Expand Up @@ -178,7 +187,7 @@ const openai = new OpenAIApi(config)
export async function getEmbeddings(input: string) {
try {
const response = await openai.createEmbedding({
model: "text-embedding-ada-002",
model: "text-embedding-3-large",
input: input.replace(/\n/g, ' ')
})
Expand Down Expand Up @@ -330,7 +339,7 @@ I can reuse my existing article loader to look up related blog posts by their pa
This recommended post functionality requires a clever use of headers, to send component rendering data as a base64 encoded header to the frontend, alongside Vercel's AI SDK's `StreamingTextResponse`.
To level set a bit, a streaming response is one where the connection between the server and client is kept open, and chunks of text are streamed back to the client as they become available. This kind of connection is preferable
when you have a lot of data to send, or when the speed with which your experience becomes interactive is critical.
when you have a lot of data to send, or when the speed with which your experience becomes interactive is critical.
As you've noticed from using ChatGPT and its kin, they begin to respond to you immediately and you can see the text response progressively filling in your screen in real-time. Why?
Expand All @@ -348,9 +357,9 @@ const serializedArticles = Buffer.from(
JSON.stringify(relatedBlogPosts)
).toString('base64')
return new StreamingTextResponse(result.toAIStream(), {
return result.toDataStreamResponse({
headers: {
"x-sources": serializedArticles
'x-sources': serializedArticles
}
});
```
Expand Down Expand Up @@ -387,7 +396,7 @@ const prompt = `
END OF CONTEXT BLOCK
Zachary will take into account any CONTEXT BLOCK that is provided in a conversation.
If the context does not provide the answer to question, Zachary will say, "I'm sorry, but I don't know the answer to that question".
Zachary will not apologize for previous responses, but instead will indicate new information was gained.
Zachary will not apologize for previous responses, but instead will indicated new information was gained.
Zachary will not invent anything that is not drawn directly from the context.
Zachary will not engage in any defamatory, overly negative, controversial, political or potentially offense conversations.
`;
Expand Down Expand Up @@ -732,8 +741,8 @@ export async function POST(req: Request) {
Zachary will not engage in any defamatory, overly negative, controversial, political or potentially offense conversations.
`;
const result = await streamText({
model: openai('gpt-4o'),
const result = streamText({
model: openai.chat('gpt-4o'),
system: prompt,
prompt: lastMessage.content,
});
Expand All @@ -742,9 +751,9 @@ export async function POST(req: Request) {
JSON.stringify(relatedBlogPosts)
).toString('base64')
return new StreamingTextResponse(result.toAIStream(), {
return result.toDataStreamResponse({
headers: {
"x-sources": serializedArticles
'x-sources': serializedArticles
}
});
}
Expand Down Expand Up @@ -971,4 +980,4 @@ jobs:
"
```
That's it for now. Thanks for reading! If you were helped in any way by this post or found it interesting, please leave a comment or like below or share it with a friend. 🙇
That's it for now. Thanks for reading! If you were helped in any way by this post or found it interesting, please leave a comment or like below or share it with a friend. 🙇
11 changes: 10 additions & 1 deletion src/components/ArticleContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ interface ArticleContentProps {
slug?: string
title?: string
previewLength?: number
previewElements?: number // Number of elements to show in preview
previewElements?: number
paywallHeader?: string
paywallBody?: string
buttonText?: string
}

export default function ArticleContent({
Expand All @@ -23,6 +26,9 @@ export default function ArticleContent({
title,
previewLength = 150,
previewElements = 3,
paywallHeader,
paywallBody,
buttonText
}: ArticleContentProps) {
const { data: session } = useSession()
const [hasPurchased, setHasPurchased] = useState(false)
Expand Down Expand Up @@ -82,6 +88,9 @@ export default function ArticleContent({
price={price!}
slug={slug!}
title={title!}
paywallHeader={paywallHeader}
paywallBody={paywallBody}
buttonText={buttonText}
/>
</>
)
Expand Down
8 changes: 6 additions & 2 deletions src/components/ArticleLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,16 @@ interface ArticleLayoutProps {
slug?: string
previewLength?: number
previewElements?: number
paywallHeader?: string
paywallBody?: string
buttonText?: string
}
}

export function ArticleLayout({
children,
metadata,
}: ArticleLayoutProps) {
// console.log('Full metadata in ArticleLayout:', metadata)

return (
<>
<Container className="mt-16 lg:mt-32">
Expand All @@ -53,6 +54,9 @@ export function ArticleLayout({
title={metadata.title}
previewLength={metadata.previewLength}
previewElements={metadata.previewElements}
paywallHeader={metadata.paywallHeader}
paywallBody={metadata.paywallBody}
buttonText={metadata.buttonText}
>
{children}
</ArticleContent>
Expand Down
16 changes: 12 additions & 4 deletions src/components/Paywall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,18 @@ interface PaywallProps {
slug: string
title: string
paywallHeader?: string
paywallBody?: string
buttonText?: string
}

export default function Paywall({ price, slug, title, paywallHeader }: PaywallProps) {
export default function Paywall({
price,
slug,
title,
paywallHeader,
paywallBody,
buttonText
}: PaywallProps) {
const { data: session } = useSession()
const router = useRouter()
const [loading, setLoading] = useState(false)
Expand All @@ -25,7 +34,6 @@ export default function Paywall({ price, slug, title, paywallHeader }: PaywallPr

setLoading(true)
try {
// Add 'blog-' prefix to distinguish from course products
router.push(`/checkout?product=blog-${slug}`)
} catch (error) {
console.error('Error:', error)
Expand All @@ -43,14 +51,14 @@ export default function Paywall({ price, slug, title, paywallHeader }: PaywallPr

<div className="text-center">
<p className="mb-6 text-lg text-zinc-700 dark:text-zinc-300">
Purchase to read the full content and support its independent creator.
{paywallBody || "Purchase to read the full content and support its independent creator."}
</p>
<Button
onClick={handlePurchase}
disabled={loading}
className="w-full sm:w-auto px-8 py-3 text-lg bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 transition-all duration-300"
>
{loading ? 'Processing...' : `Purchase for $${(price / 100).toFixed(2)}`}
{loading ? 'Processing...' : buttonText || `Purchase for $${(price / 100).toFixed(2)}`}
</Button>
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/lib/shared-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export interface PaidArticle extends Article {
isPaid: boolean
price?: number
previewLength?: number
paywallHeader?: string
paywallBody?: string
buttonText?: string
}

// Base article with slug, without paid properties
Expand Down

0 comments on commit 417bcfd

Please sign in to comment.