├── .dockerignore
├── .env.example
├── .github
└── workflows
│ └── docker-image.yml
├── .gitignore
├── Controller
├── liff
│ ├── __init__.py
│ ├── front_end.py
│ ├── knowledge_base.py
│ ├── knowledge_base_file.py
│ ├── llm_model.py
│ └── upload_file.py
└── line
│ ├── __init__.py
│ └── line.py
├── Dockerfile
├── LICENSE
├── Middleware.py
├── Model
├── BaseModel.py
├── ChatHistory.py
├── KnowledgeBase.py
├── KnowledgeBaseFile.py
├── LlmModel.py
├── Setting.py
├── UploadedFiles.py
├── UserSelectKnowledgeBace.py
└── __init__.py
├── README.md
├── Service
├── LineFunction.py
├── __init__.py
├── embedding.py
├── llm.py
└── upload_file.py
├── View
├── .gitignore
├── .npmrc
├── README.md
├── babel.config.js
├── jsconfig.json
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── App.vue
│ ├── assets
│ │ ├── data
│ │ │ ├── CityCountyData.json
│ │ │ ├── workFeature.json
│ │ │ ├── workMoney.json
│ │ │ └── workTime.json
│ │ ├── icon
│ │ │ ├── csv.svg
│ │ │ ├── doc.svg
│ │ │ ├── pdf.svg
│ │ │ ├── txt.svg
│ │ │ └── upload.svg
│ │ └── logo.png
│ ├── components
│ │ ├── basicSetting.vue
│ │ ├── bottomTab.vue
│ │ ├── fileDisplayCards
│ │ │ ├── fileDisplayCard.vue
│ │ │ └── fileSelectCard.vue
│ │ ├── fileManager.vue
│ │ ├── fileSelectWindow.vue
│ │ ├── knowledgeBase.vue
│ │ ├── knowledgeBaseSetting.vue
│ │ ├── searchBar.vue
│ │ ├── switchButton.vue
│ │ ├── uploadWindow.vue
│ │ └── whiteBackground.vue
│ ├── index.css
│ ├── main.js
│ ├── router
│ │ └── index.js
│ ├── store
│ │ └── index.js
│ └── views
│ │ └── HomeView.vue
├── tailwind.config.js
└── vue.config.js
├── config
├── database_backup.db
└── line_reply_template.py
├── db
├── files
│ └── uploaded_files.txt
└── vector_db
│ └── vector_db.txt
├── dependencies.py
├── main.py
└── requirements.txt
/.dockerignore:
--------------------------------------------------------------------------------
1 | # uploaded files
2 | db/files/*
3 | !/db/files/uploaded_files.txt
4 |
5 | # db
6 | db/*.db
7 | db/*.db-journal
8 | *.sqlite3
9 | *.bin
10 | db/vetor_db/*
11 | !/db/vector_db/vector_db.txt
12 |
13 | # Byte-compiled / optimized / DLL files
14 | __pycache__/
15 | *.py[cod]
16 | *$py.class
17 |
18 | # node modules
19 | View/node_modules/
20 |
21 | # C extensions
22 | *.so
23 |
24 | # Distribution / packaging
25 | .Python
26 | build/
27 | develop-eggs/
28 | dist/
29 | downloads/
30 | eggs/
31 | .eggs/
32 | lib/
33 | lib64/
34 | parts/
35 | sdist/
36 | var/
37 | wheels/
38 | share/python-wheels/
39 | *.egg-info/
40 | .installed.cfg
41 | *.egg
42 | MANIFEST
43 |
44 | # PyInstaller
45 | # Usually these files are written by a python script from a template
46 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
47 | *.manifest
48 | *.spec
49 |
50 | # Installer logs
51 | pip-log.txt
52 | pip-delete-this-directory.txt
53 |
54 | # Unit test / coverage reports
55 | htmlcov/
56 | .tox/
57 | .nox/
58 | .coverage
59 | .coverage.*
60 | .cache
61 | nosetests.xml
62 | coverage.xml
63 | *.cover
64 | *.py,cover
65 | .hypothesis/
66 | .pytest_cache/
67 | cover/
68 |
69 | # Translations
70 | *.mo
71 | *.pot
72 |
73 | # Django stuff:
74 | *.log
75 | local_settings.py
76 | db.sqlite3
77 | db.sqlite3-journal
78 |
79 | # Flask stuff:
80 | instance/
81 | .webassets-cache
82 |
83 | # Scrapy stuff:
84 | .scrapy
85 |
86 | # Sphinx documentation
87 | docs/_build/
88 |
89 | # PyBuilder
90 | .pybuilder/
91 | target/
92 |
93 | # Jupyter Notebook
94 | .ipynb_checkpoints
95 |
96 | # IPython
97 | profile_default/
98 | ipython_config.py
99 |
100 | # pyenv
101 | # For a library or package, you might want to ignore these files since the code is
102 | # intended to run in multiple environments; otherwise, check them in:
103 | # .python-version
104 |
105 | # pipenv
106 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
107 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
108 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
109 | # install all needed dependencies.
110 | #Pipfile.lock
111 |
112 | # poetry
113 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
114 | # This is especially recommended for binary packages to ensure reproducibility, and is more
115 | # commonly ignored for libraries.
116 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
117 | #poetry.lock
118 |
119 | # pdm
120 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
121 | #pdm.lock
122 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
123 | # in version control.
124 | # https://pdm.fming.dev/#use-with-ide
125 | .pdm.toml
126 |
127 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128 | __pypackages__/
129 |
130 | # Celery stuff
131 | celerybeat-schedule
132 | celerybeat.pid
133 |
134 | # SageMath parsed files
135 | *.sage.py
136 |
137 | # Environments
138 | .env
139 | .venv
140 | env/
141 | venv/
142 | ENV/
143 | env.bak/
144 | venv.bak/
145 |
146 | # Spyder project settings
147 | .spyderproject
148 | .spyproject
149 |
150 | # Rope project settings
151 | .ropeproject
152 |
153 | # mkdocs documentation
154 | /site
155 |
156 | # mypy
157 | .mypy_cache/
158 | .dmypy.json
159 | dmypy.json
160 |
161 | # Pyre type checker
162 | .pyre/
163 |
164 | # pytype static type analyzer
165 | .pytype/
166 |
167 | # Cython debug symbols
168 | cython_debug/
169 |
170 | # PyCharm
171 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
172 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
173 | # and can be added to the global gitignore or merged into this file. For a more nuclear
174 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
175 | #.idea/
176 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | MODEL_NAME=llama3-8b-8192|gpt-3.5-turbo|gpt-4-1106-preview
2 | BASE_URL=https://api.groq.com/openai/v1|https://api.openai.com/v1|https://api.openai.com/v1
3 | API_KEY=API_KEY_1|API_KEY_2|API_KEY_3
4 |
5 | MAX_CHAT_HISTORY=5
6 | EMBEDDING_MODEL="sentence-transformers/all-MiniLM-L6-v2"
7 | EMBEDDING_DEVICE="cpu"
8 |
9 | LINE_CHANNEL_ACCESS_TOKEN=YOUR_LINE_CHANNEL_ACCESS_TOKEN
10 | LINE_CHANNEL_SECRET=YOUR_LINE_CHANNEL_SECRET
11 | LINE_LIFF_ID=YOUR_LINE_LIFF_ID
12 | LINE_LOGIN_CHANNEL_ID=YOUR_LINE_LOGIN_CLIENT_ID
13 |
14 | FILE_MAX_SIZE=5MB
15 | SPACE_PER_USER=200MB
16 |
17 | ALLOW_FILE_TYPE=pdf,csv,txt
18 |
--------------------------------------------------------------------------------
/.github/workflows/docker-image.yml:
--------------------------------------------------------------------------------
1 | name: Docker Image CI
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Get current time
14 | uses: josStorer/get-current-time@v2
15 | id: current-time
16 | with:
17 | format: YYYYMMDD-HH
18 | utcOffset: "+08:00"
19 | -
20 | name: Checkout
21 | uses: actions/checkout@v4
22 | -
23 | name: Login to Docker Hub
24 | uses: docker/login-action@v3
25 | with:
26 | username: ${{ secrets.DOCKERHUB_USERNAME }}
27 | password: ${{ secrets.DOCKERHUB_TOKEN }}
28 | -
29 | name: Set up Docker Buildx
30 | uses: docker/setup-buildx-action@v3
31 | -
32 | name: Build and push
33 | uses: docker/build-push-action@v5
34 | with:
35 | context: .
36 | file: ./Dockerfile
37 | push: true
38 | tags: |
39 | ${{ secrets.DOCKERHUB_USERNAME }}/chatpdf-linebot:latest
40 | ${{ secrets.DOCKERHUB_USERNAME }}/chatpdf-linebot:${{ steps.current-time.outputs.formattedTime }}
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # uploaded files
2 | db/files/*
3 | !/db/files/uploaded_files.txt
4 |
5 | # db
6 | db/*.db
7 | db/*.db-journal
8 | *.sqlite3
9 | *.bin
10 | db/vetor_db/*
11 | !/db/vector_db/vector_db.txt
12 |
13 | # Byte-compiled / optimized / DLL files
14 | __pycache__/
15 | *.py[cod]
16 | *$py.class
17 |
18 | # C extensions
19 | *.so
20 |
21 | # Distribution / packaging
22 | .Python
23 | build/
24 | develop-eggs/
25 | dist/
26 | downloads/
27 | eggs/
28 | .eggs/
29 | lib/
30 | lib64/
31 | parts/
32 | sdist/
33 | var/
34 | wheels/
35 | share/python-wheels/
36 | *.egg-info/
37 | .installed.cfg
38 | *.egg
39 | MANIFEST
40 |
41 | # PyInstaller
42 | # Usually these files are written by a python script from a template
43 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
44 | *.manifest
45 | *.spec
46 |
47 | # Installer logs
48 | pip-log.txt
49 | pip-delete-this-directory.txt
50 |
51 | # Unit test / coverage reports
52 | htmlcov/
53 | .tox/
54 | .nox/
55 | .coverage
56 | .coverage.*
57 | .cache
58 | nosetests.xml
59 | coverage.xml
60 | *.cover
61 | *.py,cover
62 | .hypothesis/
63 | .pytest_cache/
64 | cover/
65 |
66 | # Translations
67 | *.mo
68 | *.pot
69 |
70 | # Django stuff:
71 | *.log
72 | local_settings.py
73 | db.sqlite3
74 | db.sqlite3-journal
75 |
76 | # Flask stuff:
77 | instance/
78 | .webassets-cache
79 |
80 | # Scrapy stuff:
81 | .scrapy
82 |
83 | # Sphinx documentation
84 | docs/_build/
85 |
86 | # PyBuilder
87 | .pybuilder/
88 | target/
89 |
90 | # Jupyter Notebook
91 | .ipynb_checkpoints
92 |
93 | # IPython
94 | profile_default/
95 | ipython_config.py
96 |
97 | # pyenv
98 | # For a library or package, you might want to ignore these files since the code is
99 | # intended to run in multiple environments; otherwise, check them in:
100 | # .python-version
101 |
102 | # pipenv
103 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
104 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
105 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
106 | # install all needed dependencies.
107 | #Pipfile.lock
108 |
109 | # poetry
110 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
111 | # This is especially recommended for binary packages to ensure reproducibility, and is more
112 | # commonly ignored for libraries.
113 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
114 | #poetry.lock
115 |
116 | # pdm
117 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
118 | #pdm.lock
119 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
120 | # in version control.
121 | # https://pdm.fming.dev/#use-with-ide
122 | .pdm.toml
123 |
124 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
125 | __pypackages__/
126 |
127 | # Celery stuff
128 | celerybeat-schedule
129 | celerybeat.pid
130 |
131 | # SageMath parsed files
132 | *.sage.py
133 |
134 | # Environments
135 | .env
136 | .venv
137 | env/
138 | venv/
139 | ENV/
140 | env.bak/
141 | venv.bak/
142 |
143 | # Spyder project settings
144 | .spyderproject
145 | .spyproject
146 |
147 | # Rope project settings
148 | .ropeproject
149 |
150 | # mkdocs documentation
151 | /site
152 |
153 | # mypy
154 | .mypy_cache/
155 | .dmypy.json
156 | dmypy.json
157 |
158 | # Pyre type checker
159 | .pyre/
160 |
161 | # pytype static type analyzer
162 | .pytype/
163 |
164 | # Cython debug symbols
165 | cython_debug/
166 |
167 | # PyCharm
168 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
169 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
170 | # and can be added to the global gitignore or merged into this file. For a more nuclear
171 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
172 | #.idea/
173 |
--------------------------------------------------------------------------------
/Controller/liff/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADT109119/ChatPDF-LineBot/25594ee1964c3492e6ee3df1047b82b4de7fe16a/Controller/liff/__init__.py
--------------------------------------------------------------------------------
/Controller/liff/front_end.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Request
2 | from fastapi.responses import FileResponse
3 |
4 | from Model.Setting import setting
5 |
6 | import os
7 | import requests
8 | import json
9 |
10 | router = APIRouter()
11 |
12 | @router.get("/liff")
13 | async def get_index():
14 | return FileResponse('View/dist/index.html')
15 |
16 | @router.get("/liff/{path:path}")
17 | async def get_static_files_or_404(path):
18 | # try open file for path
19 | file_path = os.path.join("View/dist",path)
20 | if os.path.isfile(file_path):
21 | return FileResponse(file_path)
22 | return FileResponse('View/dist/index.html')
23 |
24 | # @router.post("/line_id")
25 | # async def session_line_id(request: Request):
26 | # try:
27 | # line_id = request.json()["line_id"]
28 | # request.session["line_id"] = line_id
29 | # return {"message": "OK"}
30 | # except:
31 | # return {"message": "error"}
32 |
33 | @router.get("/liffid")
34 | async def get_liffid():
35 | return {"liff_id": setting.LINE_LIFF_ID}
36 |
37 | @router.get("/line_id/{line_id}")
38 | async def session_line_id(request: Request, line_id: str):
39 |
40 | try:
41 | url = "https://api.line.me/oauth2/v2.1/verify"
42 | headers = {"Content-Type": "application/x-www-form-urlencoded"}
43 | data = {
44 | "id_token": line_id,
45 | "client_id": setting.LINE_LOGIN_CHANNEL_ID
46 | }
47 |
48 | response = requests.post(url, headers=headers, data=data)
49 | # print(response.sub)
50 | if response.status_code == 200:
51 | temp = json.loads(response.text)
52 | # print(temp)
53 | request.session["line_id"] = temp['sub']
54 | else:
55 | request.session["line_id"] = None
56 |
57 | return {"message": "OK"}
58 | except:
59 | return {"message": "error"}
60 |
--------------------------------------------------------------------------------
/Controller/liff/knowledge_base.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Request, Depends, HTTPException
2 | from fastapi.responses import FileResponse, JSONResponse
3 |
4 | from Model.KnowledgeBase import knowledgeBase
5 |
6 | from pydantic import BaseModel
7 | from dependencies import verify_line_id
8 |
9 | import os
10 |
11 | router = APIRouter(dependencies=[Depends(verify_line_id)])
12 |
13 | @router.get("/api/knowledge_base")
14 | async def get_knowledge_base(request: Request):
15 | try:
16 | line_id = request.session.get("line_id")
17 | list = knowledgeBase.get_list(line_id)
18 | response = []
19 | for i in list:
20 | response.append({
21 | "id": i[0],
22 | "name": i[2],
23 | "model": i[3],
24 | "temperature": i[4],
25 | "score_threshold": i[5],
26 | "search_item_limit": i[6],
27 | })
28 | return JSONResponse({"status":"success","data":response})
29 | except Exception as e:
30 | print(e)
31 | return {"status": "error"}
32 |
33 | @router.get("/api/knowledge_base/{id}")
34 | async def get_knowledge_base_setting(request: Request, id: int):
35 | try:
36 | line_id = request.session.get("line_id")
37 | response = knowledgeBase.get_setting(id, line_id)
38 | response = {
39 | "id": response['id'],
40 | "name": response['name'],
41 | "model": response['model'],
42 | "temperature": response['temperature'],
43 | "score_threshold": response['score_threshold'],
44 | "search_item_limit": response['search_item_limit'],
45 | }
46 | return JSONResponse({"status":"success","data":response})
47 | except Exception as e:
48 | print(e)
49 | raise HTTPException(status_code=400, detail="unable to access")
50 |
51 | class CreateKnowledgeBaseData(BaseModel):
52 | name: str
53 | model: int
54 | temperature: float
55 | score_threshold: float
56 | search_item_limit: int
57 |
58 | @router.post("/api/knowledge_base")
59 | async def create_knowledge_base(request: Request, postdata: CreateKnowledgeBaseData):
60 | try:
61 | line_id = request.session.get("line_id")
62 | data = postdata.dict()
63 | temperature = 1 if abs(data['temperature']) > 1 else abs(data['temperature'])
64 | score_threshold = 1 if abs(data['score_threshold']) > 1 else abs(data['score_threshold'])
65 | search_item_limit = 1 if abs(data['search_item_limit']) < 1 else abs(data['search_item_limit'])
66 |
67 | knowledgeBase.saveData((None, line_id, data['name'], int(data['model']), temperature, score_threshold, search_item_limit, ))
68 | return {"status": "success"}
69 | except Exception as e:
70 | print(e)
71 | return {"status": "error"}
72 |
73 | class KnowledgeBaseData(BaseModel):
74 | id: int
75 | name: str
76 | model: int
77 | temperature: float
78 | score_threshold: float
79 | search_item_limit: int
80 |
81 | @router.put("/api/knowledge_base/{id}")
82 | async def update_knowledge_base(request: Request, id: int, postdata: KnowledgeBaseData):
83 | line_id = request.session.get("line_id")
84 | data = postdata.dict()
85 | data['temperature'] = 1 if abs(data['temperature']) > 1 else abs(data['temperature'])
86 | data['score_threshold'] = 1 if abs(data['score_threshold']) > 1 else abs(data['score_threshold'])
87 | data['search_item_limit'] = 1 if abs(data['search_item_limit']) < 1 else abs(data['search_item_limit'])
88 | try:
89 | knowledgeBase.updateData(id, line_id, data)
90 | except Exception as e:
91 | print(e)
92 | return {"status": "error"}
93 | return {"status": "success"}
94 |
95 | @router.delete("/api/knowledge_base/{id}")
96 | async def delete_knowledge_base(request: Request, id: int):
97 | line_id = request.session.get("line_id")
98 | try:
99 | knowledgeBase.deleteData(id, line_id)
100 | except Exception as e:
101 | print(e)
102 | return {"status": "error"}
103 | return {"status": "success"}
104 |
105 |
--------------------------------------------------------------------------------
/Controller/liff/knowledge_base_file.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Request, Body, Depends
2 | from fastapi.responses import FileResponse, JSONResponse
3 |
4 | from Model.KnowledgeBaseFile import knowledgeBaseFile
5 | from pydantic import BaseModel
6 | from Service.embedding import re_embedding
7 |
8 | from dependencies import verify_line_id
9 |
10 | import os
11 |
12 | router = APIRouter(dependencies=[Depends(verify_line_id)])
13 |
14 | @router.get("/api/knowledge_base_setting/{id}")
15 | async def get_knowledge_base_file(request: Request, id: int):
16 | try:
17 | line_id = request.session.get("line_id")
18 | list = knowledgeBaseFile.get_all_files(line_id, id)
19 | response = []
20 | for i in list:
21 | response.append({
22 | "id": i[0],
23 | "name": i[1],
24 | "active": i[3],
25 | "filetype": i[2].split(".")[-1]
26 | })
27 | return JSONResponse({"status": "success", "data": response})
28 | except Exception as e:
29 | print(e)
30 | return {"status": "error"}
31 |
32 | @router.post("/api/knowledge_base_setting/{id}")
33 | async def add_file_to_knowledge_base(request: Request, id: int, file_id: int = Body(..., embed=True)):
34 | try:
35 | line_id = request.session.get("line_id")
36 | knowledgeBaseFile.add_file_to_knowledge_base(line_id, id, file_id)
37 | await re_embedding(line_id, id)
38 | return {"status": "success"}
39 | except Exception as e:
40 | print(e)
41 | return {"status": "error"}
42 |
43 | @router.put("/api/knowledge_base_setting/{id}")
44 | async def update_knowledge_base_file(request: Request, id: int, file_id: int = Body(..., embed=True), active: bool = Body(..., embed=True)):
45 | try:
46 | line_id = request.session.get("line_id")
47 | knowledgeBaseFile.setActive(line_id, id, file_id, active)
48 | await re_embedding(line_id, id)
49 | except Exception as e:
50 | print(e)
51 | return {"status": "error"}
52 | return {"status": "success"}
53 |
54 | @router.delete("/api/knowledge_base_setting/{id}")
55 | async def delete_knowledge_base_file(request: Request, id: int, file_id: int = Body(..., embed=True)):
56 | try:
57 | line_id = request.session.get("line_id")
58 | knowledgeBaseFile.delete_file_from_knowledge_base(id, file_id, line_id)
59 | await re_embedding(line_id, id)
60 | except Exception as e:
61 | print(e)
62 | return {"status": "error"}
63 | return {"status": "success"}
64 |
--------------------------------------------------------------------------------
/Controller/liff/llm_model.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Request, Body
2 | from fastapi.responses import FileResponse
3 |
4 | from Model.Setting import setting
5 |
6 | import os
7 |
8 | router = APIRouter()
9 |
10 | @router.get("/api/model")
11 | async def get_all_llm_models(request: Request):
12 | try:
13 | line_id = request.session.get("line_id")
14 | list = setting.MODEL_NAME
15 | response = []
16 | for i, item in enumerate(list):
17 | response.append({
18 | "id": i,
19 | "name": item,
20 | })
21 | return {"status": "success", "data": response}
22 | except Exception as e:
23 | print(e)
24 | return {"status": "error"}
25 |
--------------------------------------------------------------------------------
/Controller/liff/upload_file.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Request, File, Form, UploadFile, Depends, HTTPException
2 | from fastapi.responses import FileResponse
3 |
4 | from Model.UploadedFiles import uploadedFiles
5 |
6 | from dependencies import verify_line_id
7 |
8 | from Service.upload_file import upload_file, delete_file
9 |
10 | import os
11 |
12 | router = APIRouter(dependencies=[Depends(verify_line_id)])
13 |
14 | @router.get("/api/upload")
15 | async def get_all_files_of_this_user(request: Request):
16 | line_id = request.session.get("line_id")
17 | list = uploadedFiles.get_all_files_list(line_id)
18 | response = []
19 | for i in list:
20 | response.append({
21 | "id": i[0],
22 | "name": i[2],
23 | "filetype": i[3].split(".")[-1]
24 | })
25 | return {"status": "success", "data": response}
26 |
27 | @router.get("/api/upload/{id}")
28 | async def get_file_data(request: Request, id: int):
29 | line_id = request.session.get("line_id")
30 | list = uploadedFiles.get_all_files_list(line_id)
31 | response = []
32 | for i in list:
33 | response.append({
34 | "id": i[0],
35 | "name": i[2],
36 | "filetype": i[3].split(".")[-1]
37 | })
38 | return {"status": "success", "data": response}
39 |
40 |
41 | @router.post("/api/upload")
42 | async def upload_file_to_server(request: Request, file: UploadFile = File(...)):
43 | try:
44 | line_id = request.session.get("line_id")
45 | # file_id = await uploadedFiles.uploaded_file(line_id, file)
46 | file_id = await upload_file(line_id, file)
47 | return {"status": "success", "file_id": file_id}
48 | except Exception as e:
49 | raise HTTPException(status_code=400, detail=str(e))
50 |
51 | # @router.put("/api/upload/{id}")
52 | # async def update_knowledge_base(request: Request, id: int):
53 | # return FileResponse('public/index.html')
54 |
55 | @router.delete("/api/upload/{id}")
56 | async def delete_file_from_server(request: Request, id: int):
57 | try:
58 | line_id = request.session.get("line_id")
59 | delete_file(id, line_id)
60 | except Exception as e:
61 | print(e)
62 | return {"status": "error"}
63 | return {"status": "success"}
64 |
65 |
--------------------------------------------------------------------------------
/Controller/line/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADT109119/ChatPDF-LineBot/25594ee1964c3492e6ee3df1047b82b4de7fe16a/Controller/line/__init__.py
--------------------------------------------------------------------------------
/Controller/line/line.py:
--------------------------------------------------------------------------------
1 | from fastapi import Request, HTTPException, APIRouter
2 |
3 | from linebot import LineBotApi, WebhookHandler
4 |
5 | from linebot.exceptions import InvalidSignatureError
6 | from linebot.models import MessageEvent, TextMessage, TextSendMessage, QuickReply, QuickReplyButton, MessageAction, FlexSendMessage
7 |
8 | import os
9 | import json
10 |
11 | from Model.Setting import setting
12 | from Model.UserSelectKnowledgeBace import userSelectKnowledgeBace
13 | from Model.ChatHistory import chatHistory
14 |
15 | from Service.llm import chat_llm
16 | import Service.LineFunction as LineFunction
17 |
18 | line_router = APIRouter()
19 | line_bot_api = LineBotApi(setting.LINE_CHANNEL_ACCESS_TOKEN)
20 | handler = WebhookHandler(setting.LINE_CHANNEL_SECRET)
21 |
22 |
23 | @line_router.post("/callback")
24 | async def callback(request: Request):
25 | signature = request.headers["X-Line-Signature"]
26 | body = await request.body()
27 | try:
28 | handler.handle(body.decode(), signature)
29 | except InvalidSignatureError:
30 | raise HTTPException(status_code=400, detail="Missing Parameters")
31 | return "OK"
32 |
33 | @handler.add(MessageEvent, message=TextMessage)
34 | def handling_message(event):
35 |
36 |
37 | if isinstance(event.message, TextMessage):
38 |
39 |
40 | user_message = event.message.text
41 | line_id = event.source.user_id
42 | QuickReplyButtons = [
43 | QuickReplyButton(
44 | action=MessageAction(label="繼續",text="繼續")
45 | ),
46 | QuickReplyButton(
47 | action=MessageAction(label="清除對話",text="/clear")
48 | )
49 | ]
50 |
51 | reply_msg = TextSendMessage(
52 | text=""
53 | )
54 |
55 | if user_message[0] == "/":
56 | func, args = parseMessage(user_message)
57 | if func == "select":
58 | try:
59 | userSelectKnowledgeBace.changeSelected(line_id, args)
60 | reply_msg = TextSendMessage(
61 | text="已切換知識庫"
62 | )
63 | except Exception as e:
64 | print(e)
65 | reply_msg = TextSendMessage(
66 | text="error"
67 | )
68 | elif func == "clear":
69 | try:
70 | chatHistory.clear(line_id)
71 | reply_msg = TextSendMessage(
72 | text="對話紀錄已清除"
73 | )
74 | except Exception as e:
75 | print(e)
76 | reply_msg = TextSendMessage(
77 | text="error"
78 | )
79 | elif func == "info":
80 | reply_msg = FlexSendMessage(
81 | alt_text='資訊版面',
82 | contents=json.loads(LineFunction.replay_info(line_id))
83 | )
84 | elif func == "help":
85 | reply_msg = FlexSendMessage(
86 | alt_text='幫助',
87 | contents=json.loads(LineFunction.replay_help())
88 | )
89 | elif func == "about":
90 | reply_msg = FlexSendMessage(
91 | alt_text='關於作者',
92 | contents=json.loads(LineFunction.replay_about_me())
93 | )
94 | else:
95 | reply_text = "請輸入正確的指令"
96 | else:
97 | reply_text = chat_llm(user_message, line_id)
98 | reply_msg = TextSendMessage(
99 | text=reply_text,
100 | quick_reply=QuickReply(
101 | items=QuickReplyButtons
102 | )
103 | )
104 |
105 | line_bot_api.reply_message(
106 | event.reply_token,
107 | reply_msg
108 | )
109 |
110 |
111 | def parseMessage(user_message):
112 | temp = user_message[1:].split(" ")
113 | func = temp[0]
114 |
115 | if len(temp) == 1:
116 | return func, ''
117 |
118 | args = temp[1]
119 | return func, args
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # 使用 Node.js 來構建 Vue.js 網頁
2 | FROM node:20 AS build
3 |
4 | # 設置工作目錄
5 | WORKDIR /app
6 |
7 | # 複製前端代碼
8 | COPY View ./View
9 |
10 | # 進入前端目錄並安裝依賴和構建
11 | WORKDIR /app/View
12 | RUN npm install && npm run build
13 |
14 | # 使用 Python 作為基礎映像
15 | FROM python:3.10.12-slim
16 |
17 | # 設置工作目錄
18 | WORKDIR /app
19 |
20 | # 設置時區
21 | ENV TZ=Asia/Taipei
22 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
23 |
24 | # 設置 IN_DOCKER 環境變量
25 | ENV IN_DOCKER=1
26 |
27 | # 複製後端代碼和構建後的前端文件
28 | COPY . .
29 | COPY --from=build /app/View/dist /app/View/dist
30 |
31 | # Install Python requirements
32 | RUN ls && \
33 | cp .env.example .env && \
34 | pip install --no-cache-dir -r requirements.txt
35 |
36 | # Expose port
37 | EXPOSE 8000
38 |
39 | # Run the application
40 | CMD ["python", "main.py"]
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Middleware.py:
--------------------------------------------------------------------------------
1 | from fastapi import Request, Response
2 | from starlette.middleware.base import BaseHTTPMiddleware
3 |
4 | class NoIndexMiddleware(BaseHTTPMiddleware):
5 | async def dispatch(self, request: Request, call_next):
6 | response = await call_next(request)
7 | response.headers["X-Robots-Tag"] = "noindex"
8 | return response
9 |
--------------------------------------------------------------------------------
/Model/BaseModel.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | class BaseModel:
4 | def __init__(self):
5 | self.filed = []
6 | self.con = self.get_db_connection()
7 | self.table = ""
8 |
9 | def get_db_connection(self):
10 | import sqlite3
11 | if os.path.exists('./db/database.db'):
12 | return sqlite3.connect('./db/database.db')
13 | else:
14 | shutil.copy('./config/database_backup.db', './db/database.db')
15 | return sqlite3.connect('./db/database.db')
16 |
17 | def sql_query(self, query, parameters=None, get_lastrow_id = False):
18 | cursor = self.con.cursor()
19 | cursor.execute(query, parameters)
20 | self.con.commit()
21 | if get_lastrow_id:
22 | return cursor.lastrowid
23 | return cursor.fetchall()
24 |
25 | def sql_query_many(self, query, parameters=None):
26 | cursor = self.con.cursor()
27 | cursor.executemany(query, parameters)
28 | self.con.commit()
29 | return cursor.fetchall()
30 |
31 |
32 | def saveData(self, data):
33 | cursor = self.con.cursor()
34 | columns = ', '.join(self.filed)
35 | values = ', '.join(['?'] * len(self.filed))
36 | query = f"INSERT INTO {self.table} ({columns}) VALUES ({values})"
37 | cursor.execute(query, data)
38 | self.con.commit()
39 |
40 | def getData(self, conditions=None, limit=None, order=None):
41 | cursor = self.con.cursor()
42 | query = f"SELECT * FROM {self.table}"
43 | if conditions:
44 | query += " WHERE " + " AND ".join(conditions)
45 |
46 | if limit:
47 | query += " LIMIT " + limit
48 |
49 | if order:
50 | query += " ORDER BY " + order
51 |
52 | cursor.execute(query)
53 | return cursor.fetchall()
54 |
--------------------------------------------------------------------------------
/Model/ChatHistory.py:
--------------------------------------------------------------------------------
1 | from Model.BaseModel import BaseModel
2 | from Model.Setting import setting
3 |
4 | class ChatHistory(BaseModel):
5 | def __init__(self):
6 | super().__init__()
7 | self.filed = ['time', 'line_id', 'user_message', 'bot_message']
8 | self.table = 'chat_history'
9 |
10 | def clear(self, line_id):
11 | return self.sql_query('DELETE FROM chat_history WHERE line_id =?', (line_id,))
12 |
13 | def get_history(self, line_id):
14 | return self.sql_query('SELECT user_message, bot_message FROM chat_history WHERE line_id =? ORDER BY time DESC LIMIT ?', (line_id, setting.MAX_CHAT_HISTORY,))
15 |
16 | chatHistory = ChatHistory()
--------------------------------------------------------------------------------
/Model/KnowledgeBase.py:
--------------------------------------------------------------------------------
1 | from Model.BaseModel import BaseModel
2 | from Model.Setting import Setting
3 | setting = Setting()
4 | class KnowledgeBase(BaseModel):
5 | def __init__(self):
6 | super().__init__()
7 | self.filed = ['id', 'line_id', 'name', 'model','temperature', 'score_threshold', 'search_item_limit']
8 | self.table = 'knowledge_base'
9 |
10 | def get_setting(self, id, line_id):
11 | result = self.sql_query(f"SELECT * FROM {self.table} WHERE id = ? AND line_id = ?", (id, line_id,))[0]
12 |
13 | return {
14 | "id": result[0],
15 | "name": result[2],
16 | "base_url": setting.BASE_URL[result[3]],
17 | "api_key": setting.API_KEY[result[3]],
18 | # "model": setting.MODEL_NAME[result[3]],
19 | "model": result[3],
20 | "temperature": result[4],
21 | "score_threshold": result[5],
22 | "search_item_limit": result[6]
23 | }
24 |
25 | def get_list(self, line_id):
26 | result = self.sql_query(f"SELECT * FROM {self.table} WHERE line_id = ?", (line_id,))
27 | return result
28 |
29 | def updateData(self, id, line_id, data: dict):
30 | self.sql_query(f"UPDATE {self.table} SET id=?, name=?, model=?, temperature=?, score_threshold=?, search_item_limit=? WHERE id = ? AND line_id = ?",
31 | (data['id'], data['name'], data['model'], data['temperature'], data['score_threshold'], data['search_item_limit'], id, line_id,))
32 |
33 | def deleteData(self, id, line_id):
34 | self.sql_query(f"DELETE FROM {self.table} WHERE id = ? AND line_id = ?", (id, line_id,))
35 |
36 | knowledgeBase = KnowledgeBase()
--------------------------------------------------------------------------------
/Model/KnowledgeBaseFile.py:
--------------------------------------------------------------------------------
1 | from Model.BaseModel import BaseModel
2 |
3 | class KnowledgeBaseFile(BaseModel):
4 | def __init__(self):
5 | super().__init__()
6 | self.filed = ['knowledge_base_id', 'file_id', 'active']
7 | self.table = 'knowledge_base_file'
8 |
9 | def get_all_files(self, line_id, knowledge_base_id):
10 | return self.sql_query(f'SELECT f.id, f.file_name, f.file_path, kbf.active FROM {self.table} AS kbf INNER JOIN knowledge_base AS kb ON kb.id=kbf.knowledge_base_id INNER JOIN uploaded_files AS f ON f.id=kbf.file_id WHERE knowledge_base_id = ? AND kb.line_id = ?', (knowledge_base_id, line_id, ))
11 |
12 | def add_file_to_knowledge_base(self, line_id, knowledge_base_id, file_id):
13 | # values = ', '.join(['?'] * len(file_ids))
14 |
15 | return self.sql_query(f"""
16 | INSERT INTO knowledge_base_file (knowledge_base_id, file_id)
17 | SELECT kb.id, uf.id
18 | FROM knowledge_base kb
19 | CROSS JOIN uploaded_files uf
20 | WHERE kb.id = ?
21 | AND kb.line_id = ?
22 | AND uf.line_id = ?
23 | AND uf.id IN (?)
24 | """, (knowledge_base_id, line_id, line_id, file_id,))
25 |
26 | def setActive(self, line_id, knowledge_base_id, file_id, active):
27 | return self.sql_query(f"""
28 | UPDATE {self.table} SET active =?
29 | WHERE knowledge_base_id=(SELECT id FROM knowledge_base WHERE id=? AND line_id=?)
30 | AND file_id=(SELECT id FROM uploaded_files WHERE id=? AND line_id=?)
31 | """, (active, knowledge_base_id, line_id, file_id, line_id))
32 |
33 | def delete_file_from_knowledge_base(self, knowledge_base_id, file_id, line_id):
34 | return self.sql_query(f"""
35 | DELETE FROM {self.table}
36 | WHERE knowledge_base_id = (SELECT id FROM knowledge_base WHERE id = ? AND line_id = ?)
37 | AND file_id = (SELECT id FROM uploaded_files WHERE id = ? AND line_id = ?)
38 | """, (knowledge_base_id, line_id, file_id, line_id))
39 |
40 | knowledgeBaseFile = KnowledgeBaseFile()
--------------------------------------------------------------------------------
/Model/LlmModel.py:
--------------------------------------------------------------------------------
1 | from Model.BaseModel import BaseModel
2 |
3 | class LlmModel(BaseModel):
4 | def __init__(self):
5 | super().__init__()
6 | self.filed = ['id', 'name', 'base_url', 'token']
7 | self.table = 'llm_model'
8 |
9 | def get_all_models(self):
10 | return self.sql_query(f"SELECT id, name from {self.table}")
--------------------------------------------------------------------------------
/Model/Setting.py:
--------------------------------------------------------------------------------
1 | import os
2 | from dotenv import load_dotenv
3 |
4 | if not os.getenv("IN_DOCKER", ""):
5 | load_dotenv(override=True)
6 |
7 | class Setting:
8 | def __init__(self):
9 | self.LINE_CHANNEL_ACCESS_TOKEN = os.getenv("LINE_CHANNEL_ACCESS_TOKEN", "YOUR_LINE_CHANNEL_ACCESS_TOKEN")
10 | self.LINE_CHANNEL_SECRET = os.getenv("LINE_CHANNEL_SECRET", "YOUR_LINE_CHANNEL_SECRET")
11 | self.LINE_LIFF_ID = os.environ.get("LINE_LIFF_ID") or os.getenv("LINE_LIFF_ID", "YOUR_LINE_LIFF_ID")
12 | self.LINE_LOGIN_CHANNEL_ID = os.getenv("LINE_LOGIN_CHANNEL_ID", "YOUR_LINE_LOGIN_CLIENT_ID")
13 |
14 | self.EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
15 | self.EMBEDDING_DEVICE = os.getenv("EMBEDDING_DEVICE", "cpu")
16 |
17 | self.MODEL_NAME = os.getenv("MODEL_NAME", "llama3-8b-8192|gpt-3.5-turbo|gpt-4-1106-preview").split("|")
18 | self.BASE_URL = os.getenv("BASE_URL", "https://api.groq.com/openai/v1|https://api.openai.com/v1|https://api.openai.com/v1").split("|")
19 | self.API_KEY = os.getenv("API_KEY", "API_KEY_1|API_KEY_2|API_KEY_3").split("|")
20 |
21 | self.FILE_MAX_SIZE = self.space_conversion(os.getenv("FILE_MAX_SIZE", "5MB"))
22 | self.SPACE_PER_USER = self.space_conversion(os.getenv("SPACE_PER_USER", "200MB"))
23 |
24 | self.ALLOW_FILE_TYPE = os.getenv("ALLOW_FILE_TYPE", "pdf,csv,txt").split(",")
25 |
26 | self.MAX_CHAT_HISTORY = int(os.getenv("MAX_CHAT_HISTORY", "5"))
27 | self.VERSION = os.getenv("VERSION", "1.3")
28 |
29 |
30 | def space_conversion(self, space):
31 | space = space.upper()
32 | space = space.replace("B", "")
33 | if 'K' in space:
34 | space = float(space.replace("K", ""))
35 | return int(space * 1024)
36 |
37 | if 'M' in space:
38 | space = float(space.replace("M", ""))
39 | return int(space * 1024 * 1024)
40 |
41 | if 'G' in space:
42 | space = float(space.replace("G", ""))
43 | return int(space * 1024 * 1024 * 1024)
44 |
45 | if 'T' in space:
46 | space = float(space.replace("T", ""))
47 | return int(space * 1024 * 1024 * 1024 * 1024)
48 |
49 | return 0
50 |
51 | def byte_to_kb_or_mb(self, byte):
52 | i = 0
53 | units = ['B', 'KB', 'MB', 'GB', 'TB']
54 | for i, unit in enumerate(units):
55 | if byte < 1024 ** (i + 1) or i == len(units) - 1:
56 | return f"{byte / (1024 ** i):.1f} {unit}"
57 |
58 | setting = Setting()
--------------------------------------------------------------------------------
/Model/UploadedFiles.py:
--------------------------------------------------------------------------------
1 | from Model.BaseModel import BaseModel
2 | import time
3 | import random
4 | import datetime
5 | import os
6 | from fastapi import UploadFile
7 | import uuid
8 | from Model.Setting import setting
9 | from fastapi import HTTPException
10 |
11 | class UploadedFiles(BaseModel):
12 | def __init__(self):
13 | super().__init__()
14 | self.filed = ['id', 'line_id', 'file_name', 'file_path']
15 | self.table = 'uploaded_files'
16 |
17 | def get_file(self, id, line_id):
18 | return self.sql_query('SELECT file_name, file_path FROM uploaded_files WHERE id =? AND line_id =?', (id, line_id, ))
19 |
20 | def get_file_amount(self, line_id):
21 | return self.sql_query('SELECT COUNT(id) FROM uploaded_files WHERE line_id =?', (line_id, ))[0][0]
22 |
23 | def get_all_files_list(self, line_id):
24 | return self.sql_query('SELECT * FROM uploaded_files WHERE line_id = ?', (line_id, ))
25 |
26 | async def upload_file(self, line_id, filename, filepath):
27 | try:
28 | return self.sql_query('INSERT INTO uploaded_files (id, line_id, file_name, file_path) VALUES (null, ?, ?, ?)', (line_id, filename, filepath, ), True)
29 | except Exception as e:
30 | raise {"error": str(e)}
31 |
32 | def delete_file(self, file_id, line_id):
33 | return self.sql_query('DELETE FROM uploaded_files WHERE id =? AND line_id =?', (file_id, line_id, ))
34 |
35 |
36 | uploadedFiles = UploadedFiles()
--------------------------------------------------------------------------------
/Model/UserSelectKnowledgeBace.py:
--------------------------------------------------------------------------------
1 | from Model.BaseModel import BaseModel
2 |
3 | class UserSelectKnowledgeBace(BaseModel):
4 | def __init__(self):
5 | super().__init__()
6 | self.filed = ['line_id', 'knowledge_base_id']
7 | self.table = 'user_select_knowledge_base'
8 |
9 | def getData(self, user_id):
10 | return self.sql_query(f"SELECT * FROM {self.table} WHERE line_id=?", (user_id, ))
11 |
12 | def changeSelected(self, line_id: str, knowledge_base_id: int):
13 | if len(self.getData(line_id)) == 0:
14 | self.sql_query(f"INSERT INTO {self.table} (line_id, knowledge_base_id) VALUES (?,?)", (line_id, knowledge_base_id, ))
15 | else:
16 | self.sql_query(f"UPDATE {self.table} SET knowledge_base_id=? WHERE line_id=?", (knowledge_base_id, line_id, ))
17 |
18 | userSelectKnowledgeBace = UserSelectKnowledgeBace()
--------------------------------------------------------------------------------
/Model/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADT109119/ChatPDF-LineBot/25594ee1964c3492e6ee3df1047b82b4de7fe16a/Model/__init__.py
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Line PDF 問答機器人
2 |
3 | 這是一個使用 LangChain、FastAPI 和 Vue 構建的 Line Bot,可以透過 Line Liff 網頁上傳 PDF 文件並回答相關問題。
4 |
5 | ## 範例
6 |
7 | |  |  |Demo 連結(每次更新時刪檔)  https://lin.ee/QajGJOY|
8 | |------|------|------|
9 |
10 | ## 說明文件
11 |
12 | * [中文](https://adt109119.github.io/ChatPDF-LineBot-Docs/)
13 | * [English](https://adt109119.github.io/ChatPDF-LineBot-Docs/en/) (使用 ChatGPT 翻譯)
14 |
15 | ## 功能
16 |
17 | 目前支援以下幾種功能:
18 | - [x] 上傳 PDF 文件
19 | - [x] 自然語言處理,從 PDF 中提取相關內容回答問題
20 | - [x] 自動 Embedding 文件
21 | - [x] 使用 Line Bot 進行互動
22 |
23 | 尚待完成:
24 | - [ ] 顯示用戶使用量
25 | - [ ] 文件預覽
26 | - [ ] 功能列表
27 |
28 | ## 技術
29 |
30 | - **LangChain**: 用於構建問答系統和文本處理
31 | - **FastAPI**: 用於構建 Web API 服務
32 | - **Vue.js**: 用於構建用戶介面
33 | - **Line Bot SDK**: 用於與 Line 平台集成
34 |
35 | ## Hugging Face 一鍵部署
36 |
37 | 本專案可以快速部署在 Hugging Face 上
38 |
39 | 支援 CloudFlare Tunnel 自訂網址
40 |
41 | 但要注意若無購買 Hugging Face 的永久儲存空間
42 |
43 | 每次更新或更改資料時檔案都會遺失
44 |
45 | [](https://huggingface.co/spaces/ADT109119/ChatPDF-LineBot?duplicate=true)
46 |
47 | ## 安裝
48 | > 使用 Docker 的人可直接跳至第 5 步
49 |
50 | ### 1. 複製存儲庫:
51 | ```
52 | git clone https://github.com/ADT109119/ChatPDF-LineBot-Docs.git
53 | ```
54 |
55 | ### 2. 安裝伺服器依賴:
56 | ```
57 | cd ChatPDF-LineBot
58 | pip install -r requirements.txt
59 | ```
60 | 可考慮在虛擬環境中執行,這裡提供 Python 內建的虛擬環境指令:
61 | ```
62 | cd ChatPDF-LineBot
63 | python -m venv .\venv
64 | .\venv\Scripts\activate
65 | pip install -r requirements.txt
66 | ```
67 |
68 | ### 3. 安裝 Node 依賴:
69 | ```
70 | cd ChatPDF-LineBot/View
71 | npn install
72 | ```
73 |
74 | ### 4. Build 前端網頁
75 | ```
76 | cd View
77 | npm install
78 | npm run build
79 | ```
80 |
81 | ### 5. 登入 LINE 平台
82 | * 創建 Line Bot。
83 | * 新增一個提供者(Provider),例如「My Provider」。
84 | * 在「My Provider」新增一個類型為「Messaging API」的頻道(Channel),例如「My AI Assistant」。
85 | * 進到「My AI Assistant」頻道頁面,點選「Messaging API」頁籤,生成一個頻道的 channel access token。
86 | * 創建 LIFF 網頁。
87 | * 在與「Messaging API」同一個 `Provider` 內(例如:「My Provider」)新增一個類型為「Line Login」的頻道(Channel),例如「My LIFF Page」。
88 | * 進到「My LIFF Page」頻道頁面,點選「LIFF」頁籤,創建一個新的 LIFF 網頁。
89 | * `Endpoint URL` 填入 `https://<你的網域或IP地址>/liff`(可先隨便填後續再來改)。
90 | * `Scopes` 選項請勾選「chat_message.write」、「openid」。
91 | * 點選 `Add` 創建。
92 |
93 | ### 6. 登入 OpenAI、Groq 平台(或其他平台)
94 | * 生成一個 OpenAI 的 API key。
95 | * 也可以生成其他平台的API Key(例如 Groq),只是該平台的 API 必須與 OpenAI 兼容,且要記得在下一步改`BASE_URL`
96 |
97 | ### 7. 設定環境變數
98 |
99 | 伺服器或 Docker 可直接設定環境變數
100 |
101 | 自己電腦上跑的話可複製 `.env.example` 文件,並改名為 `.env`,接著設定 API Key、模型等參數。
102 |
103 | 以下為 `環境變數列表` `*` 代表必改項目
104 |
105 | #### 環境變數列表
106 |
107 | | 變數 | 說明 | 預設值 |
108 | |------|------|------|
109 | | `MODEL_NAME` | 用於問答的模型名稱,多個模型以`\|`分隔 | `llama3-8b-8192\|gpt-3.5-turbo\|gpt-4-1106-preview` |
110 | | `BASE_URL` | 對應模型的 API 基礎 URL,多個 URL 以`\|`分隔 | `https://api.groq.com/openai/v1\|https://api.openai.com/v1\|https://api.openai.com/v1` |
111 | | * `API_KEY` | 對應模型的 API 密鑰,多個密鑰以`\|`分隔 | `API_KEY_1\|API_KEY_2\|API_KEY_3` |
112 | | `MAX_CHAT_HISTORY` | 保留的最大聊天記錄數 | `5` |
113 | | `EMBEDDING_MODEL` | 用於文本嵌入的模型名稱(請填入HF模型路徑) | `sentence-transformers/all-MiniLM-L6-v2` |
114 | | `EMBEDDING_DEVICE` | 運行文本嵌入模型的設備(cpu或cuda,可用cuda:0、cuda:1選擇顯卡) | `cpu` |
115 | | * `LINE_CHANNEL_ACCESS_TOKEN` | Line Bot 的access token | `YOUR_LINE_CHANNEL_ACCESS_TOKEN` |
116 | | * `LINE_CHANNEL_SECRET` | Line Bot 的secret | `YOUR_LINE_CHANNEL_SECRET` |
117 | | * `LINE_LIFF_ID` | Line LIFF 網頁 ID | `YOUR_LINE_LIFF_ID` |
118 | | * `LINE_LOGIN_CHANNEL_ID` | Line LIFF 所在的 LINE Login Channel ID | `YOUR_LINE_LOGIN_CHANNEL_ID` |
119 | | `FILE_MAX_SIZE` | 允許上傳的最大文件大小 | `5MB` |
120 | | `SPACE_PER_USER` | 每個用戶可用的最大空間大小 | `200MB` |
121 | | `ALLOW_FILE_TYPE` | 允許上傳的文件類型,多個類型以`,`分隔 | `pdf,csv,txt` |
122 |
123 |
124 | ### 8. 運行伺服器
125 |
126 | #### 使用 Docker
127 |
128 | 
129 |
130 | ```
131 | docker run -d \
132 | --name chatpdf-linebot \
133 | -p 8000:8000 \
134 | -v /local/file/store/path:/app/db
135 | -e LINE_CHANNEL_ACCESS_TOKEN=YOUR_LINE_CHANNEL_ACCESS_TOKEN \
136 | -e LINE_CHANNEL_SECRET=YOUR_LINE_CHANNEL_SECRET \
137 | -e LINE_LIFF_ID=YOUR_LINE_LIFF_ID \
138 | -e LINE_LOGIN_CHANNEL_ID=YOUR_LINE_LOGIN_CHANNEL_ID \
139 | -e MODEL_NAME=YOUR_MODELS \
140 | -e BASE_URL=BASE_URLS \
141 | -e API_KEY=API_KEYS \
142 | adt109119/chatpdf-linebot
143 | ```
144 |
145 | #### 本機直接運行
146 |
147 | 若 1. ~ 4. 步的安裝皆無問題,僅需直接執行以下指令便可開啟伺服器
148 |
149 | 預設使用 `PORT` `8000`
150 | ```
151 | python .\main.py
152 | ```
153 |
154 | ### 9. 回到 LINE 設定
155 | * Line Bot 設定
156 | * 進到「My AI Assistant」頻道頁面,點選「Messaging API」頁籤,設置「Webhook URL」,填入應用程式網址並加上「/callback」路徑,例如 `https://line.the-walking.fish.com/callback`,點選「Update」按鈕。
157 | * 點選「Verify」按鈕,驗證是否呼叫成功。
158 | * 將「Use webhook」功能開啟。
159 | * 將「Auto-reply messages」功能關閉。
160 | * 將「Greeting messages」功能關閉。
161 |
162 | * Line LIFF 網頁設定
163 | * 進到「LIFF」頁籤
164 | * 若原本`Endpoint URL` 隨便填的,現在請正式填入 LIFF 網頁網址,路徑為「/liff」,例如 `https://line.the-walking.fish.com/liff`。
165 | * 回到「LIFF」頁籤,複製「LIFF URL」
166 |
167 | * 圖文選單設定
168 | * 進到「LINE Official Account Manager」
169 | * 在側邊欄找到「圖文選單」
170 | * 點選「建立」
171 | * 名稱、版型等可照自己的喜好填寫
172 | * 在「動作」選擇「連結」,並填入上一部複製的 LIFF URL
173 | * 按「儲存」
174 |
175 | ## 貢獻
176 |
177 | 如果您有任何改進建議或錯誤修復,歡迎提交 Pull Request。
178 |
179 |
180 |
181 |
182 |
183 | ## 聯繫作者
184 |
185 | 你可以透過以下方式與我聯絡
186 |
187 | - 2.jerry32262686@gmail.com
188 |
189 |
190 | ## License
191 | This project is under the Apache 2.0 License. See [LICENSE](https://github.com/ADT109119/ChatPDF-LineBot/blob/main/LICENSE) for further details.
192 |
193 |
--------------------------------------------------------------------------------
/Service/LineFunction.py:
--------------------------------------------------------------------------------
1 | import config.line_reply_template as config
2 | from Model.Setting import setting
3 | from Model.UploadedFiles import uploadedFiles
4 |
5 | import time
6 |
7 | from Service.upload_file import calc_total_size
8 |
9 | def replay_info(line_id):
10 | temp = config.INFO_TEMPLATE
11 | temp = temp.replace("{$VERSION}", setting.VERSION)
12 | temp = temp.replace("{$FILE_AMOUNT}", str(uploadedFiles.get_file_amount(line_id)))
13 | temp = temp.replace("{$FILE_SIZE_LIMIT}", setting.byte_to_kb_or_mb(setting.FILE_MAX_SIZE))
14 | temp = temp.replace("{$USED_SPACE}", f"{setting.byte_to_kb_or_mb(calc_total_size(line_id))}/{setting.byte_to_kb_or_mb(setting.SPACE_PER_USER)}")
15 | temp = temp.replace("{$USED_SPACE_PERCENTAGE}", str(calc_total_size(line_id)/setting.SPACE_PER_USER * 100)+"%")
16 | temp = temp.replace("{$MAX_CHAT_HISTORY}", str(setting.MAX_CHAT_HISTORY))
17 | return temp
18 |
19 | def replay_help():
20 | t = time.time()
21 | tt = time.localtime(t)
22 | temp = config.HELP_TEMPLATE
23 | temp = temp.replace("{$TIME}", f"{tt.tm_year}{tt.tm_mon}{tt.tm_mday}")
24 | return temp
25 |
26 | def replay_about_me():
27 | return config.ABOUT_ME
--------------------------------------------------------------------------------
/Service/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ADT109119/ChatPDF-LineBot/25594ee1964c3492e6ee3df1047b82b4de7fe16a/Service/__init__.py
--------------------------------------------------------------------------------
/Service/embedding.py:
--------------------------------------------------------------------------------
1 | from Model.Setting import Setting
2 | setting = Setting()
3 | from langchain.embeddings import HuggingFaceEmbeddings
4 | model_name = setting.EMBEDDING_MODEL
5 | model_kwargs = {'device': setting.EMBEDDING_DEVICE}
6 | embedding = HuggingFaceEmbeddings(model_name=model_name,
7 | model_kwargs=model_kwargs)
8 |
9 | import asyncio
10 | from typing import Dict
11 | from datetime import datetime, timedelta
12 | import threading
13 |
14 | # 存儲每個id的重新嵌入任務和最後呼叫時間
15 | re_embedding_tasks: Dict[int, asyncio.Task] = {}
16 | last_call_times: Dict[int, datetime] = {}
17 |
18 | async def re_embedding(line_id: str, id: int, delay: int = 5):
19 | """
20 | Re-embedding function with a delay for a specific id.
21 |
22 | Args:
23 | id (int): The id for which the re-embedding process is being performed.
24 | delay (int, optional): The delay in seconds before executing the re-embedding process. Defaults to 5.
25 | """
26 | global re_embedding_tasks, last_call_times
27 |
28 | # 獲取當前時間
29 | now = datetime.now()
30 |
31 | # 檢查是否已經有任務在執行,以及是否在延遲時間內被再次呼叫
32 | if id in re_embedding_tasks and (now - last_call_times[id]) < timedelta(seconds=delay):
33 | # 取消之前的任務
34 | re_embedding_tasks[id].cancel()
35 |
36 | # 更新最後呼叫時間
37 | last_call_times[id] = now
38 |
39 | # 定義實際的重新嵌入協程
40 | async def re_embedding_coroutine():
41 | # 等待指定的延遲時間
42 | await asyncio.sleep(delay)
43 |
44 | # 檢查在延遲時間內是否有重複呼叫
45 | if id in re_embedding_tasks and (datetime.now() - last_call_times[id]) < timedelta(seconds=delay):
46 | # 如果有重複呼叫,則重新計時
47 | return
48 |
49 | # 執行重新生成向量資料庫的過程
50 | files = knowledgeBaseFile.get_all_files(line_id, id)
51 | print(len(files))
52 | if len(files) > 0:
53 | # 建立一個子執行緒
54 | t = threading.Thread(target = generate_vectordb, args = (line_id, id, files))
55 | # 執行該子執行緒
56 | t.start()
57 |
58 | # 從字典中移除任務和最後呼叫時間
59 | del re_embedding_tasks[id]
60 | del last_call_times[id]
61 |
62 | # 創建新的任務
63 | re_embedding_tasks[id] = asyncio.create_task(re_embedding_coroutine())
64 |
65 |
66 | from langchain.document_loaders import PyMuPDFLoader, TextLoader
67 | import glob
68 | from langchain_community.document_loaders.csv_loader import CSVLoader
69 | from langchain.text_splitter import RecursiveCharacterTextSplitter
70 | from langchain.vectorstores import Chroma
71 | import os
72 | from Model.KnowledgeBaseFile import knowledgeBaseFile
73 |
74 | def generate_vectordb(line_id, id, files):
75 | datas = []
76 |
77 | # files = knowledgeBaseFile.get_all_files(line_id, id)
78 | for i in files:
79 | path = i[2]
80 | active = i[3]
81 | if active == 0:
82 | continue
83 |
84 | if path.endswith(".pdf"):
85 | loader = PyMuPDFLoader(path)
86 | datas.extend(loader.load())
87 | elif path.endswith(".csv"):
88 | loader = CSVLoader(file_path=path, encoding="utf-8")
89 | datas.extend(loader.load())
90 | elif path.endswith(".txt"):
91 | loader = TextLoader(file_path=path, encoding="utf-8")
92 | datas.extend(loader.load())
93 |
94 | if len(datas) == 0:
95 | return
96 |
97 | text_splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=5)
98 | all_splits = text_splitter.split_documents(datas)
99 |
100 | persist_directory = f"./db/vector_db/{line_id}"
101 | if not os.path.exists(persist_directory):
102 | try:
103 | os.makedirs(persist_directory)
104 | except:
105 | raise
106 |
107 | print("embedding... ...")
108 | db = Chroma(
109 | embedding_function=embedding,
110 | persist_directory=persist_directory,
111 | collection_name = f"{line_id}_{id}",
112 | collection_metadata={"hnsw:space": "cosine"}
113 | )
114 | db.delete_collection()
115 |
116 | Chroma.from_documents(
117 | documents=all_splits,
118 | embedding=embedding,
119 | persist_directory=persist_directory,
120 | collection_name = f"{line_id}_{id}",
121 | collection_metadata={"hnsw:space": "cosine"})
122 | print("embedding finish!")
123 |
--------------------------------------------------------------------------------
/Service/llm.py:
--------------------------------------------------------------------------------
1 | import time
2 | from Model.Setting import Setting
3 |
4 | from Model.KnowledgeBase import knowledgeBase
5 | from Model.UserSelectKnowledgeBace import UserSelectKnowledgeBace
6 | from Model.ChatHistory import chatHistory
7 | from Model.Setting import setting
8 |
9 | from langchain.vectorstores import Chroma
10 | from Service.embedding import embedding
11 | from langchain_openai import ChatOpenAI
12 |
13 | from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder, HumanMessagePromptTemplate, AIMessagePromptTemplate, SystemMessagePromptTemplate
14 | from langchain_core.output_parsers import StrOutputParser
15 | from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
16 | from operator import itemgetter
17 |
18 |
19 | def chat_llm(text: str, user_id: str):
20 | print(user_id)
21 | try:
22 | select_db = UserSelectKnowledgeBace().getData(user_id)[0][1]
23 | print(select_db)
24 | vectordb = Chroma(
25 | embedding_function=embedding,
26 | persist_directory=f"./db/vector_db/{user_id}",
27 | collection_name=f"{user_id}_{select_db}",
28 | collection_metadata={"hnsw:space": "cosine"}
29 | )
30 | except Exception as e:
31 | print(e)
32 | return "查無知識庫"
33 |
34 | KnowledgeBaseSetting = knowledgeBase.get_setting(select_db, user_id)
35 | llm = ChatOpenAI(
36 | base_url=KnowledgeBaseSetting["base_url"],
37 | api_key=KnowledgeBaseSetting["api_key"],
38 | temperature=KnowledgeBaseSetting["temperature"],
39 | model= setting.MODEL_NAME[KnowledgeBaseSetting["model"]],
40 | )
41 |
42 | retriever = vectordb.as_retriever(
43 | search_type="similarity_score_threshold",
44 | search_kwargs={'score_threshold': KnowledgeBaseSetting["score_threshold"],
45 | 'k': KnowledgeBaseSetting["search_item_limit"]
46 | }
47 | )
48 |
49 | qa_prompt = ChatPromptTemplate.from_messages(
50 | [
51 | SystemMessage(content=("""# <
8 | 模型:
9 | {{ store.getters.models[props.kb.model].name }}
10 |
11 |
12 | temperature: {{ props.kb.temperature }}
13 | 搜尋嚴格度: {{ props.kb.score_threshold }}
14 | 單次參考量上限: {{ props.kb.search_item_limit }}
15 |
17 | 選擇知識庫 > 18 |
19 |