From 0cb02a1c3438fee217a720aea0d460b0f4893c0d Mon Sep 17 00:00:00 2001 From: yaojin Date: Wed, 3 Jan 2024 12:37:34 +0800 Subject: [PATCH 1/3] update host_llm stream --- .../bisheng_langchain/chat_models/host_llm.py | 240 +++++++----------- .../document_loaders/custom_kv.py | 50 ++-- .../bisheng_langchain/utils/requests.py | 5 +- src/bisheng-langchain/requirements.txt | 1 - 4 files changed, 125 insertions(+), 171 deletions(-) diff --git a/src/bisheng-langchain/bisheng_langchain/chat_models/host_llm.py b/src/bisheng-langchain/bisheng_langchain/chat_models/host_llm.py index f77b6bf1f..54c7b3630 100644 --- a/src/bisheng-langchain/bisheng_langchain/chat_models/host_llm.py +++ b/src/bisheng-langchain/bisheng_langchain/chat_models/host_llm.py @@ -7,7 +7,7 @@ import sys from typing import TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional, Tuple, Union import requests -import sseclient +from bisheng_langchain.utils.requests import Requests from langchain.callbacks.manager import AsyncCallbackManagerForLLMRun, CallbackManagerForLLMRun from langchain.chat_models.base import BaseChatModel from langchain.schema import ChatGeneration, ChatResult @@ -153,16 +153,28 @@ class BaseHostChatLLM(BaseChatModel): values['host_base_url'] = get_from_dict_or_env(values, 'host_base_url', 'HostBaseUrl') model = values['model_name'] try: - url = values['host_base_url'].split('/')[2] - config_ep = f'http://{url}/v2/models/{model}/config' - config = requests.get(url=config_ep, json={}, timeout=5).json() - policy = config.get('model_transaction_policy', {}) - values['decoupled'] = policy.get('decoupled', False) + if cls != CustomLLMChat: + url = values['host_base_url'].split('/')[2] + config_ep = f'http://{url}/v2/models/{model}/config' + config = requests.get(url=config_ep, json={}, timeout=5).json() + policy = config.get('model_transaction_policy', {}) + values['decoupled'] = policy.get('decoupled', False) + # Host class should set below code + if values['decoupled']: + values[ + 'host_base_url'] = f"{values['host_base_url']}/{values['model_name']}/generate_stream" + else: + values[ + 'host_base_url'] = f"{values['host_base_url']}/{values['model_name']}/infer" except Exception: raise Exception(f'Update Decoupled status faild for model {model}') try: - values['client'] = requests.post + if values['headers']: + headers = values['headers'] + else: + headers = {'Content-Type': 'application/json'} + values['client'] = Requests(headers=headers, request_timeout=values['request_timeout']) except AttributeError: raise ValueError('Try upgrading it with `pip install --upgrade requests`.') return values @@ -185,6 +197,7 @@ class BaseHostChatLLM(BaseChatModel): @retry_decorator def _completion_with_retry(**kwargs: Any) -> Any: + self.client.headers = self.headers messages = kwargs.get('messages') temperature = kwargs.get('temperature') top_p = kwargs.get('top_p') @@ -204,17 +217,17 @@ class BaseHostChatLLM(BaseChatModel): # print('messages:', messages) # print('functions:', kwargs.get('functions', [])) if self.verbose: - print('payload', params) - - method_name = 'infer' if not self.decoupled else 'generate' - url = f'{self.host_base_url}/{self.model_name}/{method_name}' + logger.info(f'payload={params}') try: - resp = self.client( - url=url, json=params, timeout=self.request_timeout).json() - except requests.exceptions.Timeout: - raise Exception(f'timeout in host llm infer, url=[{url}]') + resp = self.client.post(url=self.host_base_url, json=params) + if resp.text.startswith('data:'): + resp = json.loads(resp.text.replace('data:', '')) + else: + resp = resp.json() + except requests.exceptions.Timeout as exc: + raise ValueError(f'timeout in host llm infer, url=[{self.host_base_url}]') from exc except Exception as e: - raise Exception(f'exception in host llm infer: [{e}]') + raise ValueError(f'exception in host llm infer: [{e}]') from e if not resp.get('choices', []): logger.info(resp) @@ -249,63 +262,46 @@ class BaseHostChatLLM(BaseChatModel): run_manager: Optional[CallbackManagerForLLMRun] = None, **kwargs: Any, ) -> ChatResult: + '''用来处理同步请求''' message_dicts, params = self._create_message_dicts(messages, stop) params = {**params, **kwargs} response = self.completion_with_retry(messages=message_dicts, **params) return self._create_chat_result(response) - def _stream(self, **kwargs: Any) -> Any: + async def acompletion_with_retry(self, **kwargs: Any) -> Any: """Use tenacity to retry the async completion call.""" retry_decorator = _create_retry_decorator(self) @retry_decorator - def _completion_with_retry(**kwargs: Any) -> Any: - if self.streaming: - if not self.decoupled: - raise Exception('Not supported stream protocol with non decoupled model') + async def _acompletion_with_retry(**kwargs: Any) -> Any: + try: + async with self.client.apost(url=self.host_base_url, json=kwargs) as response: + if response.status != 200: + raise ValueError(f'Error: {response.status}') + async for txt in response.content.iter_any(): + if b'\n' in txt: + for txt_ in txt.split(b'\n'): + yield txt_.decode('utf-8').strip() + else: + yield txt.decode('utf-8').strip() + except requests.exceptions.Timeout as exc: + raise ValueError(f'timeout in host llm infer, url=[{self.host_base_url}]') from exc + except Exception as e: + raise ValueError(f'exception in host llm infer: [{e}]') from e - headers = {'Accept': 'text/event-stream'} - url = f'{self.host_base_url}/{self.model_name}/generate_stream' - try: - res = requests.post( - url=url, - data=json.dumps(kwargs), - headers=headers, - stream=False) - except Exception as e: - raise Exception(f'exception in host llm sse infer: [{e}]') - - res.raise_for_status() - try: - client = sseclient.SSEClient(res, timeout=self.request_timeout) - for event in client.events(): - delta_data = json.loads(event.data) - yield delta_data - except requests.exceptions.Timeout: - raise Exception(f'timeout in host llm sse infer, url=[{url}]') - except Exception as e: - raise Exception(f'exception in host llm sse infer: [{e}]') - else: - method_name = 'infer' if not self.decoupled else 'generate' - url = f'{self.host_base_url}/{self.model_name}/{method_name}' - try: - res = requests.post( - url=url, - data=json.dumps(kwargs), - stream=False, - timeout=self.request_timeout) - return res.json() - except requests.exceptions.Timeout: - raise Exception(f'timeout in host llm infer, url=[{url}]') - except Exception as e: - raise Exception(f'exception in host llm infer: [{e}]') - - if self.streaming: - for response in _completion_with_retry(**kwargs): - if response: - yield response - else: - return _completion_with_retry(**kwargs) + async for response in _acompletion_with_retry(**kwargs): + is_error = False + if response: + if response.startswith('event:error'): + is_error = True + elif response.startswith('data:'): + yield (is_error, response[len('data:'):]) + if is_error: + break + elif response.startswith('{'): + yield (is_error, response) + else: + continue async def _agenerate( self, @@ -314,9 +310,7 @@ class BaseHostChatLLM(BaseChatModel): run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, **kwargs: Any, ) -> ChatResult: - if not self.decoupled: - return self._generate(messages, stop, run_manager, **kwargs) - + """Generate chat completion with retry.""" message_dicts, params = self._create_message_dicts(messages, stop) params = {**params, **kwargs} if self.streaming: @@ -324,31 +318,41 @@ class BaseHostChatLLM(BaseChatModel): role = 'assistant' params['stream'] = True function_call: Optional[dict] = None - for stream_resp in self._stream( - messages=message_dicts, **params - ): - role = stream_resp['choices'][0]['delta'].get('role', role) - token = stream_resp['choices'][0]['delta'].get('content', '') - inner_completion += token or '' - _function_call = stream_resp['choices'][0]['delta'].get('function_call') - if _function_call: - if function_call is None: - function_call = _function_call - else: - function_call['arguments'] += _function_call['arguments'] - if run_manager: - await run_manager.on_llm_new_token(token) - message = _convert_dict_to_message( - { - 'content': inner_completion, - 'role': role, - 'function_call': function_call, - } - ) + async for is_error, stream_resp in self.acompletion_with_retry(messages=message_dicts, + **params): + output = json.loads(stream_resp) + if is_error: + logger.error(stream_resp) + raise ValueError(stream_resp) + + choices = output.get('choices') + if choices: + for choice in choices: + role = choice['delta'].get('role', role) + token = choice['delta'].get('content', '') + inner_completion += token or '' + _function_call = choice['delta'].get('function_call') + if run_manager: + await run_manager.on_llm_new_token(token) + if _function_call: + if function_call is None: + function_call = _function_call + else: + function_call['arguments'] += _function_call['arguments'] + message = _convert_dict_to_message({ + 'content': inner_completion, + 'role': role, + 'function_call': function_call, + }) return ChatResult(generations=[ChatGeneration(message=message)]) else: params['stream'] = False - response = self._stream(messages=message_dicts, **params) + response = [ + response + async for _, response in self.acompletion_with_retry(messages=message_dicts, + **params) + ] + response = json.loads(response[0]) return self._create_chat_result(response) def _create_message_dicts( @@ -373,7 +377,7 @@ class BaseHostChatLLM(BaseChatModel): gen = ChatGeneration(message=message) generations.append(gen) - llm_output = {'token_usage': response['usage'], 'model_name': self.model_name} + llm_output = {'token_usage': response.get('usage'), 'model_name': self.model_name} return ChatResult(generations=generations, llm_output=llm_output) @property @@ -525,65 +529,9 @@ class CustomLLMChat(BaseHostChatLLM): temperature: float = 0.1 top_p: float = 0.1 max_tokens: int = 4096 + host_base_url: str @property def _llm_type(self) -> str: """Return type of chat model.""" return 'custom_llm_chat' - - def completion_with_retry(self, **kwargs: Any) -> Any: - retry_decorator = _create_retry_decorator(self) - - @retry_decorator - def _completion_with_retry(**kwargs: Any) -> Any: - messages = kwargs.get('messages') - temperature = kwargs.get('temperature') - top_p = kwargs.get('top_p') - max_tokens = kwargs.get('max_tokens') - do_sample = kwargs.get('do_sample') - params = { - 'messages': messages, - 'model': self.model_name, - 'top_p': top_p, - 'temperature': temperature, - 'max_tokens': max_tokens, - 'do_sample': do_sample - } - - if self.verbose: - print('payload', params) - - resp = None - try: - resp = self.client( - url=self.host_base_url, - json=params, - timeout=self.request_timeout).json() - except requests.exceptions.Timeout: - raise Exception( - f'timeout in custom host llm infer, url=[{self.host_base_url}]') - except Exception as e: - raise Exception(f'exception in custom host llm infer: [{e}]') - - return resp - - return _completion_with_retry(**kwargs) - - def _create_chat_result(self, response: Mapping[str, Any]) -> ChatResult: - generations = [] - for res in response['choices']: - message = _convert_dict_to_message(res['message']) - gen = ChatGeneration(message=message) - generations.append(gen) - - llm_output = {'token_usage': response.get('usage', {}), 'model_name': self.model_name} - return ChatResult(generations=generations, llm_output=llm_output) - - async def _agenerate( - self, - messages: List[BaseMessage], - stop: Optional[List[str]] = None, - run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, - **kwargs: Any, - ) -> ChatResult: - return self._generate(messages, stop, run_manager, **kwargs) diff --git a/src/bisheng-langchain/bisheng_langchain/document_loaders/custom_kv.py b/src/bisheng-langchain/bisheng_langchain/document_loaders/custom_kv.py index 1a191eb84..adcbc7f86 100644 --- a/src/bisheng-langchain/bisheng_langchain/document_loaders/custom_kv.py +++ b/src/bisheng-langchain/bisheng_langchain/document_loaders/custom_kv.py @@ -21,6 +21,7 @@ from PIL import Image logger = logging.getLogger(__name__) + def convert_base64(image): image_binary = cv2.imencode('.jpg', image)[1].tobytes() x = base64.b64encode(image_binary) @@ -52,7 +53,9 @@ def transpdf2png(pdf_file): class CustomKVLoader(BaseLoader): """Extract key-value from pdf or image. """ - def __init__(self, file_path:str, + + def __init__(self, + file_path: str, elm_api_base_url: str, elm_api_key: str, schemas: str, @@ -67,8 +70,7 @@ class CustomKVLoader(BaseLoader): self.task_type = task_type self.schemas = set(schemas.split('|')) self.headers = {'Authorization': f'Bearer {elm_api_key}'} - self.requests = Requests(headers=self.headers, - request_timeout=request_timeout) + self.requests = Requests(headers=self.headers, request_timeout=request_timeout) if '~' in self.file_path: self.file_path = os.path.expanduser(self.file_path) @@ -77,14 +79,12 @@ class CustomKVLoader(BaseLoader): r = self.requests.get(self.file_path) if r.status_code != 200: - raise ValueError( - 'Check the url of your file; returned status code %s' - % r.status_code - ) + raise ValueError('Check the url of your file; returned status code %s' % + r.status_code) self.temp_dir = tempfile.TemporaryDirectory() - temp_file = Path(self.temp_dir.name) / unquote(urlparse(self.file_path - ).path.split('/')[-1]) + temp_file = Path(self.temp_dir.name) / unquote( + urlparse(self.file_path).path.split('/')[-1]) with open(temp_file, mode='wb') as f: f.write(r.content) self.file_path = str(temp_file) @@ -108,17 +108,18 @@ class CustomKVLoader(BaseLoader): # else: # raise ValueError(f'file type {file_type} is not support.') - file = {'file': open(self.file_path, 'rb')} - result = {} - if self.task_type == 'extraction-job': - url = self.elm_api_base_url + '/task' - # 创建task - body = {'scene_id': self.elem_server_id} - elif self.task_type == 'logic-job': - url = self.elm_api_base_url + '/logic-job' - body = {'logic_service_id': self.elem_server_id} + with open(self.file_path, 'rb') as file: + file = {'file': open(self.file_path, 'rb')} + result = {} + if self.task_type == 'extraction-job': + url = self.elm_api_base_url + '/task' + # 创建task + body = {'scene_id': self.elem_server_id} + elif self.task_type == 'logic-job': + url = self.elm_api_base_url + '/logic-job' + body = {'logic_service_id': self.elem_server_id} - resp = self.requests.post(url=url, json={}, data=body, files=file) + resp = self.requests.post(url=url, json={}, data=body, files=file) if resp.status_code == 200: task_id = resp.json().get('data').get('task_id') if not task_id: @@ -141,7 +142,7 @@ class CustomKVLoader(BaseLoader): # get result job_id = 'job_id' if self.task_type == 'logic-job' else 'task_id' match = re.match(r'^(?:https?:\/\/)?(?:www\.)?([^\/\n]+)', self.elm_api_base_url) - detail_url = quote_plus(match.group()+f'/logic-job-detail/{task_id}') + detail_url = quote_plus(match.group() + f'/logic-job-detail/{task_id}') result_url = url + f'/result?{job_id}={task_id}&detail_url={detail_url}' result = self.requests.get(result_url).json() # only for independent key @@ -153,10 +154,13 @@ class CustomKVLoader(BaseLoader): independent = result.get('result').get('independent_list') for element in independent: if element.get('element_name') in self.schemas: - document_result[element.get('element_name')] = [el.get('words') - for el in element.get('entity_list')] + document_result[element.get('element_name')] = [ + el.get('words') for el in element.get('entity_list') + ] except Exception as e: - logger.error(f'task_result_error scene_id={self.elem_server_id} res={result} except={str(e)}') + logger.error( + f'task_result_error scene_id={self.elem_server_id} res={result} except={str(e)}' + ) raise Exception('custom_kv parse_error') else: logger.error(f'custom_kv=create_task resp={resp.text}') diff --git a/src/bisheng-langchain/bisheng_langchain/utils/requests.py b/src/bisheng-langchain/bisheng_langchain/utils/requests.py index 806d8abd0..a826bdb95 100644 --- a/src/bisheng-langchain/bisheng_langchain/utils/requests.py +++ b/src/bisheng-langchain/bisheng_langchain/utils/requests.py @@ -4,6 +4,7 @@ from typing import Any, AsyncGenerator, Dict, Optional, Tuple, Union import aiohttp import requests +from loguru import logger from pydantic import BaseModel, Extra @@ -81,7 +82,9 @@ class Requests(BaseModel): else: timeout = aiohttp.ClientTimeout(total=self.request_timeout) async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.request(method, url, headers=self.headers, **kwargs) as response: + logger.info(f'aio_http url={url}') + async with session.request(method, url, headers=self.headers, + **kwargs) as response: yield response else: async with self.aiosession.request(method, diff --git a/src/bisheng-langchain/requirements.txt b/src/bisheng-langchain/requirements.txt index 4e96faf3c..7179f2da9 100644 --- a/src/bisheng-langchain/requirements.txt +++ b/src/bisheng-langchain/requirements.txt @@ -5,4 +5,3 @@ elasticsearch opencv-python==4.5.5.64 Pillow==9.5.0 bisheng-pyautogen -sseclient-py From 78aa7a8845a3166282da3b05270f0f588a14d113 Mon Sep 17 00:00:00 2001 From: yaojin Date: Wed, 3 Jan 2024 12:37:55 +0800 Subject: [PATCH 2/3] update host_llm stream --- src/backend/pyproject.toml | 2 +- src/bisheng-langchain/version.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index df8e2f332..647104fa8 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -18,7 +18,7 @@ include = ["./bisheng/*", "bisheng/**/*"] bisheng = "bisheng.__main__:main" [tool.poetry.dependencies] -bisheng_langchain = "0.2.1" +bisheng_langchain = "0.2.1.2" bisheng_pyautogen = "0.1.18" minio = "^7.2.0" fastapi_jwt_auth = "^0.5.0" diff --git a/src/bisheng-langchain/version.txt b/src/bisheng-langchain/version.txt index 22c08f727..7dad716c0 100644 --- a/src/bisheng-langchain/version.txt +++ b/src/bisheng-langchain/version.txt @@ -1 +1 @@ -v0.2.1 +v0.2.1.2 From 08d9c943a72220d4e4c6dcfa202e28b864829545 Mon Sep 17 00:00:00 2001 From: yaojin Date: Fri, 12 Jan 2024 17:23:08 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=BA=AF=E6=BA=90=E5=88=A4=E6=96=ADbbox=20?= =?UTF-8?q?bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/bisheng/api/v1/chat.py | 4 ++-- src/backend/bisheng/api/v2/chat.py | 1 - src/backend/bisheng/chat/utils.py | 4 ++-- src/backend/bisheng/database/base.py | 2 +- src/backend/bisheng/utils/logger.py | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/backend/bisheng/api/v1/chat.py b/src/backend/bisheng/api/v1/chat.py index 0dad0c109..68a1614dc 100644 --- a/src/backend/bisheng/api/v1/chat.py +++ b/src/backend/bisheng/api/v1/chat.py @@ -1,5 +1,5 @@ import json -from typing import List, Optional, Union +from typing import List, Optional from uuid import UUID from bisheng.api.utils import build_flow, build_input_keys_response @@ -115,10 +115,10 @@ def get_chatlist_list(*, session: Session = Depends(get_session), Authorize: Aut @router.websocket('/chat/{flow_id}') async def chat( + *, flow_id: str, websocket: WebSocket, chat_id: Optional[str] = None, - session_id: Union[None, str] = None, # noqa: F821 Authorize: AuthJWT = Depends(), ): """Websocket endpoint for chat.""" diff --git a/src/backend/bisheng/api/v2/chat.py b/src/backend/bisheng/api/v2/chat.py index 455257113..e6adab191 100644 --- a/src/backend/bisheng/api/v2/chat.py +++ b/src/backend/bisheng/api/v2/chat.py @@ -22,7 +22,6 @@ expire = 600 # reids 60s 过期 async def union_websocket(flow_id: str, websocket: WebSocket, chat_id: Optional[str] = None, - type: Optional[str] = None, knowledge_id: Optional[int] = None): """Websocket endpoint forF chat.""" if chat_id: diff --git a/src/backend/bisheng/chat/utils.py b/src/backend/bisheng/chat/utils.py index adc72ef77..c4766514a 100644 --- a/src/backend/bisheng/chat/utils.py +++ b/src/backend/bisheng/chat/utils.py @@ -111,8 +111,8 @@ async def judge_source(result, source_document, chat_id, extra: Dict): if source: for doc in source_document: # 确保每个chunk 都可溯源 - if 'bbox' not in doc.metadata or not doc.metadata['bbox'] or not doc.metadata['bbox'][ - 'chunk_bboxes']: + if 'bbox' not in doc.metadata or not doc.metadata['bbox'] or not json.loads( + doc.metadata['bbox'])['chunk_bboxes']: source = 0 break return source, result diff --git a/src/backend/bisheng/database/base.py b/src/backend/bisheng/database/base.py index 95cc92143..2c01d4362 100644 --- a/src/backend/bisheng/database/base.py +++ b/src/backend/bisheng/database/base.py @@ -31,7 +31,7 @@ def init_default_data(): session.commit() user = session.exec(select(User).limit(1)).all() - if not user: + if not user and settings.admin: md5 = hashlib.md5() md5.update(settings.admin.get('password').encode('utf-8')) user = User( diff --git a/src/backend/bisheng/utils/logger.py b/src/backend/bisheng/utils/logger.py index 666464837..4894c1df1 100644 --- a/src/backend/bisheng/utils/logger.py +++ b/src/backend/bisheng/utils/logger.py @@ -55,7 +55,7 @@ def configure(log_level: Optional[str] = None, log_file: Optional[Path] = None): log_file = Path(log_file) log_file.parent.mkdir(parents=True, exist_ok=True) - log_format_file = '[%Y-%m-%d %H:%M:%S.%f] [{level.name} process-{process.id}-{thread.id} {name}:{line}] - trace={extra[trace_id]} {message}' # noqa + log_format_file = '[{time:YYYY-MM-DD at HH:mm:ss.SSS}] [{level.name} process-{process.id}-{thread.id} {name}:{line}] - trace={extra[trace_id]} {message}' # noqa logger.add( sink=str(log_file), level=log_level.upper(),