├── .github └── workflows │ └── flake8.yml ├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── Procfile ├── README.md ├── app.json ├── img └── bot2.jpg ├── main.py ├── render.yaml ├── requirements.txt ├── runtime.txt ├── stock_peformace.py ├── stock_price.py └── yf_tool.py /.github/workflows/flake8.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | flake8: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Set up Python 11 | uses: actions/setup-python@v2 12 | with: 13 | python-version: '3.x' 14 | - name: Install flake8 15 | run: pip install flake8 16 | - name: Run flake8 17 | run: flake8 . -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | 4 | # User-specific stuff: 5 | .idea/**/workspace.xml 6 | .idea/**/tasks.xml 7 | .idea/dictionaries 8 | 9 | # Sensitive or high-churn files: 10 | .idea/**/dataSources/ 11 | .idea/**/dataSources.ids 12 | .idea/**/dataSources.xml 13 | .idea/**/dataSources.local.xml 14 | .idea/**/sqlDataSources.xml 15 | .idea/**/dynamic.xml 16 | .idea/**/uiDesigner.xml 17 | 18 | # Gradle: 19 | .idea/**/gradle.xml 20 | .idea/**/libraries 21 | 22 | # CMake 23 | cmake-build-debug/ 24 | 25 | # Mongo Explorer plugin: 26 | .idea/**/mongoSettings.xml 27 | 28 | ## File-based project format: 29 | *.iws 30 | 31 | ## Plugin-specific files: 32 | 33 | # IntelliJ 34 | /out/ 35 | 36 | # mpeltonen/sbt-idea plugin 37 | .idea_modules/ 38 | 39 | # JIRA plugin 40 | atlassian-ide-plugin.xml 41 | 42 | # Cursive Clojure plugin 43 | .idea/replstate.xml 44 | 45 | # Crashlytics plugin (for Android Studio and IntelliJ) 46 | com_crashlytics_export_strings.xml 47 | crashlytics.properties 48 | crashlytics-build.properties 49 | fabric.properties 50 | __pycache__/main.cpython-310.pyc 51 | .DS_Store 52 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.defaultFormatter": "ms-python.autopep8" 4 | }, 5 | "python.formatting.provider": "none" 6 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.11-slim 2 | 3 | # 將專案複製到容器中 4 | COPY . /app 5 | WORKDIR /app 6 | 7 | # 安裝必要的套件 8 | RUN pip install --upgrade pip 9 | COPY requirements.txt . 10 | RUN pip install -r requirements.txt 11 | 12 | EXPOSE 8080 13 | CMD uvicorn main:app --host=0.0.0.0 --port=$PORT -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn main:app --host=0.0.0.0 --port=${PORT:-5000} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Utilizing a LINE Bot integrated with LangChain in Python to assist with stock price inquiries 2 | ============== 3 | 4 | ![](./img/bot2.jpg) 5 | 6 | Installation and Usage 7 | ============= 8 | 9 | ### 1. Got A LINE Bot API devloper account 10 | 11 | - [Make sure you already registered on LINE developer console](https://developers.line.biz/console/), if you need use LINE Bot. 12 | 13 | - Create new Messaging Channel 14 | - Get `Channel Secret` on "Basic Setting" tab. 15 | - Issue `Channel Access Token` on "Messaging API" tab. 16 | - Open LINE OA manager from "Basic Setting" tab. 17 | - Go to Reply setting on OA manager, enable "webhook" 18 | 19 | ### 2. To obtain an OpenAI API token 20 | 21 | - Register for an account on the OpenAI website at . 22 | - Once you have an account, you can find your [API Keys](https://platform.openai.com/account/api-keys) in the account settings page. 23 | - If you want to use the OpenAI API for development, you can find more information and instructions in the API documentation page. 24 | - Please note that the OpenAI API is only available to users who meet certain criteria. 25 | - You can find more information about the usage conditions and limitations of the API in the API documentation page. 26 | 27 | ### 3. Deploy this on Web Platform 28 | 29 | You can choose [Heroku](https://www.heroku.com/) or [Render](http://render.com/) 30 | 31 | ### 4. Deploy this on Heroku 32 | 33 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 34 | 35 | - Input `Channel Secret` and `Channel Access Token`. 36 | - Input [OpenAI API Key](https://platform.openai.com/account/api-keys) in `OPENAI_API_KEY`. 37 | - Remember your heroku, ID 38 | 39 | ### 5. Deploy this on Render.com 40 | 41 | [![Deploy to Render](http://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy) 42 | 43 | ### 6. Go to LINE Bot Dashboard, setup basic API 44 | 45 | - Setup your basic account information. Here is some info you will need to know. 46 | - `Callback URL`: 47 | 48 | It all done. 49 | 50 | License 51 | --------------- 52 | 53 | Licensed under the Apache License, Version 2.0 (the "License"); 54 | you may not use this file except in compliance with the License. 55 | You may obtain a copy of the License at 56 | 57 | 58 | 59 | Unless required by applicable law or agreed to in writing, software 60 | distributed under the License is distributed on an "AS IS" BASIS, 61 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 62 | See the License for the specific language governing permissions and 63 | limitations under the License. 64 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LINE Bot with Langchain", 3 | "description": "Quick LINEBot with Langchain", 4 | "repository": "https://github.com/kkdai/linebot-langchain", 5 | "keywords": [ 6 | "python", 7 | "linebot", 8 | "Sample" 9 | ], 10 | "buildpacks": [ 11 | { 12 | "url": "https://github.com/heroku/heroku-buildpack-python.git" 13 | }, 14 | { 15 | "url": "heroku/python" 16 | } 17 | ], 18 | "env": { 19 | "OPENAI_API_KEY": { 20 | "description": "OpenAI Access Token", 21 | "required": true 22 | }, 23 | "ChannelAccessToken": { 24 | "description": "Channel Access Token", 25 | "required": true 26 | }, 27 | "ChannelSecret": { 28 | "description": "Channel Secret", 29 | "required": true 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /img/bot2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kkdai/linebot-langchain/116d225ccab355ef2520402f5d4f2657b5ef8006/img/bot2.jpg -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import os 16 | import sys 17 | 18 | import aiohttp 19 | 20 | from fastapi import Request, FastAPI, HTTPException 21 | 22 | from langchain.chat_models import ChatOpenAI 23 | from langchain.agents import AgentType 24 | from langchain.agents import initialize_agent 25 | 26 | from stock_price import StockPriceTool 27 | from stock_peformace import StockPercentageChangeTool 28 | from stock_peformace import StockGetBestPerformingTool 29 | 30 | from linebot import ( 31 | AsyncLineBotApi, WebhookParser 32 | ) 33 | from linebot.aiohttp_async_http_client import AiohttpAsyncHttpClient 34 | from linebot.exceptions import ( 35 | InvalidSignatureError 36 | ) 37 | from linebot.models import ( 38 | MessageEvent, TextMessage, TextSendMessage, 39 | ) 40 | 41 | from dotenv import load_dotenv, find_dotenv 42 | _ = load_dotenv(find_dotenv()) # read local .env file 43 | 44 | # get channel_secret and channel_access_token from your environment variable 45 | channel_secret = os.getenv('ChannelSecret', None) 46 | channel_access_token = os.getenv('ChannelAccessToken', None) 47 | if channel_secret is None: 48 | print('Specify LINE_CHANNEL_SECRET as environment variable.') 49 | sys.exit(1) 50 | if channel_access_token is None: 51 | print('Specify LINE_CHANNEL_ACCESS_TOKEN as environment variable.') 52 | sys.exit(1) 53 | 54 | app = FastAPI() 55 | session = aiohttp.ClientSession() 56 | async_http_client = AiohttpAsyncHttpClient(session) 57 | line_bot_api = AsyncLineBotApi(channel_access_token, async_http_client) 58 | parser = WebhookParser(channel_secret) 59 | 60 | # Langchain (you must use 0613 model to use OpenAI functions.) 61 | model = ChatOpenAI(model="gpt-3.5-turbo-0613") 62 | tools = [StockPriceTool(), StockPercentageChangeTool(), 63 | StockGetBestPerformingTool()] 64 | open_ai_agent = initialize_agent(tools, 65 | model, 66 | agent=AgentType.OPENAI_FUNCTIONS, 67 | verbose=False) 68 | 69 | 70 | @app.post("/callback") 71 | async def handle_callback(request: Request): 72 | signature = request.headers['X-Line-Signature'] 73 | 74 | # get request body as text 75 | body = await request.body() 76 | body = body.decode() 77 | 78 | try: 79 | events = parser.parse(body, signature) 80 | except InvalidSignatureError: 81 | raise HTTPException(status_code=400, detail="Invalid signature") 82 | 83 | for event in events: 84 | if not isinstance(event, MessageEvent): 85 | continue 86 | if not isinstance(event.message, TextMessage): 87 | continue 88 | 89 | tool_result = open_ai_agent.run(event.message.text) 90 | 91 | await line_bot_api.reply_message( 92 | event.reply_token, 93 | TextSendMessage(text=tool_result) 94 | ) 95 | 96 | return 'OK' 97 | -------------------------------------------------------------------------------- /render.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | # A Docker web service 3 | - type: web 4 | name: linebot-langchain 5 | runtime: python 6 | plan: free 7 | autoDeploy: false 8 | buildCommand: pip install -r requirements.txt 9 | startCommand: uvicorn main:app --host=0.0.0.0 --port=${PORT:-5000} 10 | envVars: 11 | - key: PYTHON_VERSION 12 | value: 3.10.12 13 | - key: ChannelAccessToken 14 | sync: false 15 | - key: ChannelSecret 16 | sync: false 17 | - key: OPENAI_API_KEY 18 | sync: false -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | line-bot-sdk==3.5.0 2 | fastapi 3 | uvicorn[standard] 4 | langchain==0.0.308 5 | openai==0.28.1 6 | yfinance 7 | pydantic 8 | tiktoken 9 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.10.11 -------------------------------------------------------------------------------- /stock_peformace.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from langchain.tools import BaseTool 3 | from typing import Optional, Type 4 | from pydantic import BaseModel, Field 5 | from yf_tool import get_price_change_percent, get_best_performing 6 | 7 | 8 | class StockChangePercentageCheckInput(BaseModel): 9 | """Input for Stock ticker check. for percentage check""" 10 | 11 | stockticker: str = Field(..., 12 | description="Ticker symbol for stock or index") 13 | days_ago: int = Field(..., description="Int number of days to look back") 14 | 15 | 16 | class StockPercentageChangeTool(BaseTool): 17 | name = "get_price_change_percent" 18 | description = ( 19 | "Useful for when you need to find out the percentage change " 20 | "in a stock's value. " 21 | "You should input the stock ticker used on the yfinance API " 22 | "and also input the " 23 | "number of days to check the change over" 24 | ) 25 | 26 | def _run(self, stockticker: str, days_ago: int): 27 | price_change_response = get_price_change_percent(stockticker, days_ago) 28 | 29 | return price_change_response 30 | 31 | def _arun(self, stockticker: str, days_ago: int): 32 | raise NotImplementedError("This tool does not support async") 33 | 34 | args_schema: Optional[Type[BaseModel]] = StockChangePercentageCheckInput 35 | 36 | 37 | # the best performing 38 | 39 | class StockBestPerformingInput(BaseModel): 40 | """Input for Stock ticker check. for percentage check""" 41 | 42 | stocktickers: List[str] = Field(..., 43 | description=( 44 | "Ticker symbols for " 45 | "stocks or indices" 46 | )) # Close the parenthesis here 47 | days_ago: int = Field(..., description="Int number of days to look back") 48 | 49 | 50 | class StockGetBestPerformingTool(BaseTool): 51 | name = "get_best_performing" 52 | description = ( 53 | "Useful for when you need to the performance of multiple " 54 | "stocks over a period. " 55 | "You should input a list of stock " 56 | "tickers used on the yfinance API " 57 | "and also input the number of days to check the change over" 58 | ) 59 | 60 | def _run(self, stocktickers: List[str], days_ago: int): 61 | price_change_response = get_best_performing(stocktickers, days_ago) 62 | 63 | return price_change_response 64 | 65 | def _arun(self, stockticker: List[str], days_ago: int): 66 | raise NotImplementedError("This tool does not support async") 67 | 68 | args_schema: Optional[Type[BaseModel]] = StockBestPerformingInput 69 | -------------------------------------------------------------------------------- /stock_price.py: -------------------------------------------------------------------------------- 1 | from langchain.tools import BaseTool 2 | from typing import Optional, Type 3 | from pydantic import BaseModel, Field 4 | from yf_tool import get_stock_price 5 | 6 | 7 | class StockPriceCheckInput(BaseModel): 8 | """Input for Stock price check.""" 9 | 10 | stockticker: str = Field(..., 11 | description="Ticker symbol for stock or index") 12 | 13 | 14 | class StockPriceTool(BaseTool): 15 | name = "get_stock_ticker_price" 16 | description = ( 17 | "Useful for when you need to find out the price of stock. " 18 | "You should input the stock ticker used on the yfinance API" 19 | ) 20 | 21 | def _run(self, stockticker: str): 22 | # print("i'm running") 23 | price_response = get_stock_price(stockticker) 24 | 25 | return price_response 26 | 27 | def _arun(self, stockticker: str): 28 | raise NotImplementedError("This tool does not support async") 29 | 30 | args_schema: Optional[Type[BaseModel]] = StockPriceCheckInput 31 | -------------------------------------------------------------------------------- /yf_tool.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import yfinance as yf 3 | 4 | 5 | def calculate_performance(symbol, days_ago): 6 | ticker = yf.Ticker(symbol) 7 | end_date = datetime.now() 8 | start_date = end_date - timedelta(days=days_ago) 9 | start_date = start_date.strftime('%Y-%m-%d') 10 | end_date = end_date.strftime('%Y-%m-%d') 11 | historical_data = ticker.history(start=start_date, end=end_date) 12 | old_price = historical_data['Close'].iloc[0] 13 | new_price = historical_data['Close'].iloc[-1] 14 | percent_change = ((new_price - old_price) / old_price) * 100 15 | return round(percent_change, 2) 16 | 17 | 18 | def get_best_performing(stocks, days_ago): 19 | best_stock = None 20 | best_performance = None 21 | for stock in stocks: 22 | try: 23 | performance = calculate_performance(stock, days_ago) 24 | if best_performance is None or performance > best_performance: 25 | best_stock = stock 26 | best_performance = performance 27 | except Exception as e: 28 | print(f"Could not calculate performance for {stock}: {e}") 29 | return best_stock, best_performance 30 | 31 | 32 | def get_stock_price(symbol): 33 | ticker = yf.Ticker(symbol) 34 | todays_data = ticker.history(period='1d') 35 | return round(todays_data['Close'][0], 2) 36 | 37 | 38 | def get_price_change_percent(symbol, days_ago): 39 | ticker = yf.Ticker(symbol) 40 | 41 | # Get today's date 42 | end_date = datetime.now() 43 | 44 | # Get the date N days ago 45 | start_date = end_date - timedelta(days=days_ago) 46 | 47 | # Convert dates to string format that yfinance can accept 48 | start_date = start_date.strftime('%Y-%m-%d') 49 | end_date = end_date.strftime('%Y-%m-%d') 50 | 51 | # Get the historical data 52 | historical_data = ticker.history(start=start_date, end=end_date) 53 | 54 | # Get the closing price N days ago and today's closing price 55 | old_price = historical_data['Close'].iloc[0] 56 | new_price = historical_data['Close'].iloc[-1] 57 | 58 | # Calculate the percentage change 59 | percent_change = ((new_price - old_price) / old_price) * 100 60 | 61 | return round(percent_change, 2) 62 | --------------------------------------------------------------------------------