diff --git a/agent/stockbroker/nodes/tools.tsx b/agent/stockbroker/nodes/tools.tsx index 7bc2d50..03bc0e5 100644 --- a/agent/stockbroker/nodes/tools.tsx +++ b/agent/stockbroker/nodes/tools.tsx @@ -6,7 +6,7 @@ import { z } from "zod"; import { LangGraphRunnableConfig } from "@langchain/langgraph"; import { findToolCall } from "../../find-tool-call"; import { format, subDays } from "date-fns"; -import { Price } from "../../types"; +import { Price, Snapshot } from "../../types"; async function getPricesForTicker(ticker: string): Promise<{ oneDayPrices: Price[]; @@ -48,21 +48,44 @@ async function getPricesForTicker(ticker: string): Promise<{ fetch(`${url}?${queryParamsOneDay.toString()}`, options), fetch(`${url}?${queryParamsThirtyDays.toString()}`, options), ]); + if (!resOneDay.ok || !resThirtyDays.ok) { throw new Error("Failed to fetch prices"); } + const { prices: pricesOneDay } = await resOneDay.json(); const { prices: pricesThirtyDays } = await resThirtyDays.json(); - console.log("pricesOneDay", pricesOneDay.length); - console.log("pricesThirtyDays", pricesThirtyDays.length); - return { oneDayPrices: pricesOneDay, thirtyDayPrices: pricesThirtyDays, }; } +async function getPriceSnapshotForTicker(ticker: string): Promise { + if (!process.env.FINANCIAL_DATASETS_API_KEY) { + throw new Error("Financial datasets API key not set"); + } + + const options = { + method: "GET", + headers: { "X-API-KEY": process.env.FINANCIAL_DATASETS_API_KEY }, + }; + const url = "https://api.financialdatasets.ai/prices/snapshot"; + + const queryParams = new URLSearchParams({ + ticker, + }); + + const response = await fetch(`${url}?${queryParams.toString()}`, options); + if (!response.ok) { + throw new Error("Failed to fetch price snapshot"); + } + + const { snapshot } = await response.json(); + return snapshot; +} + const llm = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 }); const getStockPriceSchema = z.object({ @@ -71,6 +94,10 @@ const getStockPriceSchema = z.object({ const getPortfolioSchema = z.object({ get_portfolio: z.boolean().describe("Should be true."), }); +const buyStockSchema = z.object({ + ticker: z.string().describe("The ticker symbol of the company"), + quantity: z.number().describe("The quantity of the stock to buy"), +}); const STOCKBROKER_TOOLS = [ { @@ -84,6 +111,11 @@ const STOCKBROKER_TOOLS = [ "A tool to get the user's portfolio details. Only call this tool if the user requests their portfolio details.", schema: getPortfolioSchema, }, + { + name: "buy-stock", + description: "A tool to buy a stock", + schema: buyStockSchema, + }, ]; export async function callTools( @@ -107,6 +139,9 @@ export async function callTools( const portfolioToolCall = message.tool_calls?.find( findToolCall("portfolio"), ); + const buyStockToolCall = message.tool_calls?.find( + findToolCall("buy-stock"), + ); if (stockbrokerToolCall) { const prices = await getPricesForTicker(stockbrokerToolCall.args.ticker); @@ -115,10 +150,20 @@ export async function callTools( ...prices, }); } - if (portfolioToolCall) { ui.write("portfolio", {}); } + if (buyStockToolCall) { + const snapshot = await getPriceSnapshotForTicker( + buyStockToolCall.args.ticker, + ); + ui.write("buy-stock", { + toolCallId: + message.tool_calls?.find((tc) => tc.name === "buy-stock")?.id ?? "", + snapshot, + quantity: buyStockToolCall.args.quantity, + }); + } return { messages: [message], diff --git a/agent/types.ts b/agent/types.ts index afdae92..20f7017 100644 --- a/agent/types.ts +++ b/agent/types.ts @@ -35,3 +35,12 @@ export type Price = { volume: number; time: string; }; + +export type Snapshot = { + price: number; + ticker: string; + day_change: number; + day_change_percent: number; + market_cap: number; + time: string; +}; diff --git a/agent/uis/index.tsx b/agent/uis/index.tsx index ad76b8c..89d151c 100644 --- a/agent/uis/index.tsx +++ b/agent/uis/index.tsx @@ -4,6 +4,7 @@ import AccommodationsList from "./trip-planner/accommodations-list"; import BookAccommodation from "./trip-planner/book-accommodation"; import RestaurantsList from "./trip-planner/restaurants-list"; import BookRestaurant from "./trip-planner/book-restaurant"; +import BuyStock from "./stockbroker/buy-stock"; const ComponentMap = { "stock-price": StockPrice, @@ -12,5 +13,6 @@ const ComponentMap = { "book-accommodation": BookAccommodation, "restaurants-list": RestaurantsList, "book-restaurant": BookRestaurant, + "buy-stock": BuyStock, } as const; export default ComponentMap; diff --git a/agent/uis/stockbroker/buy-stock/index.css b/agent/uis/stockbroker/buy-stock/index.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/agent/uis/stockbroker/buy-stock/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/agent/uis/stockbroker/buy-stock/index.tsx b/agent/uis/stockbroker/buy-stock/index.tsx new file mode 100644 index 0000000..9a727bc --- /dev/null +++ b/agent/uis/stockbroker/buy-stock/index.tsx @@ -0,0 +1,139 @@ +import "./index.css"; +import { v4 as uuidv4 } from "uuid"; +import { Snapshot } from "../../../types"; +import { Button } from "@/components/ui/button"; +import { useEffect, useState } from "react"; +import { Input } from "@/components/ui/input"; +import { UIMessage, useStreamContext } from "@langchain/langgraph-sdk/react-ui"; +import { Message } from "@langchain/langgraph-sdk"; +import { getToolResponse } from "agent/uis/utils/get-tool-response"; +import { DO_NOT_RENDER_ID_PREFIX } from "@/lib/ensure-tool-responses"; + +function Purchased({ + ticker, + quantity, + price, +}: { + ticker: string; + quantity: number; + price: number; +}) { + return ( +
+

Purchase Executed - {ticker}

+
+
+

Number of Shares

+

Market Price

+

Total Cost

+
+
+

{quantity}

+

${price}

+

${(quantity * price).toFixed(2)}

+
+
+
+ ); +} + +export default function BuyStock(props: { + toolCallId: string; + snapshot: Snapshot; + quantity: number; +}) { + const { snapshot, toolCallId } = props; + const [quantity, setQuantity] = useState(props.quantity); + const [finalPurchase, setFinalPurchase] = useState<{ + ticker: string; + quantity: number; + price: number; + }>(); + + const thread = useStreamContext< + { messages: Message[]; ui: UIMessage[] }, + { MetaType: { ui: UIMessage | undefined } } + >(); + + useEffect(() => { + if (typeof window === "undefined" || finalPurchase) return; + const toolResponse = getToolResponse(toolCallId, thread); + if (toolResponse) { + try { + const parsedContent: { + purchaseDetails: { + ticker: string; + quantity: number; + price: number; + }; + } = JSON.parse(toolResponse.content as string); + setFinalPurchase(parsedContent.purchaseDetails); + } catch { + console.error("Failed to parse tool response content."); + } + } + }, []); + + function handleBuyStock() { + const orderDetails = { + message: "Successfully purchased stock", + purchaseDetails: { + ticker: snapshot.ticker, + quantity: quantity, + price: snapshot.price, + }, + }; + + thread.submit({ + messages: [ + { + type: "tool", + tool_call_id: toolCallId, + id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`, + name: "buy-stock", + content: JSON.stringify(orderDetails), + }, + { + type: "human", + content: `Purchased ${quantity} shares of ${snapshot.ticker} at ${snapshot.price} per share`, + }, + ], + }); + + setFinalPurchase(orderDetails.purchaseDetails); + } + + if (finalPurchase) { + return ; + } + + return ( +
+

Buy {snapshot.ticker}

+
+
+

Number of Shares

+

Market Price

+

Total Cost

+
+
+ setQuantity(Number(e.target.value))} + min={1} + /> +

${snapshot.price}

+

${(quantity * snapshot.price).toFixed(2)}

+
+
+ +
+ ); +} diff --git a/agent/uis/stockbroker/stock-price/index.tsx b/agent/uis/stockbroker/stock-price/index.tsx index 675aa7c..b32b73c 100644 --- a/agent/uis/stockbroker/stock-price/index.tsx +++ b/agent/uis/stockbroker/stock-price/index.tsx @@ -1,9 +1,4 @@ import "./index.css"; -import { - useStreamContext, - type UIMessage, -} from "@langchain/langgraph-sdk/react-ui"; -import type { Message } from "@langchain/langgraph-sdk"; import { useState, useMemo } from "react"; import { ChartConfig, @@ -65,42 +60,26 @@ function DisplayRangeSelector({ ); } -function getPropsForDisplayRange(displayRange: DisplayRange, prices: Price[]) { - // Start by filtering prices by display range. use the `time` field on `price` which is a string date. compare it to the current date - const actualPrices: Price[] = []; +function getPropsForDisplayRange( + displayRange: DisplayRange, + oneDayPrices: Price[], + thirtyDayPrices: Price[], +) { const now = new Date(); - const oneDay = 24 * 60 * 60 * 1000; - const fiveDays = 5 * oneDay; - const oneMonth = 30 * oneDay; + const fiveDays = 5 * 24 * 60 * 60 * 1000; // 5 days in milliseconds switch (displayRange) { case "1d": - console.log("Calculating for 1d", prices.length); - console.log(prices[prices.length - 1]); - actualPrices.push( - ...prices.filter( - (p) => new Date(p.time).getTime() >= now.getTime() - oneDay, - ), - ); - break; + return oneDayPrices; case "5d": - console.log("Calculating for 5d", prices.length); - actualPrices.push( - ...prices.filter( - (p) => new Date(p.time).getTime() >= now.getTime() - fiveDays, - ), + return thirtyDayPrices.filter( + (p) => new Date(p.time).getTime() >= now.getTime() - fiveDays, ); - break; case "1m": - console.log("Calculating for 1m", prices.length); - actualPrices.push( - ...prices.filter( - (p) => new Date(p.time).getTime() >= now.getTime() - oneMonth, - ), - ); - break; + return thirtyDayPrices; + default: + return []; } - return actualPrices; } // TODO: UPDATE TO SUPPORT ONE DAY AND THIRTY DAY PRICES AS DIFFERENT PROPS export default function StockPrice(props: { @@ -109,12 +88,8 @@ export default function StockPrice(props: { thirtyDayPrices: Price[]; }) { const { ticker } = props; - console.log(props.prices[0], props.prices[props.prices.length - 1]); + const { oneDayPrices, thirtyDayPrices } = props; const [displayRange, setDisplayRange] = useState("1d"); - const thread = useStreamContext< - { messages: Message[]; ui: UIMessage[] }, - { MetaType: { ui: UIMessage | undefined } } - >(); const { currentPrice, @@ -126,8 +101,12 @@ export default function StockPrice(props: { chartData, change, } = useMemo(() => { - const prices = getPropsForDisplayRange(displayRange, props.prices); - console.log("prices", prices.length); + const prices = getPropsForDisplayRange( + displayRange, + oneDayPrices, + thirtyDayPrices, + ); + const firstPrice = prices[0]; const lastPrice = prices[prices.length - 1]; @@ -158,7 +137,7 @@ export default function StockPrice(props: { chartData, change, }; - }, [props.prices, displayRange]); + }, [oneDayPrices, thirtyDayPrices, displayRange]); return (