Transcription
I already have quite a few videos on this channel about AI chatbots in general and also specifically about financial chatbots. But today we're going to do something quite unique. We're going to build an AI stock analysis assistant that doesn't just communicate with us using text and numbers. It actually renders a dynamically generated user interface while talking to us. So we can ask a question, we can instruct it to do something and it will respond to us not just with an answer but also with graphics, with buttons, with forms and we can interact with this user interface that is generated by this chatbot to ask it to do further actions. We can basically interact with this assistant in a very efficient way and all of this is going to be fully customizable in the back end. Spoiler, the front end part is going to be extremely short and extremely simple. So we're going to do almost no design work at all. If you like this video, let me know by hitting a like button and subscribing. But now let us get right into it.
[music]
So our goal today is to build an AI stock analysis assistant. Now what that means to you exactly is something you can customize. I'm going to show you one version of that, but you can easily add, remove, and change functionality because we're going to do everything here using Langchain, FastAPI, YFinance in the back end. So these are going to be the three packages that we use for the functionality. And then for the dynamically generated user interface in the front end, we're going to use Tethys. And the great thing is I mentioned it already in the beginning. We don't have to code anything in the front end. We just have to embed a component that interacts with our back end. We don't have to style anything. We can do some minimal CSS to make it look a little bit better, but we don't have to deal with any front-end stuff. Now, Tethys is also sponsoring this video today, so check them out. There's a link to them in the description down below. Everything we do today here you can also do for free. You don't have to pay to be able to reproduce this. You can just sign up and get some free credits and you can build the exact same thing that we built today. But that is going to be super interesting.
So I repeat, Langchain is going to be used in the back end for the agent. We're going to create an AI agent that has access to different tools. We're going to then be able to access these functionalities or the agent itself by using a FastAPI endpoint. And we're going to use YFinance, Yahoo Finance basically, in order to be able to get real-time financial information. So our chatbot should be able to answer questions like what is the current stock price of Nvidia or plot the historical stock price of Nvidia, for example. Also, it should be able to retrieve the latest news. All stuff that requires real-time access to the internet.
Now, let us get started right away with a coding process. What we're going to do first is we're going to set up our development environment. In my case here, I'm going to use VS Code today. And I'm also going to use UV as my package manager. Now, you don't have to do that. You can also just use pip or pip3 install and all the package names. You can create a virtual environment if you want to. In my case, instead of pip install, I'm going to say UV. If you also want to use UV, I recommend you just say pip or pip3 install UV. This is going to install it as a Python package. You can also use Cargo or install it with a package from your package manager on Linux. But you just need some way to install Python packages. I'm sure if you're watching this video, you know how to do that. So just install the packages which are FastAPI, Pydantic, Uvicorn. These are just for serving the FastAPI application and for having a schema. Then we're also going to use Langchain with support for OpenAI because we're going to use the OpenAI API, at least the API definition. We're actually going to use Tethys as the provider of the responses because they have to include their format that can then be rendered dynamically in the front end. But essentially just include that. Then also YFinance for the real-time data and don't forget the python-dotenv because we are going to load API keys here. So these are and of course I need to first initialize the project but then I can do UV add and these are the packages that we're going to need. Then create a file called main.py. In my case, I already have it here autogenerated. I'm going to clear it and we're going to start from scratch.
So let us start with the imports. First of all, I want to be able to import the API key. So I'm going to say here from dotenv import load_dotenv. The idea of dotenv is that I want to create a file later on which I'm going to call .env. And in this file I will have an API key. Now I'm going to call it OPENAI_API_KEY. So, it's automatically recognized, but it's going to be a Tethys API key. So, there's going to be something in here later on, and I'm going to be able to load that into my application. Also, let me just make sure my interpreter is selecting the correct path. Now, it works after restarting. So, basically, we're going to use this load_dotenv function. I can call it right away. This is going to load all these key-value pairs. In our case, we're only going to have one. It's going to load it into the environment. So if we use an OpenAI class from Langchain or if we use an OpenAI model from Langchain or in Langchain it's going to use the respective API key automatically. So we can authenticate ourselves by doing that.
In addition to that, I'm going to say from pydantic import BaseModel. This is primarily important for the schema for defining what the endpoints accept what kind of data the endpoints accept. Then I'm also going to say here from fastapi import FastAPI itself. Then also I'm going to import from fastapi.middleware import CORSMiddleware and also from fastapi.responses import StreamingResponse because our dynamically generated user interface is going to be streamed. So we're going to see how it is being generated at least if we want to. And because of that, we need to make sure that FastAPI can actually stream the responses. In addition to that, don't forget to also import the server. Uvicorn is going to be used for serving.
And now we get to the Langchain part. We need to say from langchain.agents import create_agent. That is the basis for everything. Then of course we want to have tools. So we're going to import the tool decorator. Uh from langchain.messages import SystemMessage and HumanMessage. Then also from langchain.chat_models import ChatOpenAI because we want to change the base URL since we're going to send, as I mentioned, the request to Tethys. And finally, we also want to say from langgraph.checkpoint.memory import InMemorySaver. I also have a Langgraph course on this channel where I explain what these individual components do. I'm not going to go into too much depth here, but the create_agent is basically for creating the agent that can use tools and that can uh yeah take multiple actions before giving you the actual answer. Tool is a decorator that we can put on top of functions to make them tools that can be used by the agent. SystemMessage and HumanMessage are just helper classes to not have to write these fancy dictionary formats. ChatOpenAI is an instance for creating a model or a class for creating model instances and there we can also customize the base URL and this is mainly used for saving context or for saving message history. So that is the idea and the last thing that we need to import here is Yahoo Finance. So import yfinance as yf.
Now before we start with the or before we go into the tool definition, what we're going to do is we're going to set up the entire process itself. So we're going to have some agent that can do something and we want to have an endpoint that actually serves the answer. So the first thing is we need to create a FastAPI application. I'm going to say app = FastAPI() like this. And now we need a route that can handle the conversation. So for that we're going to say that we want to have a single endpoint that takes POST requests. So I'm going to say @app.post. The route here is going to be /api/chat and the function is going to be an asynchronous function called chat. It's going to take in a request and this request will have a certain format. I'm going to now call this request body or let's call it request_object. Maybe this is what I called it in my prepared code. request_object. And it's going to do something and then return something. For now, I'm just going to pass. But this request_object here defines what fields this POST endpoint accepts. And this is important because that has to be whatever Tethys is going to be sending to it. So whatever the component that we're going to use, the C1Chat as we're going to call it or as we're going to see it's called, this is going to send a certain type of request to this backend endpoint and we need to be able to accept it.
So what I'm going to do here is I'm going to say class RequestObject(BaseModel):. And this request_object will have a couple of fields. It's going to inherit from the Pydantic BaseModel. This is just for serialization and validation. And it's going to have the following fields. It's going to have a prompt which itself is going to be a PromptObject. So we need to define an additional class because this is going to have multiple fields itself. In addition to the prompt, we also have a thread_id for being able to respond within the same thread to remember the message history so to say. And then also a response_id which is a string. And then up here I can say class PromptObject(BaseModel):. And this BaseModel will be an individual prompt that is part of a request body. And this contains the content which is the message, the ID, and also the role of uh yeah the user or the sender so to say. Yeah. So that is the structure that is going to be sent by the component. This is not something that we're going to implement. This is what the Tethys component C1Chat that we're going to use in the React front end very easily, very simply. This is what it sends to our backend when we provide this endpoint. So we need to handle that. We're primarily going to care about the content and the thread_id. The content for obvious reasons and the thread_id to keep track of the conversations and to not lose the context. That is the basic idea.
So now in this chat endpoint we're going to basically do what we do when we work with Langchain agents. So we're going to define up here that the agent is create_agent and we need to provide a model instance. So before that we actually need to say model = ChatOpenAI() and within this ChatOpenAI instance we now need to provide two things: the model identifier and also the base URL. I'm going to copy this here from my prepared code just because I don't want to type this out but of course you can stop and just uh yeah you can pause the video and type it off yourself or you will also find the code for this video today on my GitHub. The link is also in the description down below. But basically we're saying here use GPT-5 but use it from Tethys. So Tethys as I mentioned will return certain XML structures that will render a user interface. And in order to be able to render it, you of course first need to receive the source code that allows you to render it. So the information of what the structure should look like and only the Tethys models actually do that. So make sure you have this model. And then also one more thing is we're going to need a checkpointer which is going to be the InMemorySaver that we imported here. And then we can say model=model, checkpointer=checkpointer. And then we can say tools = [] and for now, we're going to keep this as an empty list. But that is our agent now.
So how do we actually interact with this agent? We have to first define or specify the config that is going to contain the thread_id. So I'm going to say here config = {} and where we have a key 'configurable'. And now I need to look up the structure again. I always forget what this is written like but you have 'configurable' pointing to a dictionary that contains 'thread_id' and the 'thread_id' is then pointing to the actual thread_id. In our case, this is part of the response request object sorry. So request.thread_id. That is the config. We need to pass this with every request because if you pass the same thread_id, it means that this message belongs to the history that you already have for this specific thread. And since we're going to stream all of that and we're not just going to return the response, what we need to do is we need to stream on a Langchain level as well. And we need to yield the results to the front end. So we actually need to generate we need to build a generator within this endpoint. So I'm going to define a function here, a nested function called generate and this is going to be the generator of our response. But in terms of how this is done, it's actually not that difficult. Usually we do agent.invoke. Now we just do agent.stream. So I'm going to say here for token, and I think this is metadata but we're not going to use it. So I'm going to just say underscore in agent.stream(stream=True) and we're going to pass here a message history. So we're going to say messages=[...] in this dictionary is pointing to a list and this list contains messages. The first one is going to be a SystemMessage. This SystemMessage will basically instruct the model that it is a financial assistant and what it can do. We can keep it simple now. We can say "You are a stock analysis assistant. You have the ability to get real-time stock prices, historical stock prices, news, and what was the other thing that I implemented? I forgot already. The balance sheet is also what I wanted to provide here. Balance sheet data for a given ticker symbol." These are the four tools that I'm going to implement here. So getting the balance sheet data, getting the historical stock prices given a range. Maybe I should also say here "given a date range" and basically that's the idea. That's the system prompt. Then comma in the end, don't forget that because we need to provide a second message which is going to be the user message. So we need to say here HumanMessage(content=request.prompt.content). So request.prompt.content like this. That is what we send to the agent. Now important, since we're streaming, we need to make sure that the proper mode is enabled and this is called the stream_mode argument here and in our case we need to say "messages". If you choose something else, you're going to get different things out of the iteration. So you're going to get stuff but you're not going to get token and metadata. You're going to get something else. And also since we want to keep track of the conversations, config=config. That is also very important.
Now, don't forget that all this is still a for loop. So we need to also add a colon here. And what do we want to do? We want to yield the response. So the yielding of token.content. So token is um the thing that we get from the agent by streaming and we want to yield each token.content.
Now that is how we define a generator. But to actually stream the response, we need to return a StreamingResponse which is going to be based on our generate function here. And we need to also provide the proper type. The type here is media_type="text/event-stream". Of course, don't forget that we actually need to call this because we want to create an instance of it. And then we also need some headers for this. I'm going to go to a new row here. Headers = {"Cache-Control": "no-cache", "Connection": "keep-alive"}. There you go. That is going to be our StreamingResponse. And basically that is it. We of course need to start the application too. So we need to say if __name__ == "__main__": then we need to run our Uvicorn instance. So uvicorn.run(app, host="0.0.0.0", port=8888). That's that. And of course, we don't have any tool use yet. But this already should work. It just doesn't have access to tools. So I'm going to add the tools later on. But I want to show you how we can interact with this in the front end and probably we have to fix some bugs if I implemented any. But that is the backend. We just need to add some tools and then we can easily expand this to whatever we want it to be.
So let us move on to the front end. I'm going to go to my file explorer here and I'm going to create actually this should be in a backend directory. So let's take all of this um except for backend and put it into backend. Move everything into backend. There you go. And now we should be able to just say not in backend but outside of backend. I'm not sure if this works. Probably not like this but frontend. And just pull it out there. There you go. And in this frontend directory now I'm going to go inside of it and create a new Vite project. And that is basically the command. So you just need to have npm installed. And then you just say npm create vite@latest . --template react-ts. That is the command that you need to run. Once you have this I'm going to say no. I'm going to say yes. So basically this will install the packages and also run the development server right away. So we can see it works. Of course not yet connected to our backend but uh yeah that's basically it. I can click on this and this should open up a browser with this basic counter that is a default React application or default Vite application I should say. And we don't need to do a lot here. The only thing is we need to go to src. We need to go to this App.tsx. We're going to remove all of this. Uh so we don't need any of that. We don't need any of this code here. Basically, we're going to replace this with a simple component from Tethys, the C1Chat component. But in order to be able to do that, we first need to install the respective package. So, I'm going to go back into the frontend and I'm going to say npm install @tethysai/genui-sdk. So, that is the package. In addition to that, you should also install the other one that you just saw. I'm going to mention it in a second. The Crayon um package. Okay, so this one is installed. Now go back to npm install. And then we have also this @crayonai/react-ui. That is also going to be important for the styling. Once you have both of these, things become very very simple. I can remove all of this. I can zoom in a bit so you can see this better. And we're going to just say now import { C1Chat, ThemeProvider } from '@tethysai/genui-sdk'; and also the theme provider. So that is going to be the main component that interacts with our backend and that is going to be for yeah providing a theme obviously and the actual styling can now also be easily imported by saying import '@crayonai/react-ui/styles';. That is going to give us some default styling and the only thing we need to return here very easy is first of all ThemeProvider that is going to be wrapped around the C1Chat component and the only thing that we need to put here is the api_url which is going to be '/api/chat' like this. So api/chat and close it right away and for the theme if you want to you can say mode="dark". This is going to enable dark mode in the application which I personally prefer. Then we can also wrap this around uh or wrap a div container around it called app-container. So app-container for some styling stuff. And that's basically it.
Now the rest is optional, but I would also recommend doing some basic CSS styling. I really mean some basic CSS styling. So I'm going to copy paste this so you see what I mean by that. Um, basically you can get rid of the index.css. You can also um make sure you don't import it. You can also get rid of uh anything else if there is something. But besides that, you want to go to App.css. You can get rid of everything that's inside here. And you can just paste this. And of course, you will also find this on GitHub. Basically, just removing the margins and making sure Roboto is used, not the default font. And that is the front end. That is it.
Of course, except for one more tiny thing. We need to make sure that our front end knows what to target. So, we provided the endpoint, but it doesn't know the IP. So we need to go to the vite.config.ts and we need to make sure that we actually have a proxy that allows us to or allows Vite to know where it has to direct traffic to. So we're going to say here server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8888', changeOrigin: true } } }. That is now really it. Unless I did some mistake. So let's see if this works. I'm not sure if this is going to work on a UV level since I moved this to the backend directory. But let's go back. Let's go to backend. Maybe I have some problem here with UV, but maybe not. Let's say UV run main.py. There you go. Backend is running. And now second terminal. I'm going to go to the frontend directory. So like this. And I'm going to say npm run dev. And then I can just click on this. This should open up a browser and allow me. And of course, one more thing that we forgot in the backend is I mean I do have the user interface as you can see, but if I now try to send something, I'm pretty sure I'm going to get an exception because I don't have the Tethys API key yet. But let's try. Let's say something like hi here. And probably I'm not going to get a response. There you go. Because on the one hand, I get a thread ID which I uh or I'm using a thread ID which I don't have. So let me check that out. That is probably here because I'm using a thread ID like this, not like this. But that should not be the only problem because if I try again uh probably I should reload. If I try again now I should get the message that I don't have an API key and this is a problem. So what do I have here? "Incorrect API key provided." Uh of course I do have an API key but the one that I just randomly typed in. So what we need to do now is we need to go to Tethys. So in my case, I'm going to log in and I'm going to go to my profile or to the console and I can click on API keys. So the important thing is you just want to create a new API key. If you don't have one, you want to copy it, put it in the .env file and then it's going to work. So in my case now, I provided the proper API key. Let's rerun this. Let's go back to the browser. Let's refresh. And now let's say I this should hopefully give me a streaming response. There you go. With everything that can be done by this agent. And you can see I can get live prices, historical prices, news because I provided it with the information that it can do that. And you can see I get this whole user interface where I can actually once this is done uh rendering, I can actually type in stuff and it will do uh what I ask it to do. So for example, I can say Nvidia. I can say I want to have the live price and I want to say maybe a date range. I'm not going to have one. And I can say submit. This is going to trigger the next action by this assistant. Now, of course, in this case, it doesn't have actually real-time data because it doesn't have any tools that it can use to retrieve this data, but it can simulate it and it can pretend like it knows what it's doing. In this case, it doesn't know because it doesn't have any tools yet. But you can see the integration works at least on that level.
The only thing left now is to add the functionality from the Yahoo Finance API. That is probably the most simple part. Uh all we have to do is we have to create tool functions and add them to the list of tools here and we're going to do that obviously before the agent is created. So I'm going to say here @tool and the tool is going to be called get_stock_price. This get_stock_price tool will have a description. Description: "A function that returns the current stock price based on a ticker symbol." You can of course change that. And then we're going to say def get_stock_price(ticker: str) -> float: . This will get a ticker which is a string as an argument and then we're just going to use yfinance to get this ticker stock price. So we're just going to say stock = yf.Ticker(ticker) and then we're going to return stock.history(period="1d")['Close'].iloc[-1]. And it makes sense to print just so we know "get_stock_price tool is being used" just so we can see what the model actually does. So that's super simple.
We can do the same thing now with historical data. So I can copy this can and go down below paste it not twice but once that's enough. @tool def get_historical_stock_price(ticker: str, start_date: str, end_date: str) -> dict: . Description: "A function that returns a stock price over time based on a ticker symbol and a start and end date." print("get_historical_stock_price tool is being used.") Then I can say here start=start_date, end=end_date. And we're not just interested in the closing price the last one. We're interested in this whole thing as a dictionary. So I'm going to say .to_dict(). That's basically it.
Now we can do two more things. Uh one is going to be a balance sheet. So let's get the balance sheet. The latest balance sheet or I'm not sure what actually we get from Yahoo Finance if you just request the balance sheet. Probably the last couple of ones. @tool def get_balance_sheet(ticker: str) -> dict: . Description: "A function that returns the balance sheet based on a ticker symbol." print("get_balance_sheet tool is being used.") And instead of getting the history, we're now just going to get the balance sheet. Maybe we should pass a year. Maybe we should say year is an integer and and a given year and then we can let the model choose it automatically or we can provide it. So that is going to be ticker and then we want to have balance_sheet and want to say that the year is the year and that to_dict() as well. And finally, let's go to news. That's the simplest one because there we don't even have a function. We just need to say get uh let's call this stock_news and @tool def get_stock_news(ticker: str) -> dict: . Description: "Returns news based on a ticker symbol." print("get_stock_news tool is being used.") and now we just return the news object. So get the ticker and return stock.news. That should work.
And all we need to do now is we need to pass all of these here. tools = [get_stock_price, get_historical_stock_price, get_balance_sheet, get_stock_news]. That's basically it. And now we have these tools as capabilities. Let's restart the backend. Let's also just restart the frontend. and let's go into the browser and interact with our intelligent stock analysis assistant.
So what I can do now is I can say um what is the current stock price of Meta? That is hopefully going to reuse the tool. You can see here "get_stock_price tool is being used." The tool was called. Now we get the actual current price and it asks us if we want to fetch um different data if we want to have more data. Now I want to say give me or maybe let's say visualize the closing price of Meta over the past let's say 6 months. That should be data that is available in the Yahoo Finance API and our tool here can automatically visualize this without Matplotlib without using any visualization code it can just realize okay I have data here I can use this data to visualize it and to make uh a good report. As you can see here, "get_historical_stock_price tool is being used" so it knows what to do. Meta closing price last 6 months. And there you go. It plots a chart in real time. We can see how the stock price develops. Of course, if you want to have like huge fancy animations, uh like proper candlestick charts, probably you have to do it yourself to some degree, but you can get pretty good reports. Maybe I can also do something like compare AMD and Nvidia using graphs and statistics. I don't know. And of course, to some degree, this is going to hallucinate stuff because it doesn't have all the statistics and it doesn't have all the information that it needs. It can get the stock price, the historical stock price, uh maybe some news, maybe the balance sheet, but it's not going to do uh huge stuff here. But you can see a last trade Nvidia, last trade AMD. You can also see here all these tools being used. It can also plot the stock price development. Then we can also see a bar chart here, fancy with all these uh headlines, latest news that we see here, key takeaways, and then also it gives me buttons for additional actions. So I can say show one-year comparison. This is going to call the historical stock tool and it's going to allow me to go deeper into the analysis. So as you can see again "historical_stock_price tool is being used" by clicking the button and that is the basic idea.
This is a very minimalistic example and it's also just showing you the C1Chat component. So while this is loading here uh I can also show you in the docs there's two things that you can do easily. One is you can use the C1Chat component that I used here or you can use the C1 component which is more manual. So with the C1 component, you cannot just uh chat with this like in a ChatGPT chat u that we have here. But you can actually go and program triggers and do everything. You can customize this also on a front-end level and you can deal with the actions that are taken in the component manually. So you can hook this up with some other functions and animations and whatnot. I wanted to use the C1Chat because I want to focus on the backend. We can still customize the entire Langchain experience, but if you also want to customize the behavior in the front end, you can go with C1. And the cool thing about Tethys and the whole C1 thing is that it's developer-focused. So this is not something like N8N that mainly focuses on the end user, mainly focuses on business people with a little bit of programming knowledge. This entire stack here is focused on developers that want to customize, that want to tweak and uh do everything manually. So this is not a simple GUI tool. This is an actual developer library, an actual developer tool that you can use. And there's also a backend SDK if you want to.
So let's go back here to uh our answer. What did we do? We compared one-year uh prices. Great. Now last thing, visualize the balance sheet of Nvidia. Okay. Actually, I get an exception for this one. This maybe needs some fixing. I think it's just balance_sheet and that's it. Similar to the news, not entirely sure if we can even provide a year. Let's see if that fixes the problem. It's good that I tried it. So, let's do it like this. Restart the backend because I think balance sheet also in the Yahoo Finance API is just an attribute and not a function. So, you need to be careful with that. Visualize Visualize the balance sheet of Nvidia. Now you can see "balance_sheet tool is being used" and there you go. We get a visualization of the balance sheet of Nvidia. We can of course compare this now with different companies and see uh yeah the development over time and also the comparison but you get the idea. Again I don't want to repeat myself. Essentially what you can do now is you can go to the description down below try to build this yourself. You will find a code for this video today. You will find a link to Tethys. As I mentioned when you sign up you're going to get uh free credits to use. So I think it's $10 that you can use without having to do anything. No credit card nothing. You can use Tethys to have UI like this generated. You can integrate this either in a chat application like the one I um have here actually or you can integrate individual components and react to them in your own workflow and application. You don't have to have this ChatGPT sort of interface.
So that's it for this video today. I hope you enjoyed it and hope you learned something. If so, let me know by hitting a like button and leaving a comment in the comment section down below. Also, don't forget to check out Tethys, the sponsor of this video today. You'll find a link to them in the description down below. And besides that, don't forget to subscribe to this channel and hit the notification bell to not miss a single future video for free. Other than that, thank you much for watching. See you in the next video and bye.