Compare commits

...

13 Commits

Author SHA1 Message Date
“brainlds 792bf222de refactor: optimize base image build process
BASE_CI / build_bisheng_arm (push) Has been cancelled
BASE_CI / build_bisheng_amd (push) Has been cancelled
BASE_CI / combine_two_images (push) Has been cancelled
2025-08-11 16:27:41 +08:00
“brainlds 03725fc679 chore: add .gitattributes and standardize line breaks 2025-08-11 12:03:31 +08:00
“brainlds c82de18830 chore: bump llama-index version to 0.13.0 and add relevant dependencies 2025-08-08 14:49:01 +08:00
“brainlds df42971011 fix: rename recover for case change 2025-08-07 11:55:05 +08:00
“brainlds 45618e6834 fix rename recover for case change 2025-08-07 11:54:07 +08:00
“brainlds 0c75fc15da temp rename for case change 2025-08-07 11:46:30 +08:00
“brainlds c3f4e91ed4 fix: change import file name 2025-08-07 11:38:44 +08:00
“brainlds 6fdd1cd8c8 Merge branch 'feat/2.0.0' of https://github.com/dataelement/bisheng into chore/fix-vulnerability 2025-08-07 10:41:29 +08:00
“brainlds 7348388341 style: clean up jwt command 2025-08-07 10:27:27 +08:00
“brainlds 172ada0349 fix: fixed the compatibility issue with PyJWT and decoupling from patch files. 2025-07-29 16:27:10 +08:00
“brainlds fde5ef1419 ci: add branch chore/fix-vulnerability 2025-07-28 20:04:37 +08:00
“brainlds c97375d63f style: clean up comments 2025-07-28 12:02:23 +08:00
“brainlds b502337c06 chore: bump gunicorn version to 23.0.0; bump llama-index version to 0.12.28; bump mcp version to 1.10.0; bump pyjwt version to 2.4.0; bump Pillow version to 10.3.0; bump pyarrow version to 14.0.1; bump pymysql version to 1.1.1; bump python-multipart version to 14.0.1. 2025-07-28 11:38:57 +08:00
14 changed files with 1214 additions and 58 deletions
+1
View File
@@ -316,6 +316,7 @@ steps: # 定义流水线执行步骤,这些步骤将顺序执行
trigger:
branch:
- add_some_branch_you_need
- chore/fix-vulnerability
event:
- push
+34
View File
@@ -0,0 +1,34 @@
# 默认:自动识别文本,统一用 LF 存库
* text=auto eol=lf
# 明确常见文本文件用 LF
*.py text eol=lf
*.sh text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.md text eol=lf
*.txt text eol=lf
*.json text eol=lf
*.toml text eol=lf
*.cfg text eol=lf
*.ini text eol=lf
# Windows 脚本保留 CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
# 二进制:禁止任何换行转换和 diff
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.pdf binary
*.zip binary
*.tar binary
*.gz binary
*.7z binary
*.mp4 binary
*.docx binary
*.xlsx binary
*.pptx binary
-3
View File
@@ -10,8 +10,5 @@ RUN poetry update --without dev
# patch langchain-openai lib. remove this when langchain-openai support reasoning_content
RUN patch -p1 < /app/bisheng/patches/langchain_openai.patch /usr/local/lib/python3.10/site-packages/langchain_openai/chat_models/base.py
# patch fastapi-jwt-auth lib. remove this when remove fastapi-jwt-auth
# fix fastapi-jwt-auth not support pydantic:v2
RUN patch -p1 < /app/bisheng/patches/fastapi_jwt_auth.patch /usr/local/lib/python3.10/site-packages/fastapi_jwt_auth/config.py
CMD ["sh entrypoint.sh"]
+32 -36
View File
@@ -2,53 +2,49 @@ FROM python:3.10-slim
ARG PANDOC_ARCH=amd64
ENV PANDOC_ARCH=$PANDOC_ARCH
ENV PATH="${PATH}:/root/.local/bin"
WORKDIR /app
RUN echo \
deb https://mirrors.aliyun.com/debian/ bookworm main non-free non-free-firmware contrib \
deb-src https://mirrors.aliyun.com/debian/ bookworm main non-free non-free-firmware contrib \
deb https://mirrors.aliyun.com/debian-security/ bookworm-security main \
deb-src https://mirrors.aliyun.com/debian-security/ bookworm-security main \
deb https://mirrors.aliyun.com/debian/ bookworm-updates main non-free non-free-firmware contrib \
deb-src https://mirrors.aliyun.com/debian/ bookworm-updates main non-free non-free-firmware contrib \
deb https://mirrors.aliyun.com/debian/ bookworm-backports main non-free non-free-firmware contrib \
deb-src https://mirrors.aliyun.com/debian/ bookworm-backports main non-free non-free-firmware contrib \
> /etc/apt/sources.list
# 使用国内源 + 安装依赖(合并指令、清理缓存、禁用推荐包)
RUN echo "\
deb https://mirrors.aliyun.com/debian/ bookworm main non-free non-free-firmware contrib\n\
deb https://mirrors.aliyun.com/debian-security/ bookworm-security main\n\
deb https://mirrors.aliyun.com/debian/ bookworm-updates main non-free non-free-firmware contrib\n\
deb https://mirrors.aliyun.com/debian/ bookworm-backports main non-free non-free-firmware contrib" \
> /etc/apt/sources.list && \
apt-get update && \
apt-get install -y --no-install-recommends \
gcc g++ curl build-essential postgresql-server-dev-all libreoffice \
wget procps vim fonts-wqy-zenhei \
libglib2.0-0 libsm6 libxrender1 libxext6 libgl1 \
&& rm -rf /var/lib/apt/lists/*
# 安装 pandoc
RUN mkdir -p /opt/pandoc && \
cd /opt/pandoc && \
wget https://github.com/jgm/pandoc/releases/download/3.6.4/pandoc-3.6.4-linux-${PANDOC_ARCH}.tar.gz && \
tar xvf pandoc-3.6.4-linux-${PANDOC_ARCH}.tar.gz && \
cp pandoc-3.6.4/bin/pandoc /usr/bin/ && \
rm -rf /opt/pandoc
# Install lib
RUN apt-get update && apt-get install gcc g++ curl build-essential postgresql-server-dev-all wget libreoffice -y
RUN apt-get update && apt-get install procps -y
# Install pandoc
RUN mkdir -p /opt/pandoc \
&& cd /opt/pandoc \
&& wget https://github.com/jgm/pandoc/releases/download/3.6.4/pandoc-3.6.4-linux-${PANDOC_ARCH}.tar.gz \
&& tar xvf pandoc-3.6.4-linux-${PANDOC_ARCH}.tar.gz \
&& cd pandoc-3.6.4 \
&& cp bin/pandoc /usr/bin/ \
&& cd ..
# Install font
RUN apt install vim fonts-wqy-zenhei -y
# opencv
RUN apt-get update && apt-get install -y libglib2.0-0 libsm6 libxrender1 libxext6 libgl1
# 安装 Poetry
RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.8.2
# # Add Poetry to PATH
ENV PATH="${PATH}:/root/.local/bin"
# 拷贝项目依赖文件
COPY ./pyproject.toml ./
# 安装 Python 依赖
RUN python -m pip install --upgrade pip && \
pip install shapely==2.0.1
pip install shapely==2.0.1 && \
poetry config virtualenvs.create false && \
poetry install --no-interaction --no-ansi --without dev
# Install dependencies
RUN poetry config virtualenvs.create false
RUN poetry install --no-interaction --no-ansi --without dev
# 安装 NLTK 数据
RUN python -c "import nltk; nltk.download('punkt'); nltk.download('punkt_tab'); nltk.download('averaged_perceptron_tagger'); nltk.download('averaged_perceptron_tagger_eng')"
# install nltk_data
RUN python -c "import nltk; nltk.download('punkt'); nltk.download('punkt_tab'); nltk.download('averaged_perceptron_tagger'); nltk.download('averaged_perceptron_tagger_eng'); "
COPY . .
CMD ["sh", "entrypoint.sh"]
CMD ["sh entrypoint.sh"]
@@ -13,9 +13,9 @@ from langchain_core.language_models import BaseLanguageModel
from langchain_core.prompts import HumanMessagePromptTemplate, PromptTemplate
try:
from llama_index.node_parser import SimpleNodeParser
from llama_index.readers.schema import Document as LlamaindexDocument
from llama_index.schema import BaseNode
from llama_index.core.node_parser import SimpleNodeParser
from llama_index.core.schema import Document as LlamaindexDocument
from llama_index.core.schema import BaseNode
except ImportError:
raise ImportError(
"llama_index must be installed to use this function. "
@@ -8,10 +8,10 @@ import pandas as pd
nest_asyncio.apply()
from collections import defaultdict
from llama_index import ServiceContext
from llama_index.core import ServiceContext
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.evaluation import CorrectnessEvaluator
from llama_index.llms import OpenAI
from llama_index.core.evaluation import CorrectnessEvaluator
from llama_index.llms.openai import OpenAI
from tqdm import tqdm
openai_api_key = os.environ.get('OPENAI_API_KEY', '')
+5
View File
@@ -0,0 +1,5 @@
"""FastAPI extension that provides JWT Auth support (secure, easy to use and lightweight)"""
__version__ = "0.5.0"
from .auth_jwt import AuthJWT
+111
View File
@@ -0,0 +1,111 @@
from fastapi_jwt_auth.config import LoadConfig
from pydantic import ValidationError
from typing import Callable, List
from datetime import timedelta
class AuthConfig:
_token = None
_token_location = {'headers'}
_secret_key = None
_public_key = None
_private_key = None
_algorithm = "HS256"
_decode_algorithms = None
_decode_leeway = 0
_encode_issuer = None
_decode_issuer = None
_decode_audience = None
_denylist_enabled = False
_denylist_token_checks = {'access','refresh'}
_header_name = "Authorization"
_header_type = "Bearer"
_token_in_denylist_callback = None
_access_token_expires = timedelta(minutes=15)
_refresh_token_expires = timedelta(days=30)
# option for create cookies
_access_cookie_key = "access_token_cookie"
_refresh_cookie_key = "refresh_token_cookie"
_access_cookie_path = "/"
_refresh_cookie_path = "/"
_cookie_max_age = None
_cookie_domain = None
_cookie_secure = False
_cookie_samesite = None
# option for double submit csrf protection
_cookie_csrf_protect = True
_access_csrf_cookie_key = "csrf_access_token"
_refresh_csrf_cookie_key = "csrf_refresh_token"
_access_csrf_cookie_path = "/"
_refresh_csrf_cookie_path = "/"
_access_csrf_header_name = "X-CSRF-Token"
_refresh_csrf_header_name = "X-CSRF-Token"
_csrf_methods = {'POST','PUT','PATCH','DELETE'}
@property
def jwt_in_cookies(self) -> bool:
return 'cookies' in self._token_location
@property
def jwt_in_headers(self) -> bool:
return 'headers' in self._token_location
@classmethod
def load_config(cls, settings: Callable[...,List[tuple]]) -> "AuthConfig":
try:
config = LoadConfig(**{key.lower():value for key,value in settings()})
cls._token_location = config.authjwt_token_location
cls._secret_key = config.authjwt_secret_key
cls._public_key = config.authjwt_public_key
cls._private_key = config.authjwt_private_key
cls._algorithm = config.authjwt_algorithm
cls._decode_algorithms = config.authjwt_decode_algorithms
cls._decode_leeway = config.authjwt_decode_leeway
cls._encode_issuer = config.authjwt_encode_issuer
cls._decode_issuer = config.authjwt_decode_issuer
cls._decode_audience = config.authjwt_decode_audience
cls._denylist_enabled = config.authjwt_denylist_enabled
cls._denylist_token_checks = config.authjwt_denylist_token_checks
cls._header_name = config.authjwt_header_name
cls._header_type = config.authjwt_header_type
cls._access_token_expires = config.authjwt_access_token_expires
cls._refresh_token_expires = config.authjwt_refresh_token_expires
# option for create cookies
cls._access_cookie_key = config.authjwt_access_cookie_key
cls._refresh_cookie_key = config.authjwt_refresh_cookie_key
cls._access_cookie_path = config.authjwt_access_cookie_path
cls._refresh_cookie_path = config.authjwt_refresh_cookie_path
cls._cookie_max_age = config.authjwt_cookie_max_age
cls._cookie_domain = config.authjwt_cookie_domain
cls._cookie_secure = config.authjwt_cookie_secure
cls._cookie_samesite = config.authjwt_cookie_samesite
# option for double submit csrf protection
cls._cookie_csrf_protect = config.authjwt_cookie_csrf_protect
cls._access_csrf_cookie_key = config.authjwt_access_csrf_cookie_key
cls._refresh_csrf_cookie_key = config.authjwt_refresh_csrf_cookie_key
cls._access_csrf_cookie_path = config.authjwt_access_csrf_cookie_path
cls._refresh_csrf_cookie_path = config.authjwt_refresh_csrf_cookie_path
cls._access_csrf_header_name = config.authjwt_access_csrf_header_name
cls._refresh_csrf_header_name = config.authjwt_refresh_csrf_header_name
cls._csrf_methods = config.authjwt_csrf_methods
except ValidationError:
raise
except Exception:
raise TypeError("Config must be pydantic 'BaseSettings' or list of tuple")
@classmethod
def token_in_denylist_loader(cls, callback: Callable[...,bool]) -> "AuthConfig":
"""
This decorator sets the callback function that will be called when
a protected endpoint is accessed and will check if the JWT has been
been revoked. By default, this callback is not used.
*HINT*: The callback must be a function that takes decrypted_token argument,
args for object AuthJWT and this is not used, decrypted_token is decode
JWT (python dictionary) and returns *`True`* if the token has been deny,
or *`False`* otherwise.
"""
cls._token_in_denylist_callback = callback
+848
View File
@@ -0,0 +1,848 @@
import jwt, re, uuid, hmac
from jwt.algorithms import requires_cryptography, has_crypto
from datetime import datetime, timezone, timedelta
from typing import Optional, Dict, Union, Sequence
from fastapi import Request, Response, WebSocket
from fastapi_jwt_auth.auth_config import AuthConfig
from fastapi_jwt_auth.exceptions import (
InvalidHeaderError,
CSRFError,
JWTDecodeError,
RevokedTokenError,
MissingTokenError,
AccessTokenRequired,
RefreshTokenRequired,
FreshTokenRequired
)
class AuthJWT(AuthConfig):
def __init__(self,req: Request = None, res: Response = None):
"""
Get jwt header from incoming request or get
request and response object if jwt in the cookie
:param req: all incoming request
:param res: response from endpoint
"""
if res and self.jwt_in_cookies:
self._response = res
if req:
# get request object when cookies in token location
if self.jwt_in_cookies:
self._request = req
# get jwt in headers when headers in token location
if self.jwt_in_headers:
auth = req.headers.get(self._header_name.lower())
if auth: self._get_jwt_from_headers(auth)
def _get_jwt_from_headers(self,auth: str) -> "AuthJWT":
"""
Get token from the headers
:param auth: value from HeaderName
"""
header_name, header_type = self._header_name, self._header_type
parts = auth.split()
# Make sure the header is in a valid format that we are expecting, ie
if not header_type:
# <HeaderName>: <JWT>
if len(parts) != 1:
msg = "Bad {} header. Expected value '<JWT>'".format(header_name)
raise InvalidHeaderError(status_code=422,message=msg)
self._token = parts[0]
else:
# <HeaderName>: <HeaderType> <JWT>
if not re.match(r"{}\s".format(header_type),auth) or len(parts) != 2:
msg = "Bad {} header. Expected value '{} <JWT>'".format(header_name,header_type)
raise InvalidHeaderError(status_code=422,message=msg)
self._token = parts[1]
def _get_jwt_identifier(self) -> str:
return str(uuid.uuid4())
def _get_int_from_datetime(self,value: datetime) -> int:
"""
:param value: datetime with or without timezone, if don't contains timezone
it will managed as it is UTC
:return: Seconds since the Epoch
"""
if not isinstance(value, datetime): # pragma: no cover
raise TypeError('a datetime is required')
return int(value.timestamp())
def _get_secret_key(self, algorithm: str, process: str) -> str:
"""
Get key with a different algorithm
:param algorithm: algorithm for decode and encode token
:param process: for indicating get key for encode or decode token
:return: plain text or RSA depends on algorithm
"""
symmetric_algorithms, asymmetric_algorithms = {"HS256","HS384","HS512"}, requires_cryptography
if algorithm not in symmetric_algorithms and algorithm not in asymmetric_algorithms:
raise ValueError("Algorithm {} could not be found".format(algorithm))
if algorithm in symmetric_algorithms:
if not self._secret_key:
raise RuntimeError(
"authjwt_secret_key must be set when using symmetric algorithm {}".format(algorithm)
)
return self._secret_key
if algorithm in asymmetric_algorithms and not has_crypto:
raise RuntimeError(
"Missing dependencies for using asymmetric algorithms. run 'pip install fastapi-jwt-auth[asymmetric]'"
)
if process == "encode":
if not self._private_key:
raise RuntimeError(
"authjwt_private_key must be set when using asymmetric algorithm {}".format(algorithm)
)
return self._private_key
if process == "decode":
if not self._public_key:
raise RuntimeError(
"authjwt_public_key must be set when using asymmetric algorithm {}".format(algorithm)
)
return self._public_key
def _create_token(
self,
subject: Union[str,int],
type_token: str,
exp_time: Optional[int],
fresh: Optional[bool] = False,
algorithm: Optional[str] = None,
headers: Optional[Dict] = None,
issuer: Optional[str] = None,
audience: Optional[Union[str,Sequence[str]]] = None,
user_claims: Optional[Dict] = {}
) -> str:
"""
Create token for access_token and refresh_token (utf-8)
:param subject: Identifier for who this token is for example id or username from database.
:param type_token: indicate token is access_token or refresh_token
:param exp_time: Set the duration of the JWT
:param fresh: Optional when token is access_token this param required
:param algorithm: algorithm allowed to encode the token
:param headers: valid dict for specifying additional headers in JWT header section
:param issuer: expected issuer in the JWT
:param audience: expected audience in the JWT
:param user_claims: Custom claims to include in this token. This data must be dictionary
:return: Encoded token
"""
# Validation type data
if not isinstance(subject, (str,int)):
raise TypeError("subject must be a string or integer")
if not isinstance(fresh, bool):
raise TypeError("fresh must be a boolean")
if audience and not isinstance(audience, (str, list, tuple, set, frozenset)):
raise TypeError("audience must be a string or sequence")
if algorithm and not isinstance(algorithm, str):
raise TypeError("algorithm must be a string")
if user_claims and not isinstance(user_claims, dict):
raise TypeError("user_claims must be a dictionary")
# Data section
reserved_claims = {
"sub": subject,
"iat": self._get_int_from_datetime(datetime.now(timezone.utc)),
"nbf": self._get_int_from_datetime(datetime.now(timezone.utc)),
"jti": self._get_jwt_identifier()
}
custom_claims = {"type": type_token}
# for access_token only fresh needed
if type_token == 'access':
custom_claims['fresh'] = fresh
# if cookie in token location and csrf protection enabled
if self.jwt_in_cookies and self._cookie_csrf_protect:
custom_claims['csrf'] = self._get_jwt_identifier()
if exp_time:
reserved_claims['exp'] = exp_time
if issuer:
reserved_claims['iss'] = issuer
if audience:
reserved_claims['aud'] = audience
algorithm = algorithm or self._algorithm
try:
secret_key = self._get_secret_key(algorithm,"encode")
except Exception:
raise
return jwt.encode(
{**reserved_claims, **custom_claims, **user_claims},
secret_key,
algorithm=algorithm,
headers=headers
)
def _has_token_in_denylist_callback(self) -> bool:
"""
Return True if token denylist callback set
"""
return self._token_in_denylist_callback is not None
def _check_token_is_revoked(self, raw_token: Dict[str,Union[str,int,bool]]) -> None:
"""
Ensure that AUTHJWT_DENYLIST_ENABLED is true and callback regulated, and then
call function denylist callback with passing decode JWT, if true
raise exception Token has been revoked
"""
if not self._denylist_enabled:
return
if not self._has_token_in_denylist_callback():
raise RuntimeError("A token_in_denylist_callback must be provided via "
"the '@AuthJWT.token_in_denylist_loader' if "
"authjwt_denylist_enabled is 'True'")
if self._token_in_denylist_callback.__func__(raw_token):
raise RevokedTokenError(status_code=401,message="Token has been revoked")
def _get_expired_time(
self,
type_token: str,
expires_time: Optional[Union[timedelta,int,bool]] = None
) -> Union[None,int]:
"""
Dynamic token expired, if expires_time is False exp claim not created
:param type_token: indicate token is access_token or refresh_token
:param expires_time: duration expired jwt
:return: duration exp claim jwt
"""
if expires_time and not isinstance(expires_time, (timedelta,int,bool)):
raise TypeError("expires_time must be between timedelta, int, bool")
if expires_time is not False:
if type_token == 'access':
expires_time = expires_time or self._access_token_expires
if type_token == 'refresh':
expires_time = expires_time or self._refresh_token_expires
if expires_time is not False:
if isinstance(expires_time, bool):
if type_token == 'access':
expires_time = self._access_token_expires
if type_token == 'refresh':
expires_time = self._refresh_token_expires
if isinstance(expires_time, timedelta):
expires_time = int(expires_time.total_seconds())
return self._get_int_from_datetime(datetime.now(timezone.utc)) + expires_time
else:
return None
def create_access_token(
self,
subject: Union[str,int],
fresh: Optional[bool] = False,
algorithm: Optional[str] = None,
headers: Optional[Dict] = None,
expires_time: Optional[Union[timedelta,int,bool]] = None,
audience: Optional[Union[str,Sequence[str]]] = None,
user_claims: Optional[Dict] = {}
) -> str:
"""
Create a access token with 15 minutes for expired time (default),
info for param and return check to function create token
:return: hash token
"""
return self._create_token(
subject=subject,
type_token="access",
exp_time=self._get_expired_time("access",expires_time),
fresh=fresh,
algorithm=algorithm,
headers=headers,
audience=audience,
user_claims=user_claims,
issuer=self._encode_issuer
)
def create_refresh_token(
self,
subject: Union[str,int],
algorithm: Optional[str] = None,
headers: Optional[Dict] = None,
expires_time: Optional[Union[timedelta,int,bool]] = None,
audience: Optional[Union[str,Sequence[str]]] = None,
user_claims: Optional[Dict] = {}
) -> str:
"""
Create a refresh token with 30 days for expired time (default),
info for param and return check to function create token
:return: hash token
"""
return self._create_token(
subject=subject,
type_token="refresh",
exp_time=self._get_expired_time("refresh",expires_time),
algorithm=algorithm,
headers=headers,
audience=audience,
user_claims=user_claims
)
def _get_csrf_token(self,encoded_token: str) -> str:
"""
Returns the CSRF double submit token from an encoded JWT.
:param encoded_token: The encoded JWT
:return: The CSRF double submit token
"""
return self._verified_token(encoded_token)['csrf']
def set_access_cookies(
self,
encoded_access_token: str,
response: Optional[Response] = None,
max_age: Optional[int] = None
) -> None:
"""
Configures the response to set access token in a cookie.
this will also set the CSRF double submit values in a separate cookie
:param encoded_access_token: The encoded access token to set in the cookies
:param response: The FastAPI response object to set the access cookies in
:param max_age: The max age of the cookie value should be the number of seconds (integer)
"""
if not self.jwt_in_cookies:
raise RuntimeWarning(
"set_access_cookies() called without 'authjwt_token_location' configured to use cookies"
)
if max_age and not isinstance(max_age,int):
raise TypeError("max_age must be a integer")
if response and not isinstance(response,Response):
raise TypeError("The response must be an object response FastAPI")
response = response or self._response
# Set the access JWT in the cookie
response.set_cookie(
self._access_cookie_key,
encoded_access_token,
max_age=max_age or self._cookie_max_age,
path=self._access_cookie_path,
domain=self._cookie_domain,
secure=self._cookie_secure,
httponly=True,
samesite=self._cookie_samesite
)
# If enabled, set the csrf double submit access cookie
if self._cookie_csrf_protect:
response.set_cookie(
self._access_csrf_cookie_key,
self._get_csrf_token(encoded_access_token),
max_age=max_age or self._cookie_max_age,
path=self._access_csrf_cookie_path,
domain=self._cookie_domain,
secure=self._cookie_secure,
httponly=False,
samesite=self._cookie_samesite
)
def set_refresh_cookies(
self,
encoded_refresh_token: str,
response: Optional[Response] = None,
max_age: Optional[int] = None
) -> None:
"""
Configures the response to set refresh token in a cookie.
this will also set the CSRF double submit values in a separate cookie
:param encoded_refresh_token: The encoded refresh token to set in the cookies
:param response: The FastAPI response object to set the refresh cookies in
:param max_age: The max age of the cookie value should be the number of seconds (integer)
"""
if not self.jwt_in_cookies:
raise RuntimeWarning(
"set_refresh_cookies() called without 'authjwt_token_location' configured to use cookies"
)
if max_age and not isinstance(max_age,int):
raise TypeError("max_age must be a integer")
if response and not isinstance(response,Response):
raise TypeError("The response must be an object response FastAPI")
response = response or self._response
# Set the refresh JWT in the cookie
response.set_cookie(
self._refresh_cookie_key,
encoded_refresh_token,
max_age=max_age or self._cookie_max_age,
path=self._refresh_cookie_path,
domain=self._cookie_domain,
secure=self._cookie_secure,
httponly=True,
samesite=self._cookie_samesite
)
# If enabled, set the csrf double submit refresh cookie
if self._cookie_csrf_protect:
response.set_cookie(
self._refresh_csrf_cookie_key,
self._get_csrf_token(encoded_refresh_token),
max_age=max_age or self._cookie_max_age,
path=self._refresh_csrf_cookie_path,
domain=self._cookie_domain,
secure=self._cookie_secure,
httponly=False,
samesite=self._cookie_samesite
)
def unset_jwt_cookies(self,response: Optional[Response] = None) -> None:
"""
Unset (delete) all jwt stored in a cookie
:param response: The FastAPI response object to delete the JWT cookies in.
"""
self.unset_access_cookies(response)
self.unset_refresh_cookies(response)
def unset_access_cookies(self,response: Optional[Response] = None) -> None:
"""
Remove access token and access CSRF double submit from the response cookies
:param response: The FastAPI response object to delete the access cookies in.
"""
if not self.jwt_in_cookies:
raise RuntimeWarning(
"unset_access_cookies() called without 'authjwt_token_location' configured to use cookies"
)
if response and not isinstance(response,Response):
raise TypeError("The response must be an object response FastAPI")
response = response or self._response
response.delete_cookie(
self._access_cookie_key,
path=self._access_cookie_path,
domain=self._cookie_domain
)
if self._cookie_csrf_protect:
response.delete_cookie(
self._access_csrf_cookie_key,
path=self._access_csrf_cookie_path,
domain=self._cookie_domain
)
def unset_refresh_cookies(self,response: Optional[Response] = None) -> None:
"""
Remove refresh token and refresh CSRF double submit from the response cookies
:param response: The FastAPI response object to delete the refresh cookies in.
"""
if not self.jwt_in_cookies:
raise RuntimeWarning(
"unset_refresh_cookies() called without 'authjwt_token_location' configured to use cookies"
)
if response and not isinstance(response,Response):
raise TypeError("The response must be an object response FastAPI")
response = response or self._response
response.delete_cookie(
self._refresh_cookie_key,
path=self._refresh_cookie_path,
domain=self._cookie_domain
)
if self._cookie_csrf_protect:
response.delete_cookie(
self._refresh_csrf_cookie_key,
path=self._refresh_csrf_cookie_path,
domain=self._cookie_domain
)
def _verify_and_get_jwt_optional_in_cookies(
self,
request: Union[Request,WebSocket],
csrf_token: Optional[str] = None,
) -> "AuthJWT":
"""
Optionally check if cookies have a valid access token. if an access token present in
cookies, self._token will set. raises exception error when an access token is invalid
or doesn't match with CSRF token double submit
:param request: for identity get cookies from HTTP or WebSocket
:param csrf_token: the CSRF double submit token
"""
if not isinstance(request,(Request,WebSocket)):
raise TypeError("request must be an instance of 'Request' or 'WebSocket'")
cookie_key = self._access_cookie_key
cookie = request.cookies.get(cookie_key)
if not isinstance(request, WebSocket):
csrf_token = request.headers.get(self._access_csrf_header_name)
if cookie and self._cookie_csrf_protect and not csrf_token:
if isinstance(request, WebSocket) or request.method in self._csrf_methods:
raise CSRFError(status_code=401,message="Missing CSRF Token")
# set token from cookie and verify jwt
self._token = cookie
self._verify_jwt_optional_in_request(self._token)
decoded_token = self.get_raw_jwt()
if decoded_token and self._cookie_csrf_protect and csrf_token:
if isinstance(request, WebSocket) or request.method in self._csrf_methods:
if 'csrf' not in decoded_token:
raise JWTDecodeError(status_code=422,message="Missing claim: csrf")
if not hmac.compare_digest(csrf_token,decoded_token['csrf']):
raise CSRFError(status_code=401,message="CSRF double submit tokens do not match")
def _verify_and_get_jwt_in_cookies(
self,
type_token: str,
request: Union[Request,WebSocket],
csrf_token: Optional[str] = None,
fresh: Optional[bool] = False,
) -> "AuthJWT":
"""
Check if cookies have a valid access or refresh token. if an token present in
cookies, self._token will set. raises exception error when an access or refresh token
is invalid or doesn't match with CSRF token double submit
:param type_token: indicate token is access or refresh token
:param request: for identity get cookies from HTTP or WebSocket
:param csrf_token: the CSRF double submit token
:param fresh: check freshness token if True
"""
if type_token not in ['access','refresh']:
raise ValueError("type_token must be between 'access' or 'refresh'")
if not isinstance(request,(Request,WebSocket)):
raise TypeError("request must be an instance of 'Request' or 'WebSocket'")
if type_token == 'access':
cookie_key = self._access_cookie_key
cookie = request.cookies.get(cookie_key)
if not isinstance(request, WebSocket):
csrf_token = request.headers.get(self._access_csrf_header_name)
if type_token == 'refresh':
cookie_key = self._refresh_cookie_key
cookie = request.cookies.get(cookie_key)
if not isinstance(request, WebSocket):
csrf_token = request.headers.get(self._refresh_csrf_header_name)
if not cookie:
raise MissingTokenError(status_code=401,message="Missing cookie {}".format(cookie_key))
if self._cookie_csrf_protect and not csrf_token:
if isinstance(request, WebSocket) or request.method in self._csrf_methods:
raise CSRFError(status_code=401,message="Missing CSRF Token")
# set token from cookie and verify jwt
self._token = cookie
self._verify_jwt_in_request(self._token,type_token,'cookies',fresh)
decoded_token = self.get_raw_jwt()
if self._cookie_csrf_protect and csrf_token:
if isinstance(request, WebSocket) or request.method in self._csrf_methods:
if 'csrf' not in decoded_token:
raise JWTDecodeError(status_code=422,message="Missing claim: csrf")
if not hmac.compare_digest(csrf_token,decoded_token['csrf']):
raise CSRFError(status_code=401,message="CSRF double submit tokens do not match")
def _verify_jwt_optional_in_request(self,token: str) -> None:
"""
Optionally check if this request has a valid access token
:param token: The encoded JWT
"""
if token: self._verifying_token(token)
if token and self.get_raw_jwt(token)['type'] != 'access':
raise AccessTokenRequired(status_code=422,message="Only access tokens are allowed")
def _verify_jwt_in_request(
self,
token: str,
type_token: str,
token_from: str,
fresh: Optional[bool] = False
) -> None:
"""
Ensure that the requester has a valid token. this also check the freshness of the access token
:param token: The encoded JWT
:param type_token: indicate token is access or refresh token
:param token_from: indicate token from headers cookies, websocket
:param fresh: check freshness token if True
"""
if type_token not in ['access','refresh']:
raise ValueError("type_token must be between 'access' or 'refresh'")
if token_from not in ['headers','cookies','websocket']:
raise ValueError("token_from must be between 'headers', 'cookies', 'websocket'")
if not token:
if token_from == 'headers':
raise MissingTokenError(status_code=401,message="Missing {} Header".format(self._header_name))
if token_from == 'websocket':
raise MissingTokenError(status_code=1008,message="Missing {} token from Query or Path".format(type_token))
# verify jwt
issuer = self._decode_issuer if type_token == 'access' else None
self._verifying_token(token,issuer)
if self.get_raw_jwt(token)['type'] != type_token:
msg = "Only {} tokens are allowed".format(type_token)
if type_token == 'access':
raise AccessTokenRequired(status_code=422,message=msg)
if type_token == 'refresh':
raise RefreshTokenRequired(status_code=422,message=msg)
if fresh and not self.get_raw_jwt(token)['fresh']:
raise FreshTokenRequired(status_code=401,message="Fresh token required")
def _verifying_token(self,encoded_token: str, issuer: Optional[str] = None) -> None:
"""
Verified token and check if token is revoked
:param encoded_token: token hash
:param issuer: expected issuer in the JWT
"""
raw_token = self._verified_token(encoded_token,issuer)
if raw_token['type'] in self._denylist_token_checks:
self._check_token_is_revoked(raw_token)
def _verified_token(self,encoded_token: str, issuer: Optional[str] = None) -> Dict[str,Union[str,int,bool]]:
"""
Verified token and catch all error from jwt package and return decode token
:param encoded_token: token hash
:param issuer: expected issuer in the JWT
:return: raw data from the hash token in the form of a dictionary
"""
algorithms = self._decode_algorithms or [self._algorithm]
try:
unverified_headers = self.get_unverified_jwt_headers(encoded_token)
except Exception as err:
raise InvalidHeaderError(status_code=422,message=str(err))
try:
secret_key = self._get_secret_key(unverified_headers['alg'],"decode")
except Exception:
raise
try:
return jwt.decode(
encoded_token,
secret_key,
issuer=issuer,
audience=self._decode_audience,
leeway=self._decode_leeway,
algorithms=algorithms
)
except Exception as err:
raise JWTDecodeError(status_code=422,message=str(err))
def jwt_required(
self,
auth_from: str = "request",
token: Optional[str] = None,
websocket: Optional[WebSocket] = None,
csrf_token: Optional[str] = None,
) -> None:
"""
Only access token can access this function
:param auth_from: for identity get token from HTTP or WebSocket
:param token: the encoded JWT, it's required if the protected endpoint use WebSocket to
authorization and get token from Query Url or Path
:param websocket: an instance of WebSocket, it's required if protected endpoint use a cookie to authorization
:param csrf_token: the CSRF double submit token. since WebSocket cannot add specifying additional headers
its must be passing csrf_token manually and can achieve by Query Url or Path
"""
if auth_from == "websocket":
if websocket: self._verify_and_get_jwt_in_cookies('access',websocket,csrf_token)
else: self._verify_jwt_in_request(token,'access','websocket')
if auth_from == "request":
if len(self._token_location) == 2:
if self._token and self.jwt_in_headers:
self._verify_jwt_in_request(self._token,'access','headers')
if not self._token and self.jwt_in_cookies:
self._verify_and_get_jwt_in_cookies('access',self._request)
else:
if self.jwt_in_headers:
self._verify_jwt_in_request(self._token,'access','headers')
if self.jwt_in_cookies:
self._verify_and_get_jwt_in_cookies('access',self._request)
def jwt_optional(
self,
auth_from: str = "request",
token: Optional[str] = None,
websocket: Optional[WebSocket] = None,
csrf_token: Optional[str] = None,
) -> None:
"""
If an access token in present in the request you can get data from get_raw_jwt() or get_jwt_subject(),
If no access token is present in the request, this endpoint will still be called, but
get_raw_jwt() or get_jwt_subject() will return None
:param auth_from: for identity get token from HTTP or WebSocket
:param token: the encoded JWT, it's required if the protected endpoint use WebSocket to
authorization and get token from Query Url or Path
:param websocket: an instance of WebSocket, it's required if protected endpoint use a cookie to authorization
:param csrf_token: the CSRF double submit token. since WebSocket cannot add specifying additional headers
its must be passing csrf_token manually and can achieve by Query Url or Path
"""
if auth_from == "websocket":
if websocket: self._verify_and_get_jwt_optional_in_cookies(websocket,csrf_token)
else: self._verify_jwt_optional_in_request(token)
if auth_from == "request":
if len(self._token_location) == 2:
if self._token and self.jwt_in_headers:
self._verify_jwt_optional_in_request(self._token)
if not self._token and self.jwt_in_cookies:
self._verify_and_get_jwt_optional_in_cookies(self._request)
else:
if self.jwt_in_headers:
self._verify_jwt_optional_in_request(self._token)
if self.jwt_in_cookies:
self._verify_and_get_jwt_optional_in_cookies(self._request)
def jwt_refresh_token_required(
self,
auth_from: str = "request",
token: Optional[str] = None,
websocket: Optional[WebSocket] = None,
csrf_token: Optional[str] = None,
) -> None:
"""
This function will ensure that the requester has a valid refresh token
:param auth_from: for identity get token from HTTP or WebSocket
:param token: the encoded JWT, it's required if the protected endpoint use WebSocket to
authorization and get token from Query Url or Path
:param websocket: an instance of WebSocket, it's required if protected endpoint use a cookie to authorization
:param csrf_token: the CSRF double submit token. since WebSocket cannot add specifying additional headers
its must be passing csrf_token manually and can achieve by Query Url or Path
"""
if auth_from == "websocket":
if websocket: self._verify_and_get_jwt_in_cookies('refresh',websocket,csrf_token)
else: self._verify_jwt_in_request(token,'refresh','websocket')
if auth_from == "request":
if len(self._token_location) == 2:
if self._token and self.jwt_in_headers:
self._verify_jwt_in_request(self._token,'refresh','headers')
if not self._token and self.jwt_in_cookies:
self._verify_and_get_jwt_in_cookies('refresh',self._request)
else:
if self.jwt_in_headers:
self._verify_jwt_in_request(self._token,'refresh','headers')
if self.jwt_in_cookies:
self._verify_and_get_jwt_in_cookies('refresh',self._request)
def fresh_jwt_required(
self,
auth_from: str = "request",
token: Optional[str] = None,
websocket: Optional[WebSocket] = None,
csrf_token: Optional[str] = None,
) -> None:
"""
This function will ensure that the requester has a valid access token and fresh token
:param auth_from: for identity get token from HTTP or WebSocket
:param token: the encoded JWT, it's required if the protected endpoint use WebSocket to
authorization and get token from Query Url or Path
:param websocket: an instance of WebSocket, it's required if protected endpoint use a cookie to authorization
:param csrf_token: the CSRF double submit token. since WebSocket cannot add specifying additional headers
its must be passing csrf_token manually and can achieve by Query Url or Path
"""
if auth_from == "websocket":
if websocket: self._verify_and_get_jwt_in_cookies('access',websocket,csrf_token,True)
else: self._verify_jwt_in_request(token,'access','websocket',True)
if auth_from == "request":
if len(self._token_location) == 2:
if self._token and self.jwt_in_headers:
self._verify_jwt_in_request(self._token,'access','headers',True)
if not self._token and self.jwt_in_cookies:
self._verify_and_get_jwt_in_cookies('access',self._request,fresh=True)
else:
if self.jwt_in_headers:
self._verify_jwt_in_request(self._token,'access','headers',True)
if self.jwt_in_cookies:
self._verify_and_get_jwt_in_cookies('access',self._request,fresh=True)
def get_raw_jwt(self,encoded_token: Optional[str] = None) -> Optional[Dict[str,Union[str,int,bool]]]:
"""
this will return the python dictionary which has all of the claims of the JWT that is accessing the endpoint.
If no JWT is currently present, return None instead
:param encoded_token: The encoded JWT from parameter
:return: claims of JWT
"""
token = encoded_token or self._token
if token:
return self._verified_token(token)
return None
def get_jti(self,encoded_token: str) -> str:
"""
Returns the JTI (unique identifier) of an encoded JWT
:param encoded_token: The encoded JWT from parameter
:return: string of JTI
"""
return self._verified_token(encoded_token)['jti']
def get_jwt_subject(self) -> Optional[Union[str,int]]:
"""
this will return the subject of the JWT that is accessing this endpoint.
If no JWT is present, `None` is returned instead.
:return: sub of JWT
"""
if self._token:
return self._verified_token(self._token)['sub']
return None
def get_unverified_jwt_headers(self,encoded_token: Optional[str] = None) -> dict:
"""
Returns the Headers of an encoded JWT without verifying the actual signature of JWT
:param encoded_token: The encoded JWT to get the Header from
:return: JWT header parameters as a dictionary
"""
encoded_token = encoded_token or self._token
return jwt.get_unverified_header(encoded_token)
+85
View File
@@ -0,0 +1,85 @@
from datetime import timedelta
from typing import Optional, Union, List
from pydantic import (
BaseModel,
validator,
StrictBool,
StrictInt,
StrictStr
)
class LoadConfig(BaseModel):
authjwt_token_location: Optional[List[StrictStr]] = {'headers'}
authjwt_secret_key: Optional[StrictStr] = None
authjwt_public_key: Optional[StrictStr] = None
authjwt_private_key: Optional[StrictStr] = None
authjwt_algorithm: Optional[StrictStr] = "HS256"
authjwt_decode_algorithms: Optional[List[StrictStr]] = None
authjwt_decode_leeway: Optional[Union[StrictInt,timedelta]] = 0
authjwt_encode_issuer: Optional[StrictStr] = None
authjwt_decode_issuer: Optional[StrictStr] = None
authjwt_decode_audience: Optional[Union[StrictStr,List[StrictStr]]] = None
authjwt_denylist_enabled: Optional[StrictBool] = False
authjwt_denylist_token_checks: Optional[List[StrictStr]] = {'access','refresh'}
authjwt_header_name: Optional[StrictStr] = "Authorization"
authjwt_header_type: Optional[StrictStr] = "Bearer"
authjwt_access_token_expires: Optional[Union[StrictBool,StrictInt,timedelta]] = timedelta(minutes=15)
authjwt_refresh_token_expires: Optional[Union[StrictBool,StrictInt,timedelta]] = timedelta(days=30)
# option for create cookies
authjwt_access_cookie_key: Optional[StrictStr] = "access_token_cookie"
authjwt_refresh_cookie_key: Optional[StrictStr] = "refresh_token_cookie"
authjwt_access_cookie_path: Optional[StrictStr] = "/"
authjwt_refresh_cookie_path: Optional[StrictStr] = "/"
authjwt_cookie_max_age: Optional[StrictInt] = None
authjwt_cookie_domain: Optional[StrictStr] = None
authjwt_cookie_secure: Optional[StrictBool] = False
authjwt_cookie_samesite: Optional[StrictStr] = None
# option for double submit csrf protection
authjwt_cookie_csrf_protect: Optional[StrictBool] = True
authjwt_access_csrf_cookie_key: Optional[StrictStr] = "csrf_access_token"
authjwt_refresh_csrf_cookie_key: Optional[StrictStr] = "csrf_refresh_token"
authjwt_access_csrf_cookie_path: Optional[StrictStr] = "/"
authjwt_refresh_csrf_cookie_path: Optional[StrictStr] = "/"
authjwt_access_csrf_header_name: Optional[StrictStr] = "X-CSRF-Token"
authjwt_refresh_csrf_header_name: Optional[StrictStr] = "X-CSRF-Token"
authjwt_csrf_methods: Optional[List[StrictStr]] = {'POST','PUT','PATCH','DELETE'}
@validator('authjwt_access_token_expires')
def validate_access_token_expires(cls, v):
if v is True:
raise ValueError("The 'authjwt_access_token_expires' only accept value False (bool)")
return v
@validator('authjwt_refresh_token_expires')
def validate_refresh_token_expires(cls, v):
if v is True:
raise ValueError("The 'authjwt_refresh_token_expires' only accept value False (bool)")
return v
@validator('authjwt_denylist_token_checks', each_item=True)
def validate_denylist_token_checks(cls, v):
if v not in ['access','refresh']:
raise ValueError("The 'authjwt_denylist_token_checks' must be between 'access' or 'refresh'")
return v
@validator('authjwt_token_location', each_item=True)
def validate_token_location(cls, v):
if v not in ['headers','cookies']:
raise ValueError("The 'authjwt_token_location' must be between 'headers' or 'cookies'")
return v
@validator('authjwt_cookie_samesite')
def validate_cookie_samesite(cls, v):
if v not in ['strict','lax','none']:
raise ValueError("The 'authjwt_cookie_samesite' must be between 'strict', 'lax', 'none'")
return v
@validator('authjwt_csrf_methods', each_item=True)
def validate_csrf_methods(cls, v):
if v.upper() not in {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"}:
raise ValueError("The 'authjwt_csrf_methods' must be between http request methods")
return v.upper()
class Config:
str_min_length = 1
str_strip_whitespace = True
@@ -0,0 +1,72 @@
class AuthJWTException(Exception):
"""
Base except which all fastapi_jwt_auth errors extend
"""
pass
class InvalidHeaderError(AuthJWTException):
"""
An error getting jwt in header or jwt header information from a request
"""
def __init__(self,status_code: int, message: str):
self.status_code = status_code
self.message = message
class JWTDecodeError(AuthJWTException):
"""
An error decoding a JWT
"""
def __init__(self,status_code: int, message: str):
self.status_code = status_code
self.message = message
class CSRFError(AuthJWTException):
"""
An error with CSRF protection
"""
def __init__(self,status_code: int, message: str):
self.status_code = status_code
self.message = message
class MissingTokenError(AuthJWTException):
"""
Error raised when token not found
"""
def __init__(self,status_code: int, message: str):
self.status_code = status_code
self.message = message
class RevokedTokenError(AuthJWTException):
"""
Error raised when a revoked token attempt to access a protected endpoint
"""
def __init__(self,status_code: int, message: str):
self.status_code = status_code
self.message = message
class AccessTokenRequired(AuthJWTException):
"""
Error raised when a valid, non-access JWT attempt to access an endpoint
protected by jwt_required, jwt_optional, fresh_jwt_required
"""
def __init__(self,status_code: int, message: str):
self.status_code = status_code
self.message = message
class RefreshTokenRequired(AuthJWTException):
"""
Error raised when a valid, non-refresh JWT attempt to access an endpoint
protected by jwt_refresh_token_required
"""
def __init__(self,status_code: int, message: str):
self.status_code = status_code
self.message = message
class FreshTokenRequired(AuthJWTException):
"""
Error raised when a valid, non-fresh JWT attempt to access an endpoint
protected by fresh_jwt_required
"""
def __init__(self,status_code: int, message: str):
self.status_code = status_code
self.message = message
+18 -11
View File
@@ -47,28 +47,28 @@ beautifulsoup4 = "^4.12.2"
google-search-results = "^2.4.1"
google-api-python-client = "^2.79.0"
typer = "^0.9.0"
gunicorn = "^20.1.0"
gunicorn = "23.0.0"
pandas = "^2.0.0"
chromadb = "^0.3.21"
rich = "^13.4.2"
networkx = "^3.1"
unstructured = "^0.17.2"
pypdf = "*" # 和llama-index包冲突
pypdf = "*"
lxml = "^4.9.2"
pysrt = "^1.1.2"
fake-useragent = "^1.1.3"
docstring-parser = "^0.15"
psycopg2-binary = "^2.9.6"
pyarrow = "^12.0.0"
pyarrow = "14.0.1"
tiktoken = "*"
wikipedia = "^1.4.0"
qdrant-client = "^1.3.0"
websockets = "^10.3"
weaviate-client = "^3.21.0"
cohere = "^4.11.0"
python-multipart = "^0.0.6"
python-multipart = "0.0.18"
sqlmodel = "^0.0.14"
pymysql = ">=1.0,<2.0"
pymysql = "1.1.1"
pymilvus = "2.5.10"
elasticsearch = "^8.9.0"
orjson = "^3.9.1"
@@ -77,7 +77,7 @@ cachetools = "^5.3.1"
types-cachetools = "^5.3.0.5"
appdirs = "^1.4.4"
supabase = "^2.4.0"
certifi = "^2023.5.7"
certifi = ">=2024.7.4"
psycopg = "^3.1.9"
psycopg-binary = "^3.1.9"
emoji = "^2.10.1"
@@ -86,17 +86,15 @@ scipy = "*"
arxiv = "2.1.0"
matplotlib = "3.8.4"
cchardet = "^2.1.7"
llama-index = "0.9.48"
tenacity = "<8.4.0"
bisheng-ragas = "^1.0.2"
qianfan = "^0.4.4"
dashscope = "^1.20.3"
blobfile = "^3.0.0"
mcp = "^1.6.0"
mcp = "1.10.0"
numpy = "^1.26.2"
pyjwt = "^1.7.1"
pyjwt = "2.4.0"
tencentcloud-sdk-python = "^3.0.1373"
fastapi-jwt-auth = "^0.5.0"
langchain-openai = "^0.3.16"
markdownify = "^1.1.0"
readability = "^0.3.2"
@@ -111,11 +109,20 @@ tabulate = "^0.9.0"
zhipuai = "^1.0.7"
websocket-client = "^1.8.0"
opencv-python = "4.5.5.64"
Pillow = "9.5.0"
Pillow = "10.3.0"
filetype = "1.2.0"
aiomysql = "^0.2.0"
sse-starlette = "^2.3.6"
transformers = "^4.53.0"
llama-index = "0.13.0"
llama-index-core = ">=0.13.0,<0.14"
llama-index-cli = ">=0.5.0,<0.6"
llama-index-llms-openai = ">=0.5,<0.6"
llama-index-embeddings-openai = ">=0.5,<0.6"
llama-index-readers-file = ">=0.5,<0.6"
llama-index-readers-llama-parse = "0.5.0"
llama-index-indices-managed-llama-cloud = "0.9.0"
llama-cloud = "0.1.35"
[tool.poetry.dev-dependencies]
black = "^23.1.0"
@@ -5,7 +5,7 @@ import { saveSop, startLinsight } from '~/api/linsight';
import { useLinsightManager, useLinsightSessionManager } from '~/hooks/useLinsightManager';
import { Button, Textarea } from '../ui';
import CopyButton from './components/CopyButton';
import { LoadingDots } from './components/sopLoading';
import { LoadingDots } from './components/SopLoading';
import SopMarkdown from './SopMarkdown';
export const enum SopStatus {
@@ -6,7 +6,7 @@ import { useGetLinsightToolList, useGetOrgToolList, useGetPersonalToolList } fro
import { useGenerateSop, useLinsightManager } from '~/hooks/useLinsightManager';
import { formatTime } from '~/utils';
import { SopCase } from './case';
import { LoadingBox } from './components/sopLoading';
import { LoadingBox } from './components/SopLoading';
import { Header } from './Header';
import { SOPEditor, SopStatus } from './SOPEditor';
import { TaskFlow } from './TaskFlow';