From 1f945118c5007e607ca2813e1e2409d336ae939d Mon Sep 17 00:00:00 2001 From: zaidmukaddam Date: Sun, 11 Aug 2024 18:04:33 +0530 Subject: [PATCH] changes: - added retrieval, stock and weather tools and UI - switched to llama 3.1 for question suggestions --- app/actions.ts | 13 +- app/api/chat/route.ts | 75 ++++++++ app/page.tsx | 199 +++++++++++++++++++- components/stock-chart.tsx | 85 +++++++++ components/ui/chart.tsx | 370 +++++++++++++++++++++++++++++++++++++ package.json | 2 + pnpm-lock.yaml | 246 +++++++++++++++++++++++- 7 files changed, 980 insertions(+), 10 deletions(-) create mode 100644 components/stock-chart.tsx create mode 100644 components/ui/chart.tsx diff --git a/app/actions.ts b/app/actions.ts index 24b9486..d37d25f 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -1,7 +1,7 @@ 'use server'; import { generateObject } from 'ai'; -import { openai } from '@ai-sdk/openai'; +import { createOpenAI as createGroq } from '@ai-sdk/openai'; import { z } from 'zod'; export interface Message { @@ -9,14 +9,19 @@ export interface Message { content: string; } +const groq = createGroq({ + baseURL: 'https://api.groq.com/openai/v1', + apiKey: process.env.GROQ_API_KEY, +}); + export async function suggestQuestions(history: Message[]) { 'use server'; const { object } = await generateObject({ - model: openai('gpt-4o-mini'), + model: groq('llama-3.1-70b-versatile'), temperature: 0, - system: -`You are a search engine query generator. You 'have' to create 3 questions for the search engine based on the message history which has been provided to you. + system: + `You are a search engine query generator. You 'have' to create 3 questions for the search engine based on the message history which has been provided to you. The questions should be open-ended and should encourage further discussion while maintaining the whole context. Limit it to 5-10 words per question. Always put the user input's context is some way so that the next search knows what to search for exactly. Never use pronouns in the questions as they blur the context.`, diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 371f9c0..2773658 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -2,12 +2,14 @@ import { openai } from "@ai-sdk/openai"; import { anthropic } from '@ai-sdk/anthropic' import { convertToCoreMessages, streamText, tool } from "ai"; import { z } from "zod"; +import { geolocation } from '@vercel/functions' // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages, model } = await req.json(); + const { latitude, longitude, city } = geolocation(req) let ansmodel; @@ -22,12 +24,17 @@ export async function POST(req: Request) { messages: convertToCoreMessages(messages), system: "You are an AI web search engine that helps users find information on the internet." + + "The user is located in " + city + " at latitude " + latitude + " and longitude " + longitude + "." + + "Use this geolocation data for weather tool." + "You use the 'web_search' tool to search for information on the internet." + "Always call the 'web_search' tool to get the information, no need to do a chain of thought or say anything else, go straight to the point." + "Once you have found the information, you provide the user with the information you found in brief like a news paper detail." + "The detail should be 3-5 paragraphs in 10-12 sentences, some time pointers, each with citations in the [Text](link) format always!" + "Citations can be inline of the text like this: Hey there! [Google](https://google.com) is a search engine." + "Do not start the responses with newline characters, always start with the first sentence." + + "When the user asks about a Stock, you should 'always' first gather news about it with web search tool, then show the chart and then write your response. Follow these steps in this order only!" + + "Never use the retrieve tool for general search. Always use it when the user provides an url! " + + "For weather related questions, use get_weather_data tool and write your response. No need to call any other tool. Put citation to OpenWeatherMaps API everytime." + "The current date is: " + new Date() .toLocaleDateString("en-US", { @@ -84,6 +91,74 @@ export async function POST(req: Request) { } } }), + retrieve: tool({ + description: 'Retrieve the information from the web search tool.', + parameters: z.object({ + url: z.string().describe('The URL to retrieve the information from.') + }), + execute: async ({ url }: { url: string }) => { + let hasError = false + + let results; + try { + const response = await fetch(`https://r.jina.ai/${url}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'X-With-Generated-Alt': 'true' + } + }) + const json = await response.json() + if (!json.data || json.data.length === 0) { + hasError = true + } else { + // Limit the content to 5000 characters + if (json.data.content.length > 5000) { + json.data.content = json.data.content.slice(0, 5000) + } + results = { + results: [ + { + title: json.data.title, + content: json.data.content, + url: json.data.url + } + ], + query: '', + images: [] + } + } + } catch (error) { + hasError = true + console.error('Retrieve API error:', error) + } + + if (hasError || !results) { + return results + } + + return results + } + }), + get_weather_data: tool({ + description: "Get the weather data for the given coordinates.", + parameters: z.object({ + lat: z.number().describe('The latitude of the location.'), + lon: z.number().describe('The longitude of the location.') + }), + execute: async ({ lat, lon }: { lat: number, lon: number }) => { + const apiKey = process.env.OPENWEATHER_API_KEY + const response = await fetch(`https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&appid=${apiKey}`) + const data = await response.json() + return data + } + }), + stock_chart_ui: tool({ + description: 'Display the stock chart for the given stock symbol after web search.', + parameters: z.object({ + symbol: z.string().describe('The stock symbol to display the chart for.') + }), + }), }, onFinish: async (event) => { console.log(event.text); diff --git a/app/page.tsx b/app/page.tsx index 6ed2dce..567c94f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -29,6 +29,8 @@ import { AlignLeft, Newspaper, Copy, + TrendingUp, + Cloud, } from 'lucide-react'; import { HoverCard, @@ -57,8 +59,23 @@ import { } from "@/components/ui/dropdown-menu"; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; +import StockChart from '@/components/stock-chart'; +import { Line, LineChart, CartesianGrid, XAxis, YAxis, ResponsiveContainer } from "recharts"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart"; export default function Home() { const inputRef = useRef(null); @@ -78,7 +95,7 @@ export default function Home() { body: { model: selectedModel === 'Speed' ? 'gpt-4o-mini' : selectedModel === 'Quality (GPT)' ? 'gpt-4o' : 'claude-3-5-sonnet-20240620', }, - maxToolRoundtrips: 1, + maxToolRoundtrips: 2, onFinish: async (message, { finishReason }) => { if (finishReason === 'stop') { const newHistory: Message[] = [{ role: "user", content: lastSubmittedQuery, }, { role: "assistant", content: message.content }]; @@ -87,6 +104,11 @@ export default function Home() { } setIsAnimating(false); }, + onToolCall({ toolCall, }) { + if (toolCall.toolName === 'stock_chart_ui') { + return 'Stock chart was shown to the user.'; + } + }, onError: (error) => { console.error("Chat error:", error); toast.error("An error occurred. Please try again."); @@ -189,11 +211,180 @@ export default function Home() { ); } + interface WeatherDataPoint { + date: string; + minTemp: number; + maxTemp: number; + } + + const WeatherChart: React.FC<{ result: any }> = React.memo(({ result }) => { + const { chartData, minTemp, maxTemp } = useMemo(() => { + const weatherData: WeatherDataPoint[] = result.list.map((item: any) => ({ + date: new Date(item.dt * 1000).toLocaleDateString(), + minTemp: Number((item.main.temp_min - 273.15).toFixed(1)), + maxTemp: Number((item.main.temp_max - 273.15).toFixed(1)), + })); + + // Group data by date and calculate min and max temperatures + const groupedData: { [key: string]: WeatherDataPoint } = weatherData.reduce((acc, curr) => { + if (!acc[curr.date]) { + acc[curr.date] = { ...curr }; + } else { + acc[curr.date].minTemp = Math.min(acc[curr.date].minTemp, curr.minTemp); + acc[curr.date].maxTemp = Math.max(acc[curr.date].maxTemp, curr.maxTemp); + } + return acc; + }, {} as { [key: string]: WeatherDataPoint }); + + const chartData = Object.values(groupedData); + + // Calculate overall min and max temperatures + const minTemp = Math.min(...chartData.map(d => d.minTemp)); + const maxTemp = Math.max(...chartData.map(d => d.maxTemp)); + + return { chartData, minTemp, maxTemp }; + }, [result]); + + const chartConfig: ChartConfig = useMemo(() => ({ + minTemp: { + label: "Min Temp.", + color: "hsl(var(--chart-1))", + }, + maxTemp: { + label: "Max Temp.", + color: "hsl(var(--chart-2))", + }, + }), []); + + return ( + + + Weather Forecast for {result.city.name} + + Showing min and max temperatures for the next 5 days + + + + + + + + new Date(value).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} + /> + `${value}°C`} + /> + } /> + + + + + + + +
+
+
+ {result.city.name}, {result.city.country} +
+
+ Next 5 days forecast +
+
+
+
+
+ ); + }); + + + WeatherChart.displayName = 'WeatherChart'; + const renderToolInvocation = (toolInvocation: ToolInvocation, index: number) => { const args = JSON.parse(JSON.stringify(toolInvocation.args)); const result = 'result' in toolInvocation ? JSON.parse(JSON.stringify(toolInvocation.result)) : null; + if (toolInvocation.toolName === 'stock_chart_ui') { + return ( +
+
+ +

{args.symbol}

+
+ + + + + +
+ ); + } + + if (toolInvocation.toolName === 'get_weather_data') { + if (!result) { + return ( +
+
+ + Fetching weather data... +
+
+ {[0, 1, 2].map((index) => ( + + ))} +
+
+ ); + } + + if (isLoading) { + return ( + + + + + +
+ + + ); + } + + return ; + } + return (
{!result ? ( @@ -403,9 +594,9 @@ export default function Home() { }, [append, setMessages]); const exampleQueries = [ - "Meta Llama 3.1 405B", + "Weather in Doha", "Latest on Paris Olympics", - "What is Github Models?", + "Summary: https://openai.com/index/gpt-4o-system-card/", "OpenAI GPT-4o mini" ]; diff --git a/components/stock-chart.tsx b/components/stock-chart.tsx new file mode 100644 index 0000000..1e5fe05 --- /dev/null +++ b/components/stock-chart.tsx @@ -0,0 +1,85 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +// from https://github.com/bklieger-groq/stockbot-on-groq/blob/main/components/tradingview/stock-chart.tsx +'use client' + +import React, { useEffect, useRef, memo } from 'react' + +export function StockChart({ props: symbol }: { props: string }) { + const container = useRef(null) + + useEffect(() => { + if (!container.current) return + const script = document.createElement('script') + script.src = + 'https://s3.tradingview.com/external-embedding/embed-widget-advanced-chart.js' + script.type = 'text/javascript' + script.async = true + script.innerHTML = JSON.stringify({ + autosize: true, + symbol: symbol, + interval: 'D', + timezone: 'Etc/UTC', + theme: 'light', + style: '1', + locale: 'en', + backgroundColor: 'rgba(255, 255, 255, 1)', + gridColor: 'rgba(247, 247, 247, 1)', + withdateranges: true, + hide_side_toolbar: false, + allow_symbol_change: true, + calendar: false, + hide_top_toolbar: true, + support_host: 'https://www.tradingview.com' + }) + + container.current.appendChild(script) + + return () => { + if (container.current) { + container.current.removeChild(script) + } + } + }, [symbol]) + + return ( + + ) +} + +export default memo(StockChart) \ No newline at end of file diff --git a/components/ui/chart.tsx b/components/ui/chart.tsx new file mode 100644 index 0000000..0510f5b --- /dev/null +++ b/components/ui/chart.tsx @@ -0,0 +1,370 @@ +"use client" + +import * as React from "react" +import * as RechartsPrimitive from "recharts" +import { + NameType, + Payload, + ValueType, +} from "recharts/types/component/DefaultTooltipContent" + +import { cn } from "@/lib/utils" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +} + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a ") + } + + return context +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId() + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` + + return ( + +
+ + + {children} + +
+
+ ) +}) +ChartContainer.displayName = "Chart" + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([_, config]) => config.theme || config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( +