├── .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 | 
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 | [](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 | [](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 |
--------------------------------------------------------------------------------