Update a.py
Browse files
a.py
CHANGED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
from typing import Annotated
|
| 4 |
+
import logging
|
| 5 |
+
import json
|
| 6 |
+
import asyncio
|
| 7 |
+
|
| 8 |
+
from livekit import agents
|
| 9 |
+
from livekit.agents import AgentSession, Agent, RoomInputOptions, function_tool, get_job_context
|
| 10 |
+
from livekit import api
|
| 11 |
+
from livekit.agents import ConversationItemAddedEvent
|
| 12 |
+
from livekit.plugins import (
|
| 13 |
+
silero,
|
| 14 |
+
google,
|
| 15 |
+
noise_cancellation,
|
| 16 |
+
)
|
| 17 |
+
from pydantic import Field
|
| 18 |
+
# from livekit.plugins.turn_detector.multilingual import MultilingualModel
|
| 19 |
+
from utils import compare_cars_md
|
| 20 |
+
from pipelinev2 import RAGAgent, RAGConfig, llm
|
| 21 |
+
|
| 22 |
+
load_dotenv()
|
| 23 |
+
|
| 24 |
+
# we will store the transcripts and token usage in a json
|
| 25 |
+
log_dir = Path("logs")
|
| 26 |
+
log_dir.mkdir(exist_ok=True) # creates a folder if it didn't exist
|
| 27 |
+
transcript_path = log_dir / "transcript.json"
|
| 28 |
+
metrics_path = log_dir / "metrics.json"
|
| 29 |
+
|
| 30 |
+
# initialize files
|
| 31 |
+
transcript_path.write_text("[]", encoding='utf-8')
|
| 32 |
+
metrics_path.write_text("{}", encoding='utf-8')
|
| 33 |
+
|
| 34 |
+
# Setup logging
|
| 35 |
+
logger = logging.getLogger(__name__)
|
| 36 |
+
logger.setLevel(logging.DEBUG)
|
| 37 |
+
|
| 38 |
+
# Initialize RAG Agent
|
| 39 |
+
config = RAGConfig(
|
| 40 |
+
use_hybrid=False,
|
| 41 |
+
qna_file_path="QnA.json",
|
| 42 |
+
)
|
| 43 |
+
rag_agent = RAGAgent(llm=llm, config=config)
|
| 44 |
+
logger.info("✅ RAG Agent initialized")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@function_tool()
|
| 48 |
+
async def compare_two_cars(
|
| 49 |
+
car1: Annotated[str, Field(description="Tên xe thứ nhất cần so sánh (ví dụ: 'Honda City', 'Toyota Vios', 'Lynk & Co 05')")],
|
| 50 |
+
car2: Annotated[str, Field(description="Tên xe thứ hai cần so sánh (ví dụ: 'Mazda 3', 'Hyundai Accent', 'Lynk & Co 01')")],
|
| 51 |
+
) -> str:
|
| 52 |
+
"""Gọi tool này khi khách hàng muốn so sánh hai mẫu xe
|
| 53 |
+
- Chỉ hỏi rõ lại 2 mẫu xe khách hàng quan tâm nếu chưa rõ yêu cầu
|
| 54 |
+
- Sử dụng tool compare_two_cars để lấy thông tin so sánh chi tiết
|
| 55 |
+
- Phân tích ngắn gọn dưới 2 câu và tư vấn dựa trên kết quả so sánh
|
| 56 |
+
"""
|
| 57 |
+
logger.info(f"🔧 TOOL CALLED: compare_two_cars")
|
| 58 |
+
logger.info(f" 📌 Car 1: {car1}")
|
| 59 |
+
logger.info(f" 📌 Car 2: {car2}")
|
| 60 |
+
|
| 61 |
+
try:
|
| 62 |
+
result = compare_cars_md(car1, car2)
|
| 63 |
+
logger.info(f" ✅ Comparison successful, result length: {len(result)} chars")
|
| 64 |
+
return result
|
| 65 |
+
except Exception as e:
|
| 66 |
+
logger.error(f" ❌ Error comparing cars: {str(e)}")
|
| 67 |
+
return f"Xin lỗi, đã có lỗi khi so sánh hai xe: {str(e)}"
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@function_tool()
|
| 71 |
+
async def query_car_information(
|
| 72 |
+
question: Annotated[str, Field(description="Câu hỏi về thông tin xe Lynk & Co, Volvo, Geely")]
|
| 73 |
+
) -> str:
|
| 74 |
+
"""Truy vấn thông tin chi tiết về xe từ cơ sở dữ liệu.
|
| 75 |
+
|
| 76 |
+
Sử dụng khi khách hàng hỏi về:
|
| 77 |
+
- Thông tin các dòng xe Lynk & Co (01, 03+, 05, 06, 08, 09)
|
| 78 |
+
- Thông tin xe Volvo
|
| 79 |
+
- Thông tin xe Geely
|
| 80 |
+
- Đặc điểm kỹ thuật, tính năng, giá cả
|
| 81 |
+
- Câu hỏi chung về các thương hiệu
|
| 82 |
+
|
| 83 |
+
VÍ DỤ:
|
| 84 |
+
- "Lynk & Co 01 có những tính năng gì?"
|
| 85 |
+
- "Giá xe Volvo bao nhiêu?"
|
| 86 |
+
- "So sánh động cơ các dòng Lynk & Co"
|
| 87 |
+
"""
|
| 88 |
+
logger.info(f"🔍 TOOL CALLED: query_car_information")
|
| 89 |
+
logger.info(f" 📌 Question: {question}")
|
| 90 |
+
|
| 91 |
+
try:
|
| 92 |
+
# Gọi RAG agent để truy vấn thông tin
|
| 93 |
+
result = rag_agent.invoke(question)
|
| 94 |
+
logger.info(f" ✅ Query successful, result length: {len(result)} chars")
|
| 95 |
+
return result
|
| 96 |
+
except Exception as e:
|
| 97 |
+
logger.error(f" ❌ Error querying car information: {str(e)}")
|
| 98 |
+
return f"Xin lỗi, đã có lỗi khi tìm kiếm thông tin: {str(e)}"
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
@function_tool()
|
| 102 |
+
async def handle_off_topic(
|
| 103 |
+
question: Annotated[str, Field(description="Câu hỏi không liên quan đến ô tô mà khách hàng đặt ra")]
|
| 104 |
+
) -> str:
|
| 105 |
+
"""GỌI TOOL NÀY khi khách hỏi về các vấn đề KHÔNG liên quan đến ô tô, xe hơi, mua bán xe.
|
| 106 |
+
|
| 107 |
+
SAU KHI gọi tool này, hãy:
|
| 108 |
+
- Trả lời câu hỏi một cách NGẮN GỌN trong 1 câu theo phong cách HÀI HƯỚC
|
| 109 |
+
- Khéo léo chuyển hướng về tư vấn xe
|
| 110 |
+
- Giữ phong cách thân thiện, chuyên nghiệp"""
|
| 111 |
+
logger.info(f"🎭 TOOL CALLED: handle_off_topic")
|
| 112 |
+
logger.info(f" 📌 Question: {question}")
|
| 113 |
+
logger.info(f" ✅ Letting Gemini handle off-topic response creatively")
|
| 114 |
+
|
| 115 |
+
return f"Đây là câu hỏi không liên quan đến xe. Hãy trả lời ngắn gọn, hài hước và chuyển hướng về tư vấn xe."
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
@function_tool()
|
| 119 |
+
async def navigate_to_screen(
|
| 120 |
+
route: Annotated[str, Field(description="Route: /home_page, /car_detail, /tourist_show_room_page, /compare_cars, /web_view_360_car_page")],
|
| 121 |
+
product_name: Annotated[str | None, Field(description="Tên xe. VD: 'Lynk & Co 01'")] = None,
|
| 122 |
+
color_type: Annotated[str | None, Field(description="Màu xe. VD: 'Đỏ'")] = None,
|
| 123 |
+
variant_type: Annotated[str | None, Field(description="Phiên bản. VD: 'RS'")] = None,
|
| 124 |
+
title: Annotated[str | None, Field(description="Tiêu đề trang")] = None,
|
| 125 |
+
url: Annotated[str | None, Field(description="URL 360")] = None,
|
| 126 |
+
is_interior: Annotated[bool | None, Field(description="True=nội thất, False=ngoại thất")] = None,
|
| 127 |
+
show_header: Annotated[bool, Field(description="Hiện header")] = True,
|
| 128 |
+
) -> str:
|
| 129 |
+
"""Điều hướng đến các trang trong app.
|
| 130 |
+
|
| 131 |
+
VÍ DỤ:
|
| 132 |
+
- Về trang chủ → route="/home_page"
|
| 133 |
+
- Xem xe Lynk & Co 01 → route="/car_detail", product_name="Lynk & Co 01"
|
| 134 |
+
- Xem showroom → route="/tourist_show_room_page"
|
| 135 |
+
- So sánh xe → route="/compare_cars"
|
| 136 |
+
- Xem nội thất xe Lynk & Co 01, màu Đỏ, phiên bản RS →
|
| 137 |
+
route="/web_view_360_car_page", product_name="Lynk & Co 01", color_type="Đỏ", variant_type="RS", is_interior=True
|
| 138 |
+
"""
|
| 139 |
+
logger.info(f"🧭 TOOL CALLED: navigate_to_screen")
|
| 140 |
+
logger.info(f" 📌 Route: {route}")
|
| 141 |
+
logger.info(f" 📌 Product: {product_name}")
|
| 142 |
+
logger.info(f" 📌 Color: {color_type}")
|
| 143 |
+
logger.info(f" 📌 Variant: {variant_type}")
|
| 144 |
+
|
| 145 |
+
# Tạo params dict, chỉ thêm các tham số không None
|
| 146 |
+
params = {}
|
| 147 |
+
if product_name:
|
| 148 |
+
params["productName"] = product_name
|
| 149 |
+
if color_type:
|
| 150 |
+
params["colorType"] = color_type
|
| 151 |
+
if variant_type:
|
| 152 |
+
params["variantType"] = variant_type
|
| 153 |
+
if title:
|
| 154 |
+
params["title"] = title
|
| 155 |
+
if url:
|
| 156 |
+
params["url"] = url
|
| 157 |
+
if is_interior is not None:
|
| 158 |
+
params["isInterior"] = is_interior
|
| 159 |
+
if show_header is not None:
|
| 160 |
+
params["showHeader"] = show_header
|
| 161 |
+
|
| 162 |
+
navigation_data = {
|
| 163 |
+
"route": route,
|
| 164 |
+
"params": params
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
logger.info(f" 📱 Navigation data: {json.dumps(navigation_data, ensure_ascii=False)}")
|
| 168 |
+
logger.info(f" ✅ Navigation command prepared")
|
| 169 |
+
|
| 170 |
+
# Trả về JSON để app xử lý
|
| 171 |
+
return json.dumps(navigation_data, ensure_ascii=False)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
@function_tool()
|
| 175 |
+
async def end_call() -> str:
|
| 176 |
+
"""GỌI TOOL NÀY khi khách hàng nói TẠM BIỆT hoặc MUỐN KẾT THÚC cuộc gọi.
|
| 177 |
+
|
| 178 |
+
TRƯỚC KHI gọi tool này:
|
| 179 |
+
- Nếu khách hàng tích cực: Nói "Cảm ơn quý khách đã quan tâm. Chúc quý khách một ngày tốt lành!"
|
| 180 |
+
- Nếu khách hàng tiêu cực: Nói "Xin lỗi quý khách. Mong được phục vụ quý khách tốt hơn lần sau!"
|
| 181 |
+
|
| 182 |
+
SAU ĐÓ MỚI gọi tool này để kết thúc cuộc gọi."""
|
| 183 |
+
logger.info("📞 TOOL CALLED: end_call - Ending conversation")
|
| 184 |
+
|
| 185 |
+
try:
|
| 186 |
+
job_ctx = get_job_context()
|
| 187 |
+
await job_ctx.api.room.delete_room(api.DeleteRoomRequest(room=job_ctx.room.name))
|
| 188 |
+
logger.info("✅ Room deleted successfully")
|
| 189 |
+
return "Cuộc trò chuyện đã kết thúc. Cảm ơn quý khách!"
|
| 190 |
+
except Exception as e:
|
| 191 |
+
logger.error(f"❌ Error ending call: {str(e)}")
|
| 192 |
+
return "Đã kết thúc cuộc trò chuyện"
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
class Assistant(Agent):
|
| 196 |
+
def __init__(self) -> None:
|
| 197 |
+
logger.info("🤖 Initializing Assistant Agent with tools")
|
| 198 |
+
super().__init__(
|
| 199 |
+
instructions='''Bạn là trợ lý tư vấn xe tại VETC.
|
| 200 |
+
|
| 201 |
+
QUAN TRỌNG - Sử dụng tools:
|
| 202 |
+
|
| 203 |
+
1. query_car_information: Khi khách hỏi về thông tin xe (tính năng, giá, kỹ thuật)
|
| 204 |
+
VD: "Lynk & Co 01 có gì?", "Giá xe Volvo?"
|
| 205 |
+
|
| 206 |
+
2. navigate_to_screen: Khi khách muốn xem trang/màn hình
|
| 207 |
+
- "về trang chủ" → navigate_to_screen(route="/home_page")
|
| 208 |
+
- "xem xe [tên]" → navigate_to_screen(route="/car_detail", product_name="tên")
|
| 209 |
+
- "xem showroom" → navigate_to_screen(route="/tourist_show_room_page")
|
| 210 |
+
- "so sánh xe" → navigate_to_screen(route="/compare_cars")
|
| 211 |
+
|
| 212 |
+
3. compare_two_cars: Khi khách muốn so sánh 2 xe cụ thể
|
| 213 |
+
VD: "So sánh Lynk & Co 01 và 05"
|
| 214 |
+
|
| 215 |
+
4. handle_off_topic: Khi câu hỏi không liên quan xe
|
| 216 |
+
|
| 217 |
+
Phong cách:
|
| 218 |
+
- Xưng hô "quý khách"
|
| 219 |
+
- Thân thiện, chuyên nghiệp
|
| 220 |
+
- Trả lời ngắn gọn, súc tích''',
|
| 221 |
+
tools=[query_car_information, compare_two_cars, handle_off_topic, navigate_to_screen, end_call]
|
| 222 |
+
)
|
| 223 |
+
logger.info(f"✅ Agent initialized with {len(self.tools)} tool(s)")
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
async def log_transcription(event: ConversationItemAddedEvent):
|
| 227 |
+
"""Lưu transcript vào file JSON"""
|
| 228 |
+
try:
|
| 229 |
+
item = event.item
|
| 230 |
+
role = item.role
|
| 231 |
+
text = item.text_content
|
| 232 |
+
|
| 233 |
+
# Đọc transcript hiện tại
|
| 234 |
+
transcript = json.loads(transcript_path.read_text(encoding='utf-8'))
|
| 235 |
+
|
| 236 |
+
# Thêm entry mới
|
| 237 |
+
transcript.append({
|
| 238 |
+
"role": role,
|
| 239 |
+
"text": text,
|
| 240 |
+
"timestamp": str(asyncio.get_event_loop().time())
|
| 241 |
+
})
|
| 242 |
+
|
| 243 |
+
# Lưu chỉ 100 message gần nhất
|
| 244 |
+
transcript_path.write_text(json.dumps(transcript[-100:], indent=2, ensure_ascii=False), encoding='utf-8')
|
| 245 |
+
|
| 246 |
+
logger.info(f"📝 Transcript logged: {role} - {text[:50]}...")
|
| 247 |
+
except Exception as e:
|
| 248 |
+
logger.error(f"❌ Error logging transcript: {str(e)}")
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
async def entrypoint(ctx: agents.JobContext):
|
| 252 |
+
logger.info("🚀 Starting entrypoint...")
|
| 253 |
+
|
| 254 |
+
session = AgentSession(
|
| 255 |
+
llm=google.beta.realtime.RealtimeModel(
|
| 256 |
+
model="gemini-2.0-flash-live-001",
|
| 257 |
+
voice="Aoede",
|
| 258 |
+
temperature=0.8,
|
| 259 |
+
instructions="You are a helpful assistant",
|
| 260 |
+
language="vi-VN",
|
| 261 |
+
input_audio_transcription={},
|
| 262 |
+
output_audio_transcription={},
|
| 263 |
+
),
|
| 264 |
+
vad=silero.VAD.load(),
|
| 265 |
+
# turn_detection=MultilingualModel(),
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
# Đăng ký event handler cho transcript logging
|
| 269 |
+
@session.on("conversation_item_added")
|
| 270 |
+
def _on_conversation_item(event: ConversationItemAddedEvent):
|
| 271 |
+
asyncio.create_task(log_transcription(event))
|
| 272 |
+
|
| 273 |
+
# Đăng ký event handler cho function calls
|
| 274 |
+
@session.on("function_calls_collected")
|
| 275 |
+
def _on_function_calls(event):
|
| 276 |
+
logger.info(f"🔔 FUNCTION CALLS COLLECTED EVENT")
|
| 277 |
+
logger.info(f" Function calls: {event}")
|
| 278 |
+
|
| 279 |
+
@session.on("function_calls_finished")
|
| 280 |
+
def _on_function_calls_finished(event):
|
| 281 |
+
logger.info(f"✅ FUNCTION CALLS FINISHED EVENT")
|
| 282 |
+
logger.info(f" Results: {event}")
|
| 283 |
+
|
| 284 |
+
logger.info("📡 Starting session...")
|
| 285 |
+
|
| 286 |
+
await session.start(
|
| 287 |
+
room=ctx.room,
|
| 288 |
+
agent=Assistant(),
|
| 289 |
+
room_input_options=RoomInputOptions(
|
| 290 |
+
# LiveKit Cloud enhanced noise cancellation
|
| 291 |
+
# - If self-hosting, omit this parameter
|
| 292 |
+
# - For telephony applications, use `BVCTelephony` for best results
|
| 293 |
+
noise_cancellation=noise_cancellation.BVC(),
|
| 294 |
+
),
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
await session.generate_reply(
|
| 298 |
+
instructions="Chào bạn, tôi có thể giúp gì cho bạn?"
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
if __name__ == "__main__":
|
| 303 |
+
agents.cli.run_app(agents.WorkerOptions(entrypoint_fnc=entrypoint))
|