CoolFace
Apppublic

llmbb/LLMBB-Agent

sourceHugging Faceapache-2.0updated 3y agoView on Hugging Face
1likes
openai_api.py564 linesDownload Raw Back to root
1# coding=utf-82# Implements API for Qwen-7B in OpenAI's format. (https://platform.openai.com/docs/api-reference/chat)3# Usage: python openai_api.py4# Visit http://localhost:8000/docs for documents.5 6import re7import copy8import json9import time10from argparse import ArgumentParser11from contextlib import asynccontextmanager12from typing import Dict, List, Literal, Optional, Union13 14import torch15import uvicorn16from fastapi import FastAPI, HTTPException17from fastapi.middleware.cors import CORSMiddleware18from pydantic import BaseModel, Field19from sse_starlette.sse import EventSourceResponse20from transformers import AutoTokenizer, AutoModelForCausalLM21from transformers.generation import GenerationConfig22from starlette.middleware.base import BaseHTTPMiddleware23from starlette.requests import Request24from starlette.responses import Response25import base6426 27 28class BasicAuthMiddleware(BaseHTTPMiddleware):29    def __init__(self, app, username: str, password: str):30        super().__init__(app)31        self.required_credentials = base64.b64encode(f"{username}:{password}".encode()).decode()32 33    async def dispatch(self, request: Request, call_next):34        authorization: str = request.headers.get("Authorization")35        if authorization:36            try:37                schema, credentials = authorization.split()38                if credentials == self.required_credentials:39                    return await call_next(request)40            except ValueError:41                pass42 43        headers = {'WWW-Authenticate': 'Basic'}44        return Response(status_code=401, headers=headers)45 46 47def _gc(forced: bool = False):48    global args49    if args.disable_gc and not forced:50        return51 52    import gc53    gc.collect()54    if torch.cuda.is_available():55        torch.cuda.empty_cache()56 57 58@asynccontextmanager59async def lifespan(app: FastAPI):  # collects GPU memory60    yield61    _gc(forced=True)62 63 64app = FastAPI(lifespan=lifespan)65 66app.add_middleware(67    CORSMiddleware,68    allow_origins=["*"],69    allow_credentials=True,70    allow_methods=["*"],71    allow_headers=["*"],72)73 74 75class ModelCard(BaseModel):76    id: str77    object: str = "model"78    created: int = Field(default_factory=lambda: int(time.time()))79    owned_by: str = "owner"80    root: Optional[str] = None81    parent: Optional[str] = None82    permission: Optional[list] = None83 84 85class ModelList(BaseModel):86    object: str = "list"87    data: List[ModelCard] = []88 89 90class ChatMessage(BaseModel):91    role: Literal["user", "assistant", "system", "function"]92    content: Optional[str]93    function_call: Optional[Dict] = None94 95 96class DeltaMessage(BaseModel):97    role: Optional[Literal["user", "assistant", "system"]] = None98    content: Optional[str] = None99 100 101class ChatCompletionRequest(BaseModel):102    model: str103    messages: List[ChatMessage]104    functions: Optional[List[Dict]] = None105    temperature: Optional[float] = None106    top_p: Optional[float] = None107    max_length: Optional[int] = None108    stream: Optional[bool] = False109    stop: Optional[List[str]] = None110 111 112class ChatCompletionResponseChoice(BaseModel):113    index: int114    message: ChatMessage115    finish_reason: Literal["stop", "length", "function_call"]116 117 118class ChatCompletionResponseStreamChoice(BaseModel):119    index: int120    delta: DeltaMessage121    finish_reason: Optional[Literal["stop", "length"]]122 123 124class ChatCompletionResponse(BaseModel):125    model: str126    object: Literal["chat.completion", "chat.completion.chunk"]127    choices: List[128        Union[ChatCompletionResponseChoice, ChatCompletionResponseStreamChoice]129    ]130    created: Optional[int] = Field(default_factory=lambda: int(time.time()))131 132 133@app.get("/v1/models", response_model=ModelList)134async def list_models():135    global model_args136    model_card = ModelCard(id="gpt-3.5-turbo")137    return ModelList(data=[model_card])138 139 140# To work around that unpleasant leading-\n tokenization issue!141def add_extra_stop_words(stop_words):142    if stop_words:143        _stop_words = []144        _stop_words.extend(stop_words)145        for x in stop_words:146            s = x.lstrip("\n")147            if s and (s not in _stop_words):148                _stop_words.append(s)149        return _stop_words150    return stop_words151 152 153def trim_stop_words(response, stop_words):154    if stop_words:155        for stop in stop_words:156            idx = response.find(stop)157            if idx != -1:158                response = response[:idx]159    return response160 161 162TOOL_DESC = """{name_for_model}: Call this tool to interact with the {name_for_human} API. What is the {name_for_human} API useful for? {description_for_model} Parameters: {parameters}"""163 164REACT_INSTRUCTION = """Answer the following questions as best you can. You have access to the following APIs:165 166{tools_text}167 168Use the following format:169 170Question: the input question you must answer171Thought: you should always think about what to do172Action: the action to take, should be one of [{tools_name_text}]173Action Input: the input to the action174Observation: the result of the action175... (this Thought/Action/Action Input/Observation can be repeated zero or more times)176Thought: I now know the final answer177Final Answer: the final answer to the original input question178 179Begin!"""180 181_TEXT_COMPLETION_CMD = object()182 183 184#185# Temporarily, the system role does not work as expected.186# We advise that you write the setups for role-play in your query,187# i.e., use the user role instead of the system role.188#189# TODO: Use real system role when the model is ready.190#191def parse_messages(messages, functions):192    if all(m.role != "user" for m in messages):193        raise HTTPException(194            status_code=400,195            detail=f"Invalid request: Expecting at least one user message.",196        )197 198    messages = copy.deepcopy(messages)199    default_system = "You are a helpful assistant."200    system = ""201    if messages[0].role == "system":202        system = messages.pop(0).content.lstrip("\n").rstrip()203        if system == default_system:204            system = ""205 206    if functions:207        tools_text = []208        tools_name_text = []209        for func_info in functions:210            name = func_info.get("name", "")211            name_m = func_info.get("name_for_model", name)212            name_h = func_info.get("name_for_human", name)213            desc = func_info.get("description", "")214            desc_m = func_info.get("description_for_model", desc)215            tool = TOOL_DESC.format(216                name_for_model=name_m,217                name_for_human=name_h,218                # Hint: You can add the following format requirements in description:219                #   "Format the arguments as a JSON object."220                #   "Enclose the code within triple backticks (`) at the beginning and end of the code."221                description_for_model=desc_m,222                parameters=json.dumps(func_info["parameters"], ensure_ascii=False),223            )224            tools_text.append(tool)225            tools_name_text.append(name_m)226        tools_text = "\n\n".join(tools_text)227        tools_name_text = ", ".join(tools_name_text)228        system += "\n\n" + REACT_INSTRUCTION.format(229            tools_text=tools_text,230            tools_name_text=tools_name_text,231        )232        system = system.lstrip("\n").rstrip()233 234    dummy_thought = {235        "en": "\nThought: I now know the final answer.\nFinal answer: ",236        "zh": "\nThought: 我会作答了。\nFinal answer: ",237    }238 239    _messages = messages240    messages = []241    for m_idx, m in enumerate(_messages):242        role, content, func_call = m.role, m.content, m.function_call243        if content:244            content = content.lstrip("\n").rstrip()245        if role == "function":246            if (len(messages) == 0) or (messages[-1].role != "assistant"):247                raise HTTPException(248                    status_code=400,249                    detail=f"Invalid request: Expecting role assistant before role function.",250                )251            messages[-1].content += f"\nObservation: {content}"252            if m_idx == len(_messages) - 1:253                messages[-1].content += "\nThought:"254        elif role == "assistant":255            if len(messages) == 0:256                raise HTTPException(257                    status_code=400,258                    detail=f"Invalid request: Expecting role user before role assistant.",259                )260            last_msg = messages[-1].content261            last_msg_has_zh = len(re.findall(r"[\u4e00-\u9fff]+", last_msg)) > 0262            if func_call is None:263                if functions:264                    content = dummy_thought["zh" if last_msg_has_zh else "en"] + content265            else:266                f_name, f_args = func_call["name"], func_call["arguments"]267                if not content:268                    if last_msg_has_zh:269                        content = f"Thought: 我可以使用 {f_name} API。"270                    else:271                        content = f"Thought: I can use {f_name}."272                content = f"\n{content}\nAction: {f_name}\nAction Input: {f_args}"273            if messages[-1].role == "user":274                messages.append(275                    ChatMessage(role="assistant", content=content.lstrip("\n").rstrip())276                )277            else:278                messages[-1].content += content279        elif role == "user":280            messages.append(281                ChatMessage(role="user", content=content.lstrip("\n").rstrip())282            )283        else:284            raise HTTPException(285                status_code=400, detail=f"Invalid request: Incorrect role {role}."286            )287 288    query = _TEXT_COMPLETION_CMD289    if messages[-1].role == "user":290        query = messages[-1].content291        messages = messages[:-1]292 293    if len(messages) % 2 != 0:294        raise HTTPException(status_code=400, detail="Invalid request")295 296    history = []  # [(Q1, A1), (Q2, A2), ..., (Q_last_turn, A_last_turn)]297    for i in range(0, len(messages), 2):298        if messages[i].role == "user" and messages[i + 1].role == "assistant":299            usr_msg = messages[i].content.lstrip("\n").rstrip()300            bot_msg = messages[i + 1].content.lstrip("\n").rstrip()301            if system and (i == len(messages) - 2):302                usr_msg = f"{system}\n\nQuestion: {usr_msg}"303                system = ""304            for t in dummy_thought.values():305                t = t.lstrip("\n")306                if bot_msg.startswith(t) and ("\nAction: " in bot_msg):307                    bot_msg = bot_msg[len(t):]308            history.append([usr_msg, bot_msg])309        else:310            raise HTTPException(311                status_code=400,312                detail="Invalid request: Expecting exactly one user (or function) role before every assistant role.",313            )314    if system:315        assert query is not _TEXT_COMPLETION_CMD316        query = f"{system}\n\nQuestion: {query}"317    return query, history318 319 320def parse_response(response):321    func_name, func_args = "", ""322    i = response.rfind("\nAction:")323    j = response.rfind("\nAction Input:")324    k = response.rfind("\nObservation:")325    if 0 <= i < j:  # If the text has `Action` and `Action input`,326        if k < j:  # but does not contain `Observation`,327            # then it is likely that `Observation` is omitted by the LLM,328            # because the output text may have discarded the stop word.329            response = response.rstrip() + "\nObservation:"  # Add it back.330        k = response.rfind("\nObservation:")331        func_name = response[i + len("\nAction:"): j].strip()332        func_args = response[j + len("\nAction Input:"): k].strip()333    if func_name:334        choice_data = ChatCompletionResponseChoice(335            index=0,336            message=ChatMessage(337                role="assistant",338                content=response[:i],339                function_call={"name": func_name, "arguments": func_args},340            ),341            finish_reason="function_call",342        )343        return choice_data344    z = response.rfind("\nFinal Answer: ")345    if z >= 0:346        response = response[z + len("\nFinal Answer: "):]347    choice_data = ChatCompletionResponseChoice(348        index=0,349        message=ChatMessage(role="assistant", content=response),350        finish_reason="stop",351    )352    return choice_data353 354 355# completion mode, not chat mode356def text_complete_last_message(history, stop_words_ids, gen_kwargs):357    im_start = "<|im_start|>"358    im_end = "<|im_end|>"359    prompt = f"{im_start}system\nYou are a helpful assistant.{im_end}"360    for i, (query, response) in enumerate(history):361        query = query.lstrip("\n").rstrip()362        response = response.lstrip("\n").rstrip()363        prompt += f"\n{im_start}user\n{query}{im_end}"364        prompt += f"\n{im_start}assistant\n{response}{im_end}"365    prompt = prompt[: -len(im_end)]366 367    _stop_words_ids = [tokenizer.encode(im_end)]368    if stop_words_ids:369        for s in stop_words_ids:370            _stop_words_ids.append(s)371    stop_words_ids = _stop_words_ids372 373    input_ids = torch.tensor([tokenizer.encode(prompt)]).to(model.device)374    output = model.generate(input_ids, stop_words_ids=stop_words_ids, **gen_kwargs).tolist()[0]375    output = tokenizer.decode(output, errors="ignore")376    assert output.startswith(prompt)377    output = output[len(prompt):]378    output = trim_stop_words(output, ["<|endoftext|>", im_end])379    print(f"<completion>\n{prompt}\n<!-- *** -->\n{output}\n</completion>")380    return output381 382 383@app.post("/v1/chat/completions", response_model=ChatCompletionResponse)384async def create_chat_completion(request: ChatCompletionRequest):385    global model, tokenizer386 387    gen_kwargs = {}388    if request.temperature is not None:389        if request.temperature < 0.01:390            gen_kwargs['top_k'] = 1  # greedy decoding391        else:392            # Not recommended. Please tune top_p instead.393            gen_kwargs['temperature'] = request.temperature394    if request.top_p is not None:395        gen_kwargs['top_p'] = request.top_p396 397    stop_words = add_extra_stop_words(request.stop)398    if request.functions:399        stop_words = stop_words or []400        if "Observation:" not in stop_words:401            stop_words.append("Observation:")402 403    query, history = parse_messages(request.messages, request.functions)404 405    if request.stream:406        if request.functions:407            raise HTTPException(408                status_code=400,409                detail="Invalid request: Function calling is not yet implemented for stream mode.",410            )411        generate = predict(query, history, request.model, stop_words, gen_kwargs)412        return generate413        # return EventSourceResponse(generate, media_type="text/event-stream")414 415    stop_words_ids = [tokenizer.encode(s) for s in stop_words] if stop_words else None416    if query is _TEXT_COMPLETION_CMD:417        response = text_complete_last_message(history, stop_words_ids=stop_words_ids, gen_kwargs=gen_kwargs)418    else:419        response, _ = model.chat(420            tokenizer,421            query,422            history=history,423            stop_words_ids=stop_words_ids,424            **gen_kwargs425        )426        print(f"<chat>\n{history}\n{query}\n<!-- *** -->\n{response}\n</chat>")427    _gc()428 429    response = trim_stop_words(response, stop_words)430    if request.functions:431        choice_data = parse_response(response)432    else:433        choice_data = ChatCompletionResponseChoice(434            index=0,435            message=ChatMessage(role="assistant", content=response),436            finish_reason="stop",437        )438    return ChatCompletionResponse(439        model=request.model, choices=[choice_data], object="chat.completion"440    )441 442 443def _dump_json(data: BaseModel, *args, **kwargs) -> str:444    try:445        return data.model_dump_json(*args, **kwargs)446    except AttributeError:  # pydantic<2.0.0447        return data.json(*args, **kwargs)  # noqa448 449 450async def predict(451        query: str, history: List[List[str]], model_id: str, stop_words: List[str], gen_kwargs: Dict,452):453    global model, tokenizer454    choice_data = ChatCompletionResponseStreamChoice(455        index=0, delta=DeltaMessage(role="assistant"), finish_reason=None456    )457    chunk = ChatCompletionResponse(458        model=model_id, choices=[choice_data], object="chat.completion.chunk"459    )460    yield "{}".format(_dump_json(chunk, exclude_unset=True))461 462    current_length = 0463    stop_words_ids = [tokenizer.encode(s) for s in stop_words] if stop_words else None464    if stop_words:465        # TODO: It's a little bit tricky to trim stop words in the stream mode.466        raise HTTPException(467            status_code=400,468            detail="Invalid request: custom stop words are not yet supported for stream mode.",469        )470    response_generator = model.chat_stream(471        tokenizer, query, history=history, stop_words_ids=stop_words_ids, **gen_kwargs472    )473    for new_response in response_generator:474        if len(new_response) == current_length:475            continue476 477        new_text = new_response[current_length:]478        current_length = len(new_response)479 480        choice_data = ChatCompletionResponseStreamChoice(481            index=0, delta=DeltaMessage(content=new_text), finish_reason=None482        )483        chunk = ChatCompletionResponse(484            model=model_id, choices=[choice_data], object="chat.completion.chunk"485        )486        yield "{}".format(_dump_json(chunk, exclude_unset=True))487 488    choice_data = ChatCompletionResponseStreamChoice(489        index=0, delta=DeltaMessage(), finish_reason="stop"490    )491    chunk = ChatCompletionResponse(492        model=model_id, choices=[choice_data], object="chat.completion.chunk"493    )494    yield "{}".format(_dump_json(chunk, exclude_unset=True))495    yield "[DONE]"496 497    _gc()498 499 500def _get_args():501    parser = ArgumentParser()502    parser.add_argument(503        "-c",504        "--checkpoint-path",505        type=str,506        default="Qwen/Qwen-7B-Chat",507        help="Checkpoint name or path, default to %(default)r",508    )509    parser.add_argument(510        "--api-auth", help="API authentication credentials"511    )512    parser.add_argument(513        "--cpu-only", action="store_true", help="Run demo with CPU only"514    )515    parser.add_argument(516        "--server-port", type=int, default=8000, help="Demo server port."517    )518    parser.add_argument(519        "--server-name",520        type=str,521        default="127.0.0.1",522        help="Demo server name. Default: 127.0.0.1, which is only visible from the local computer."523             " If you want other computers to access your server, use 0.0.0.0 instead.",524    )525    parser.add_argument("--disable-gc", action="store_true",526                        help="Disable GC after each response generated.")527 528    args = parser.parse_args()529    return args530 531 532if __name__ == "__main__":533    args = _get_args()534 535    tokenizer = AutoTokenizer.from_pretrained(536        args.checkpoint_path,537        trust_remote_code=True,538        resume_download=True,539    )540 541    if args.api_auth:542        app.add_middleware(543            BasicAuthMiddleware, username=args.api_auth.split(":")[0], password=args.api_auth.split(":")[1]544        )545 546    if args.cpu_only:547        device_map = "cpu"548    else:549        device_map = "auto"550 551    model = AutoModelForCausalLM.from_pretrained(552        args.checkpoint_path,553        device_map=device_map,554        trust_remote_code=True,555        resume_download=True,556    ).eval()557 558    model.generation_config = GenerationConfig.from_pretrained(559        args.checkpoint_path,560        trust_remote_code=True,561        resume_download=True,562    )563 564    uvicorn.run(app, host=args.server_name, port=args.server_port, workers=1)