codeBOKER/customer_service
1
1import re2import json3from config import pc, index, EMBED_MODEL, hf_client, PROMPT, HF_MODEL, TRANSFER_PROMPT4from database import db_manager5from transfers import (6 prepare_transfer,7 confirm_transfer,8 cancel_transfer,9 get_pending_transfer,10 get_account_balance,11 get_sender_account,12)13 14 15MODEL_NAME = HF_MODEL16BASE_PROMPT = PROMPT or "You are a helpful banking customer service assistant."17TRANSFER_PROMPT = TRANSFER_PROMPT or (18 "Current user telegram_id is {telegram_id}. "19 "The model must never choose, guess, extract, or override any telegram_id for tool calls. "20 "Always act only for the current authenticated user from server-side request context. "21 "If the user asks for another person's balance or provides another person's telegram ID, refuse and explain that you can only access the current user's own account. "22 "If the user asks for their balance, call check_account_balance. "23 "For money transfers, first collect the receiver account serial ID and the amount if it is missing. "24 "Then call prepare_money_transfer to fetch the receiver name and store the pending transfer. "25 "Show the receiver name back to the user and ask for explicit confirmation. "26 "Only call confirm_money_transfer after the user clearly agrees. "27 "If the user rejects the receiver or wants to stop, call cancel_money_transfer. "28 "Never claim a transfer is completed unless confirm_money_transfer returns success. "29 "If a tool result returns 'need_user_name': true, ask the user for their name and then call create_illusion_account with their name. "30 "If a tool result indicates an illusion account was created (contains 'is_illusion': true or mentions testing), inform the user that an illusion account with 2000 YER balance was created for testing purposes. "31 "If confirm_money_transfer returns a result with 'is_illusion': true, make sure to display the testing disclaimer message to the user."32)33 34 35def clean_ai_response(text: str) -> str:36 if not text:37 return ""38 39 text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)40 41 text = re.sub(r'<br\s*/?>', '\n', text, flags=re.IGNORECASE)42 text = re.sub(r'</?(p|div|span|section)[^>]*>', '\n', text, flags=re.IGNORECASE)43 44 text = re.sub(r'<[^>]+>', '', text)45 46 text = re.sub(r'^\|.*\|\s*$', '', text, flags=re.MULTILINE)47 text = re.sub(r'^[\s|:-]+$', '', text, flags=re.MULTILINE)48 49 text = re.sub(r'^#{1,6}\s*', '', text, flags=re.MULTILINE)50 51 text = re.sub(r'\n{3,}', '\n\n', text)52 53 return text.strip()54 55async def search_bank_knowledge(query: str):56 query_embedding = pc.inference.embed(57 model=EMBED_MODEL,58 inputs=[query],59 parameters={"input_type": "query"}60 )61 search_results = index.query(62 vector=query_embedding[0].values,63 top_k=3,64 include_metadata=True65 )66 return "\n".join([res.metadata['original_text'] for res in search_results.matches])67 68TOOLS = [69 {70 "type": "function",71 "function": {72 "name": "search_bank_knowledge",73 "description": "Use this tool to search the official Hadhramout Bank profile for accurate information about services, organizational structure, capital, and policies.",74 "parameters": {75 "type": "object",76 "properties": {77 "query": {78 "type": "string",79 "description": "The search query (e.g., 'What is Hadhramout Bank capital?' or 'individual services')."80 }81 },82 "required": ["query"]83 }84 }85 },86 {87 "type": "function",88 "function": {89 "name": "check_account_balance",90 "description": "Use this tool when the user wants to check their own account balance. The server identifies the current user from request context.",91 "parameters": {92 "type": "object",93 "properties": {},94 "required": []95 }96 }97 },98 {99 "type": "function",100 "function": {101 "name": "prepare_money_transfer",102 "description": "Use this tool when the user wants to transfer money. The server identifies the sender from request context, looks up the receiver by account serial ID, stores a pending transfer, and returns the receiver name for confirmation before any money is sent.",103 "parameters": {104 "type": "object",105 "properties": {106 "receiver_serial_id": {107 "type": "string",108 "description": "The serial ID of the receiver account."109 },110 "amount": {111 "type": "number",112 "description": "Amount to transfer. Ask the user for it if missing."113 }114 },115 "required": ["receiver_serial_id"]116 }117 }118 },119 {120 "type": "function",121 "function": {122 "name": "confirm_money_transfer",123 "description": "Use this tool only after the user confirms that the receiver account is correct and the transfer should proceed.",124 "parameters": {125 "type": "object",126 "properties": {},127 "required": []128 }129 }130 },131 {132 "type": "function",133 "function": {134 "name": "cancel_money_transfer",135 "description": "Use this tool when the user says the receiver is wrong or wants to stop a pending money transfer.",136 "parameters": {137 "type": "object",138 "properties": {},139 "required": []140 }141 }142 },143 {144 "type": "function",145 "function": {146 "name": "get_pending_money_transfer",147 "description": "Use this tool to inspect the current pending transfer for the current user before asking for confirmation or when the user asks about the transfer details.",148 "parameters": {149 "type": "object",150 "properties": {},151 "required": []152 }153 }154 },155 {156 "type": "function",157 "function": {158 "name": "create_illusion_account",159 "description": "Use this tool when the user needs an illusion account created for testing purposes. Requires the user's name.",160 "parameters": {161 "type": "object",162 "properties": {163 "user_name": {164 "type": "string",165 "description": "The name of the user for the illusion account."166 }167 },168 "required": ["user_name"]169 }170 }171 }172]173 174 175async def run_tool(tool_name: str, args: dict, telegram_id: int):176 if tool_name == "search_bank_knowledge":177 return await search_bank_knowledge(args["query"])178 if tool_name == "check_account_balance":179 return json.dumps(get_account_balance(telegram_id), ensure_ascii=False)180 if tool_name == "prepare_money_transfer":181 return json.dumps(182 prepare_transfer(183 telegram_id=telegram_id,184 receiver_serial_id=args["receiver_serial_id"],185 amount=args.get("amount"),186 ),187 ensure_ascii=False,188 )189 if tool_name == "confirm_money_transfer":190 return json.dumps(confirm_transfer(telegram_id), ensure_ascii=False)191 if tool_name == "cancel_money_transfer":192 return json.dumps(cancel_transfer(telegram_id), ensure_ascii=False)193 if tool_name == "get_pending_money_transfer":194 return json.dumps(get_pending_transfer(telegram_id), ensure_ascii=False)195 if tool_name == "create_illusion_account":196 return json.dumps(get_sender_account(telegram_id, args["user_name"]), ensure_ascii=False)197 return json.dumps({"success": False, "message": f"Unknown tool: {tool_name}"}, ensure_ascii=False)198 199async def get_ai_response(user_query: str, telegram_id: int):200 conversation_history = []201 if db_manager:202 raw_history = await db_manager.get_conversation_history(telegram_id, limit=6)203 raw_history.reverse()204 for msg in raw_history:205 if msg.get('message_text'):206 role = "user" if msg['message_type'] == 'user' else "assistant"207 conversation_history.append({"role": role, "content": msg['message_text']})208 209 transfer_instructions = TRANSFER_PROMPT.format(telegram_id=telegram_id)210 messages = [{"role": "system", "content": f"{BASE_PROMPT}\n\n{transfer_instructions}"}] + conversation_history + [{"role": "user", "content": user_query}]211 212 213 import asyncio214 loop = asyncio.get_event_loop()215 216 217 def call_hf(msgs):218 return hf_client.chat.completions.create(219 model=MODEL_NAME,220 messages=msgs,221 tools=TOOLS,222 tool_choice="auto",223 temperature=0.1,224 max_tokens=800225 )226 227 completion = await loop.run_in_executor(None, lambda: call_hf(messages))228 response_message = completion.choices[0].message229 230 for _ in range(4):231 if not response_message.tool_calls:232 break233 234 messages.append(response_message)235 for tool_call in response_message.tool_calls:236 args = json.loads(tool_call.function.arguments or "{}")237 tool_result = await run_tool(tool_call.function.name, args, telegram_id)238 messages.append({239 "role": "tool",240 "tool_call_id": tool_call.id,241 "content": tool_result242 })243 244 completion = await loop.run_in_executor(None, lambda: call_hf(messages))245 response_message = completion.choices[0].message246 247 return clean_ai_response(response_message.content if response_message.content else "")248 