วิธีสร้าง Agent ด้วย OpenAI [backend]

LLMs เราอาจจะคุ้นชินกับ chatbot ที่ถามและตอบคำถามทีละคำถามได้ แต่ทว่า AI chatbot มันมีความเก่งที่สามารถเอาคำตอบที่ได้ ไปสร้างคำถามอื่นๆต่อ หรือที่เราจะเรียกว่าเป็น Agent ในการตอบคำถามตาม Task ที่เราระบุเป็น Template ทิ้งไว้ โดย Backend ที่เราทำขึ้นมา เป็นการทำให้การทำงานทั้งหมดเป็นไปอย่างราบรื่น ในการส่งคำตอบไปยังแต่ละ Agent เมื่อใช้งานผ่าน web application

สำหรับตัวอย่างนี้จะเป็นการสร้างชุดของ Agent ที่จะเป็นเหมือนผู้ช่วยในการแบ่ง task งานในการ develop application ไปยังแต่ละทีมได้แก่ frontend, backend และ designer. เพื่อให้ผู้ใช้งานสร้าง guideline เบื้องต้นให้กับทีมงานของตัวเอง ทำงานได้รวดเร็วขึ้น

Setting Up a FastAPI Project

เหมือนกับทุกๆตัวอย่างที่ผ่านมา เราเลือกใช้งาน FastAPI เป็น Framework ในการสร้าง API ขึ้นมาใช้งาน

ทำการติดตั้ง python3 library ที่จำเป็นสำหรับ FastAPI

pip install fastapi
pip install "uvicorn[standard]"

สร้างไฟล์ app.py และทำการ initial FastAPI ขึ้นมา

from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
@app.get("/helloworld")
async def helloworld():
    return {"message": "Hello World"}

จากตัวอย่าง code จะเป็นการ initial FastAPI project แล้ว enable CORS สำหรับการเชื่อมต่อกับ frontend ให้เรียบร้อย และการสั่ง run จะใช้คำสั่งว่า

uvicorn app:app

เป็นการสั่งให้ FastAPI ที่เราประกาศไว้ในไฟล์ app.py ทำงาน โดย server จะรันที่ port 8000 เป็น default

แล้วเราจะทำการสร้างอีกหนึ่งไฟล์ที่ชื่อว่า LocalTemplate.py สำหรับเก็บ template ตั้งต้นในการถามคำถามไปยัง Chatbot ประกอบไปด้วย

  1. Manager-template เป็น template ที่เอาไว้แบ่ง task สำหรับคำถามที่เข้ามา เพื่อนำคำตอบไปใช้งานในแต่ละ agent
  2. Agent-template เราจะแบ่งออกเป็น 3 ตัว ได้แก่ Frontend Backend และ Designer ซึ่งแต่ละตัวก็จะตอบคำถามในส่วนของตัวเอง จากโจทย์ที่ได้รับมาจาก Manager
  3. Conclusion-template เอาไว้สรุปคำตอบทั้งหมดที่เราได้รับมาให้กระชับมากยิ่งขึ้น

ในส่วนของ API design เราจะทำการแบ่ง API เป็น 5 เส้น สำหรับการใช้งาน เดี๋ยวเราจะมาเล่าให้ฟังในหัวข้อถัดไปกันนะครับ

Developing the Manager Agent API

API เส้นแรกที่เราสร้างก็คือ

POST: breakdown

@app.post('/breakdown')
def breakdown_question(customer_need : str):
    model_256 = ChatOpenAI(model_name="gpt-4-1106-preview", temperature=0.3, max_tokens=256,openai_api_key = OPENAI_API_KEY)
    breakdown_chain = ChatPromptTemplate.from_template(LocalTemplate.get_manager()) | model_256
    result = breakdown_chain.invoke({"question": customer_need})
    arr = result.content.split('<question>')[1:]
    task_list = []
    for i in arr : 
        full_task = remove_tag(i,['<question>','<role>','</question>','</role>','</sub-question>']).strip()
        full_task_list = full_task.split('<sub-question>')
        role = full_task_list[0]
        task = full_task_list[1]
        task_list.append({'role':role,'task':task})

    json_compatible_item_data = jsonable_encoder(task_list)
    return JSONResponse(content=json_compatible_item_data)

หน้าที่ของมันมีหลักๆก็คือการรับ input ส่วนที่เป็นคำถามที่ user ต้องการเข้า ยกตัวอย่างเช่น “create blog“ API นี้จะสร้างคำตอบที่จำเป็นสำหรับแต่ละ Agent ที่จะบอกว่า การ create web blog แต่ละ Agent มีงานอะไรต้องทำบ้าง

Building Task Processing APIs

จากหัวข้อเมื่อกี้เราจะได้ ข้อมูลที่แต่ละ Agent ต้องทำมาเป็นคำถามตั้งต้นแล้ว ขึ้นตอนนี้เราจะทำ API ทำให้แบ่งข้อมูลที่ได้รับมาจากตัว breakdown มาส่งเข้า Agent เพื่อให้สร้างคำตอบเฉพาะในสิ่งที่ตัวเองต้องทำออกมา โดยเราใช้ template ที่สร้างไว้ใน LocalTemplate เป็นตัวระบุว่าเราจะให้ตอบออกมาในรูปแบบไหน

POST: build

def build_task(task_list : List[task_list]):
    model_256 = ChatOpenAI(model_name="gpt-4-1106-preview", temperature=0.3, max_tokens=256,openai_api_key = OPENAI_API_KEY)
    frontend_chain = ChatPromptTemplate.from_template(LocalTemplate.get_frontend()) | model_256 
    frontend_result = frontend_chain.invoke({"task": task_list[0].task})
    
    backend_chain = ChatPromptTemplate.from_template(LocalTemplate.get_backend()) | model_256 
    backend_result = backend_chain.invoke({"task": task_list[1].task})

    designer_chain = ChatPromptTemplate.from_template(LocalTemplate.get_designer()) | model_256 
    designer_result = designer_chain.invoke({"task": task_list[2].task})
    
    frontend_text = frontend_result.content.split('<step>')[1:]
    frontend_json = text_to_json(frontend_text,'<description>',['<task>','</task>','<step>','</step>','</description>'])
    backend_text = backend_result.content.split('<step>')[1:]
    backend_json = text_to_json(backend_text,'<description>',['<task>','</task>','<step>','</step>','</description>'])
    designer_text = designer_result.content.split('<step>')[1:]
    designer_json = text_to_json(designer_text,'<description>',['<task>','</task>','<step>','</step>','</description>'])

    result = {
        'raw' : {
            "frontend_task": frontend_result.content, 
            "backend_task": frontend_result.content, 
            "designer_task": frontend_result.content
        },
        'json' : {
            "frontend_task": frontend_json, 
            "backend_task": backend_json, 
            "designer_task": designer_json
        }
    }


    json_compatible_item_data = jsonable_encoder(result)
    return JSONResponse(content=json_compatible_item_data)

จากที่เรายกตัวอย่างไว้ที่เราได้ส่งคำถามเข้าไปว่า create blog เพื่อให้ตัว breakdown API สร้าง สิ่งที่แต่ละ Agent ต้องทำออกมาแบบสรุป และเราเอาคำตอบตรงนั้นมาเข้า build API ที่เราเรียกว่า Agent ซึ่งจะมองเป็นตัวแทนของแต่ละ role ก็ได้ ตามที่เล่าไปตอนแรก เราก็จะคำตอบที่เป็น Task ที่ต้องทำงานสำหรับ frontend, backend, designer ออกมาเพื่อนำไปใช้งานต่อได้แล้ว

Creating a Manager Summary API

API สำหรับการสรุปข้อมูลทั้งหมดที่เราได้มา ตั้งแตคำถามเริ่มต้น จนถึง task ที่ได้ออกมาในแต่ละ agent ให้เป็นข้อมูลที่กระชับและเข้าใจง่าย

POST: conclude

@app.post('/conclude')
def build_conclusion(customer_need : str, frontend_task : str, backend_task : str, designer_task : str):
    model_512 = ChatOpenAI(model_name="gpt-4-1106-preview", temperature=0.3, max_tokens=512,openai_api_key = OPENAI_API_KEY)
    customer_chain = ChatPromptTemplate.from_template(LocalTemplate.get_conclusion()) | model_512
    customer_result = customer_chain.invoke({
        "customer_need" : customer_need,
        "frontend_task": frontend_task, 
        "backend_task": backend_task, 
        "designer_task": designer_task
    })

    customer_json = remove_tag(customer_result.content,['<conclude>','</conclude>','<text>','</text>'])
    result =  {
        'raw' : customer_result.content,
        'json' : {
            'customer_need' : customer_json
        }
    }
    json_compatible_item_data = jsonable_encoder(result)
    return JSONResponse(content=json_compatible_item_data)

Implementing a Chained-LLM Wrapper API

API นี้จะเป็นการรวบการทำงานทั้งหมดที่เล่ามาให้อยู่ใน API เดียว ซึ่งจะทำให้ง่ายต่อการนำไป integrate ร่วมกับ frontend มากขึ้น

POST: query

@app.post('/query')
def query_with_chain(question : str):
    customer = question
    model_256 = ChatOpenAI(model_name="gpt-4-1106-preview", temperature=0.3, max_tokens=256,openai_api_key = OPENAI_API_KEY)
    model_512 = ChatOpenAI(model_name="gpt-4-1106-preview", temperature=0.3, max_tokens=512,openai_api_key = OPENAI_API_KEY)
    PO_Final_Chain = ChatPromptTemplate.from_template(LocalTemplate.get_manager()) | model_256
    result = PO_Final_Chain.invoke({"question": customer})
    # print(result.content)
    arr = result.content.split('<question>')[1:]
    task_list = []
    for i in arr : 
        full_task = i.replace('\n','').replace('</question>','').replace('<role>','').replace('</role>','').replace('</sub-question>','').strip()
        role = full_task.split('<sub-question>')[0].strip()
        task = full_task.split('<sub-question>')[1].strip()
        task_list.append({'role':role,'task':task})

    frontend_chain = ChatPromptTemplate.from_template(LocalTemplate.get_frontend()) | model_256 
    frontend_result = frontend_chain.invoke({"task": task_list[0]['task']})

    backend_chain = ChatPromptTemplate.from_template(LocalTemplate.get_backend()) | model_256 
    backend_result = backend_chain.invoke({"task": task_list[1]['task']})

    designer_chain = ChatPromptTemplate.from_template(LocalTemplate.get_designer()) | model_256 
    designer_result = designer_chain.invoke({"task": task_list[2]['task']})

    customer_chain = ChatPromptTemplate.from_template(LocalTemplate.get_conclusion()) | model_512
    customer_result = customer_chain.invoke({
        "customer_need" : customer,
        "frontend_task": frontend_result.content, 
        "backend_task": backend_result.content, 
        "designer_task": designer_result.content
    })

    customer_json = remove_tag(customer_result.content,['<conclude>','</conclude>','<text>','</text>'])
    result =  {
        'raw' : customer_result.content,
        'json' : {
            'customer_need' : customer_json
        }
    }

    json_compatible_item_data = jsonable_encoder(result)
    return JSONResponse(content=json_compatible_item_data)

เพียงแค่โยนคำถามในสิ่งที่ต้องการเข้ามา API เส้นนี้จะทำการสร้าง task ส่งไปยังทุก Agent และสรุปคำตอบทุกอย่างกลับมา และทำการคืนค่าออกไปใน response เดียว

Direct OpenAI Prompt API Integration

และเพื่อให้การใช้งานตัว chatbot เราครบเครื่องมากขึ้น นอกจากที่เราจะสร้าง flow ในการตอบคำถามที่ Agent แล้ว เรายังมีการ develop API ในส่วนที่เป็นการ bypass คำถามที่เข้ามาแล้วส่งไปยัง chatbot ตรงๆ

POST: queryWithoutChain

def query_without_chain(question : str):
    model_512 = ChatOpenAI(model_name="gpt-4-1106-preview", temperature=0.3, max_tokens=512,openai_api_key = OPENAI_API_KEY)
    chain = ChatPromptTemplate.from_template('{question}') | model_512
    customer_result = chain.invoke({"question": question})
    
    result =  {
        'raw' : customer_result.content,
        'json' : {
            'customer_need' : customer_result.content
        }
    }

    json_compatible_item_data = jsonable_encoder(result)

    return JSONResponse(content=json_compatible_item_data)

Deploying and Monitoring on EC2

มาถึงขั้นตอนในการ deploy FastAPI เพื่อใช้งานบน EC2 instance โดยจะมีขั้นตอนที่ไม่ยากเลยดังนี้

Step 1: สร้าง session โดยค่า name ให้เราเปลี่ยนเป็นชื่อที่เราต้องการได้

screen -S name

Step 2: เข้าไปยัง folder ของ api

cd path/to/api

Step 3: สั่ง start FastAPI ให้รันที่ port 8000

uvicorn app:app --host 0.0.0.0

Step 4: ออกจาก session screen ปัจจุบัน (detach)

Ctrl+a d

เราก็จะได้ server ทำงานอยู่ background สำหรับการใช้งานได้ แม้เราออกจาก server ไปแล้ว

Setting Up API Gateway and Implementing CORS

และในการนำไปใช้งานที่สะดวกมากขึ้น เราจะทำการ implement ตัว API ที่เราทำเข้ากับ API Gateway เพื่อให้การ manage และจำกัดการใช้งาน

ทำการเพิ่ม configuration สำหรับ allow CORS ให้กับ FastAPI

from fastapi.middleware.cors import CORSMiddleware
app.app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

จากนั้นเราจะเชื่อมต่อ server นี้ไปยัง AWS API Gateway เพื่อให้ API Gateway ช่วยในการจัดการเรื่อง authentication และ usage ต่างๆสำหรับคนที่จะเข้ามาใช้งานในแต่ละ request ที่เกิดขึ้น และทางเราได้มี document ที่พูดถึงการใช้งาน API Gateway · VulturePrime ไว้สำหรับทุกคนแล้ว

Conclusion

สุดท้ายนี้ เราจะมาสรุปการทำงานทั้งหมดที่เราได้ลงมือไปกัน เพื่อทบทวนความเข้าใจกันอีกซักรอบ ในการทำ chatbot รูปแบบที่เป็น Agent

เราได้เริ่มต้นจากเตรียม Template สำหรับรับคำถามเพื่อกระจายงานไปยัง Agent ต่างๆ และ Template ที่แต่ละ Agent ต้องใช้งาน รวมถึง Template ที่สำหรับสรุปข้อมูลทั้งหมดที่เกิดขึ้น

จากนั้นเราเริ่มสร้าง API ที่ใช้ในการส่งต่อ Input/Output ของแต่ละ Template ให้ทำงานต่อกันเป็น Chain ที่ต่อเนื่อง เพื่อสร้างคำตอบทั้งหมดที่เราต้องการออกมา

Aa

© 2023, All Rights Reserved, VulturePrime co., ltd.