GitHub Action commited on
Commit
f2e1f49
·
0 Parent(s):

Update for HF

Browse files
.codex ADDED
File without changes
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ .env
2
+ __pycache__/
3
+ *.pyc
4
+ .venv/
5
+ venv/
6
+ .python-version
7
+ .DS_Store
8
+ rest.http
=3.0.0 ADDED
File without changes
Dockerfile ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.11 slim image for smaller size
2
+ FROM python:3.11-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Set environment variables for Hugging Face Spaces (can be overridden for local development)
8
+ ENV PYTHONDONTWRITEBYTECODE=1 \
9
+ PYTHONUNBUFFERED=1 \
10
+ PORT=7860 \
11
+ HF_HOME=/app/cache \
12
+ TRANSFORMERS_CACHE=/app/cache
13
+
14
+ # Install system dependencies including curl for health check
15
+ RUN apt-get update && apt-get install -y \
16
+ gcc \
17
+ curl \
18
+ && rm -rf /var/lib/apt/lists/*
19
+
20
+ # Copy requirements first for better caching
21
+ COPY requirements.txt .
22
+
23
+ # Install Python dependencies
24
+ RUN pip install --no-cache-dir --upgrade pip && \
25
+ pip install --no-cache-dir -r requirements.txt
26
+
27
+ # Copy all application code
28
+ COPY . .
29
+
30
+ # Create cache directory for Hugging Face models
31
+ RUN mkdir -p /app/cache && chmod 777 /app/cache
32
+
33
+ # Expose port (default 8000 for local, can be set to 7860 for Hugging Face Spaces)
34
+ EXPOSE ${PORT}
35
+
36
+ # Health check
37
+ HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
38
+ CMD curl -f http://localhost:${PORT}/ || exit 1
39
+
40
+ # Run the application (uses PORT environment variable)
41
+ CMD sh -c "uvicorn main:app --host 0.0.0.0 --port ${PORT}"
README.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Customer Service AI
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ sdk_version: "latest"
8
+ app_file: main.py
9
+ pinned: false
10
+ ---
README_HF.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Customer Service AI
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ sdk_version: "latest"
8
+ app_file: main.py
9
+ pinned: false
10
+ ---
ai_service.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import json
3
+ from config import pc, index, EMBED_MODEL, hf_client, PROMPT, HF_MODEL, TRANSFER_PROMPT
4
+ from database import db_manager
5
+ from 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
+
15
+ MODEL_NAME = HF_MODEL
16
+ BASE_PROMPT = PROMPT or "You are a helpful banking customer service assistant."
17
+ TRANSFER_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
+
35
+ def 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
+
55
+ async 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=True
65
+ )
66
+ return "\n".join([res.metadata['original_text'] for res in search_results.matches])
67
+
68
+ TOOLS = [
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
+
175
+ async 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
+
199
+ async 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 asyncio
214
+ 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=800
225
+ )
226
+
227
+ completion = await loop.run_in_executor(None, lambda: call_hf(messages))
228
+ response_message = completion.choices[0].message
229
+
230
+ for _ in range(4):
231
+ if not response_message.tool_calls:
232
+ break
233
+
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_result
242
+ })
243
+
244
+ completion = await loop.run_in_executor(None, lambda: call_hf(messages))
245
+ response_message = completion.choices[0].message
246
+
247
+ return clean_ai_response(response_message.content if response_message.content else "")
config.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pinecone import Pinecone
3
+ from openai import OpenAI
4
+ from dotenv import load_dotenv
5
+
6
+ # Load environment variables from .env file
7
+ load_dotenv()
8
+
9
+ # Environment Variables
10
+ PINECONE_API_KEY = os.environ.get("PINECONE_API_KEY")
11
+ PINECONE_INDEX = os.environ.get("PINECONE_INDEX", "customerserviceindex")
12
+ HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HF_API_KEY")
13
+ TELEGRAM_TOKEN = os.environ.get("TELEGRAM_TOKEN")
14
+ TELEGRAM_WEBHOOK_SECRET = os.environ.get("TELEGRAM_WEBHOOK_SECRET")
15
+ SUPABASE_URL = os.environ.get("SUPABASE_URL")
16
+ SUPABASE_KEY = os.environ.get("SUPABASE_KEY")
17
+ TELEGRAM_DOMAIN = os.environ.get("TELEGRAM_DOMAIN", "https://api.telegram.org").rstrip("/")
18
+
19
+ # Only create TELEGRAM_URL if token exists
20
+ TELEGRAM_URL = f"{TELEGRAM_DOMAIN}/bot{TELEGRAM_TOKEN}/sendMessage" if TELEGRAM_TOKEN and TELEGRAM_DOMAIN else None
21
+
22
+ EMBED_MODEL = os.environ.get("EMBED_MODEL", "multilingual-e5-large")
23
+ HF_MODEL = os.environ.get(
24
+ "HF_MODEL",
25
+ "dphn/Dolphin-Mistral-24B-Venice-Edition",
26
+ )
27
+ PROMPT = os.environ.get("PROMPT")
28
+ TRANSFER_PROMPT = os.environ.get("TRANSFER_PROMPT")
29
+
30
+ # Initialize clients only if API keys are available
31
+ pc = None
32
+ if PINECONE_API_KEY:
33
+ pc = Pinecone(api_key=PINECONE_API_KEY)
34
+
35
+ hf_client = None
36
+ if HF_TOKEN:
37
+ try:
38
+ hf_client = OpenAI(
39
+ base_url="https://router.huggingface.co/v1",
40
+ api_key=HF_TOKEN,
41
+ )
42
+ except Exception as e:
43
+ print(f"Warning: Failed to initialize Hugging Face OpenAI client: {e}")
44
+ hf_client = None
45
+
46
+ # Initialize index only if Pinecone client is available
47
+ index = None
48
+ if pc:
49
+ index = pc.Index(PINECONE_INDEX)
database.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from datetime import datetime
3
+ from typing import List, Dict, Optional
4
+ from supabase import create_async_client, AsyncClient
5
+ import logging
6
+ from config import SUPABASE_URL, SUPABASE_KEY
7
+
8
+ class DatabaseManager:
9
+ def __init__(self, supabase_url: str = SUPABASE_URL, supabase_key: str = SUPABASE_KEY):
10
+ if not supabase_url or not supabase_key:
11
+ raise ValueError("SUPABASE_URL and SUPABASE_KEY must be set")
12
+ self.supabase_url = supabase_url
13
+ self.supabase_key = supabase_key
14
+ self.supabase: Optional[AsyncClient] = None
15
+ self.logger = logging.getLogger(__name__)
16
+
17
+ async def _ensure_connection(self):
18
+ if self.supabase is None:
19
+ await self.connect()
20
+
21
+ async def connect(self):
22
+ """Initialize the async client"""
23
+ if not self.supabase:
24
+ self.supabase = await create_async_client(self.supabase_url, self.supabase_key)
25
+
26
+ async def create_or_update_user(self, telegram_id: int, username: str = None,
27
+ first_name: str = None, last_name: str = None):
28
+ await self._ensure_connection()
29
+ try:
30
+ existing_user = await self.supabase.table("users").select("id").eq("telegram_id", telegram_id).execute()
31
+
32
+ user_data = {
33
+ "telegram_id": telegram_id,
34
+ "username": username,
35
+ "first_name": first_name,
36
+ "last_name": last_name,
37
+ "updated_at": datetime.utcnow().isoformat()
38
+ }
39
+
40
+ if existing_user.data:
41
+ result = await self.supabase.table("users").update(user_data).eq("telegram_id", telegram_id).execute()
42
+ else:
43
+ user_data["created_at"] = datetime.utcnow().isoformat()
44
+ result = await self.supabase.table("users").insert(user_data).execute()
45
+
46
+ return result.data[0] if result.data else None
47
+ except Exception as e:
48
+ self.logger.error(f"Error creating/updating user: {e}")
49
+ return None
50
+
51
+ async def save_message(self, telegram_id: int, message_text: str, message_type: str):
52
+ await self._ensure_connection()
53
+ try:
54
+ await self.create_or_update_user(telegram_id)
55
+
56
+ message_data = {
57
+ "telegram_id": telegram_id,
58
+ "message_text": message_text,
59
+ "message_type": message_type,
60
+ "created_at": datetime.utcnow().isoformat()
61
+ }
62
+
63
+ result = await self.supabase.table("messages").insert(message_data).execute()
64
+ await self._ensure_active_session(telegram_id)
65
+
66
+ return result.data[0] if result.data else None
67
+ except Exception as e:
68
+ self.logger.error(f"Error saving message: {e}")
69
+ return None
70
+
71
+ async def get_conversation_history(self, telegram_id: int, limit: int = 10) -> List[Dict]:
72
+ await self._ensure_connection()
73
+ try:
74
+ result = await (self.supabase.table("messages")
75
+ .select("message_text, message_type, created_at")
76
+ .eq("telegram_id", telegram_id)
77
+ .order("created_at", desc=True)
78
+ .limit(limit)
79
+ .execute())
80
+ return result.data if result.data else []
81
+ except Exception as e:
82
+ self.logger.error(f"Error getting history: {e}")
83
+ return []
84
+
85
+ async def _ensure_active_session(self, telegram_id: int):
86
+ await self._ensure_connection()
87
+ try:
88
+ active = await (self.supabase.table("conversation_sessions")
89
+ .select("id")
90
+ .eq("telegram_id", telegram_id)
91
+ .is_("session_end", "null")
92
+ .execute())
93
+
94
+ if not active.data:
95
+ session_data = {
96
+ "telegram_id": telegram_id,
97
+ "session_start": datetime.utcnow().isoformat(),
98
+ "created_at": datetime.utcnow().isoformat()
99
+ }
100
+ await self.supabase.table("conversation_sessions").insert(session_data).execute()
101
+ except Exception as e:
102
+ self.logger.error(f"Error ensuring session: {e}")
103
+
104
+ db_manager = DatabaseManager()
main.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Header, Request
2
+ from schemas import AiTestRequest, WebhookData
3
+ from security import validate_webhook_secret
4
+ from telegram_handlers import telegram_webhook
5
+ from utils import dns_test, test_ai_response
6
+
7
+ app = FastAPI()
8
+
9
+ @app.get("/")
10
+ async def root():
11
+ return {"message": "Hadhramout Bank AI Backend is Live"}
12
+
13
+ @app.post("/webhook")
14
+ async def webhook(
15
+ request: Request,
16
+ data: WebhookData,
17
+ x_telegram_bot_api_secret_token: str | None = Header(default=None),
18
+ ):
19
+ print("-------message arive at this time \n")
20
+ validate_webhook_secret(request, x_telegram_bot_api_secret_token)
21
+
22
+ return await telegram_webhook(data)
23
+
24
+ @app.get("/dns-test")
25
+ async def dns(
26
+ request: Request,
27
+ x_telegram_bot_api_secret_token: str | None = Header(default=None),
28
+ ):
29
+ validate_webhook_secret(request, x_telegram_bot_api_secret_token)
30
+ return await dns_test()
31
+
32
+ @app.post("/ai-test")
33
+ async def ai(
34
+ request: Request,
35
+ data: AiTestRequest,
36
+ x_telegram_bot_api_secret_token: str | None = Header(default=None),
37
+ ):
38
+ validate_webhook_secret(request, x_telegram_bot_api_secret_token)
39
+ return await test_ai_response(data.message, data.telegram_id)
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ pinecone
4
+ httpx
5
+ python-dotenv
6
+ supabase
7
+ openai
rest.http ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ POST http://localhost:8000/ai-test HTTP/1.1
2
+ Content-Type: application/json
3
+ X-Telegram-Bot-Api-Secret-Token: {{$dotenv TELEGRAM_WEBHOOK_SECRET}}
4
+
5
+ {
6
+ "message": "confirm then tell me how much that he has in his account",
7
+ "telegram_id": 12
8
+ }
9
+ ###
10
+ GET https://codeboker-customer-service.hf.space/ai-test HTTP/1.1
11
+
12
+ ###
13
+ POST https://codeboker-customer-service.hf.space/webhook HTTP/1.1
14
+ Content-Type: application/json
15
+ X-Telegram-Bot-Api-Secret-Token: {{$dotenv TELEGRAM_WEBHOOK_SECRET}}
16
+
17
+ {
18
+ "message": {
19
+ "chat": {"id": 123},
20
+ "text": "السلام عليكم وؤحمة الله اريد ان استعلم عنلاالخدمات الالكترونية"
21
+ }
22
+ }
schemas.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class ChatInfo(BaseModel):
5
+ id: int
6
+ username: str = None
7
+ first_name: str = None
8
+ last_name: str = None
9
+
10
+
11
+ class Message(BaseModel):
12
+ chat: ChatInfo
13
+ text: str = ""
14
+
15
+
16
+ class WebhookData(BaseModel):
17
+ message: Message = None
18
+
19
+
20
+ class AiTestRequest(BaseModel):
21
+ message: str
22
+ telegram_id: int = 12
security.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import defaultdict, deque
2
+ from time import monotonic
3
+
4
+ from fastapi import HTTPException, Request
5
+
6
+ from config import TELEGRAM_WEBHOOK_SECRET
7
+
8
+ FAILED_ATTEMPT_WINDOW_SECONDS = 60
9
+ FAILED_ATTEMPT_LIMIT = 5
10
+ BLOCK_DURATION_SECONDS = 15 * 60
11
+
12
+ _failed_secret_attempts: dict[str, deque[float]] = defaultdict(deque)
13
+ _blocked_clients: dict[str, float] = {}
14
+
15
+
16
+ def _get_client_key(request: Request) -> str:
17
+ forwarded_for = request.headers.get("x-forwarded-for")
18
+ if forwarded_for:
19
+ return forwarded_for.split(",")[0].strip()
20
+
21
+ if request.client and request.client.host:
22
+ return request.client.host
23
+
24
+ return "unknown"
25
+
26
+
27
+ def _prune_failed_attempts(client_key: str, now: float) -> deque[float]:
28
+ attempts = _failed_secret_attempts[client_key]
29
+ cutoff = now - FAILED_ATTEMPT_WINDOW_SECONDS
30
+ while attempts and attempts[0] < cutoff:
31
+ attempts.popleft()
32
+ return attempts
33
+
34
+
35
+ def validate_webhook_secret(request: Request, secret_header: str | None) -> None:
36
+ if not TELEGRAM_WEBHOOK_SECRET:
37
+ raise HTTPException(status_code=500, detail="Webhook secret is not configured")
38
+
39
+ client_key = _get_client_key(request)
40
+ now = monotonic()
41
+ blocked_until = _blocked_clients.get(client_key)
42
+ if blocked_until and now < blocked_until:
43
+ raise HTTPException(status_code=429, detail="Too many requests")
44
+
45
+ if secret_header != TELEGRAM_WEBHOOK_SECRET:
46
+ attempts = _prune_failed_attempts(client_key, now)
47
+ attempts.append(now)
48
+ if len(attempts) >= FAILED_ATTEMPT_LIMIT:
49
+ _blocked_clients[client_key] = now + BLOCK_DURATION_SECONDS
50
+ attempts.clear()
51
+ raise HTTPException(status_code=403, detail="Forbidden")
52
+
53
+ _blocked_clients.pop(client_key, None)
54
+ _failed_secret_attempts.pop(client_key, None)
supabase_schema.sql ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Supabase Database Schema for Conversation History
2
+ -- Run this in your Supabase SQL Editor
3
+
4
+ -- Enable UUID extension
5
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
6
+
7
+ -- Users table to store user information
8
+ CREATE TABLE IF NOT EXISTS users (
9
+ id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
10
+ telegram_id BIGINT UNIQUE NOT NULL,
11
+ username VARCHAR(255),
12
+ first_name VARCHAR(255),
13
+ last_name VARCHAR(255),
14
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
15
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
16
+ );
17
+
18
+ -- Messages table to store conversation history
19
+ CREATE TABLE IF NOT EXISTS messages (
20
+ id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
21
+ telegram_id BIGINT NOT NULL,
22
+ message_text TEXT NOT NULL,
23
+ message_type VARCHAR(20) NOT NULL CHECK (message_type IN ('user', 'assistant')),
24
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
25
+ FOREIGN KEY (telegram_id) REFERENCES users(telegram_id) ON DELETE CASCADE
26
+ );
27
+
28
+ -- Conversation sessions to group messages by session
29
+ CREATE TABLE IF NOT EXISTS conversation_sessions (
30
+ id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
31
+ telegram_id BIGINT NOT NULL,
32
+ session_start TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
33
+ session_end TIMESTAMP WITH TIME ZONE,
34
+ message_count INTEGER DEFAULT 0,
35
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
36
+ FOREIGN KEY (telegram_id) REFERENCES users(telegram_id) ON DELETE CASCADE
37
+ );
38
+
39
+ -- Indexes for better performance
40
+ CREATE INDEX IF NOT EXISTS idx_messages_telegram_id_created_at ON messages(telegram_id, created_at);
41
+ CREATE INDEX IF NOT EXISTS idx_users_telegram_id ON users(telegram_id);
42
+ CREATE INDEX IF NOT EXISTS idx_sessions_telegram_id ON conversation_sessions(telegram_id);
43
+
44
+ -- Function to update updated_at timestamp
45
+ CREATE OR REPLACE FUNCTION update_updated_at_column()
46
+ RETURNS TRIGGER AS $$
47
+ BEGIN
48
+ NEW.updated_at = NOW();
49
+ RETURN NEW;
50
+ END;
51
+ $$ language 'plpgsql';
52
+
53
+ -- Trigger to update user's updated_at timestamp
54
+ CREATE TRIGGER update_user_updated_at
55
+ BEFORE UPDATE ON users
56
+ FOR EACH ROW
57
+ EXECUTE FUNCTION update_updated_at_column();
58
+
59
+ -- Trigger to update session message count
60
+ CREATE OR REPLACE FUNCTION update_session_count()
61
+ RETURNS TRIGGER AS $$
62
+ BEGIN
63
+ UPDATE conversation_sessions
64
+ SET message_count = message_count + 1
65
+ WHERE telegram_id = NEW.telegram_id AND session_end IS NULL;
66
+ RETURN NEW;
67
+ END;
68
+ $$ language 'plpgsql';
69
+
70
+ CREATE TRIGGER update_session_message_count
71
+ AFTER INSERT ON messages
72
+ FOR EACH ROW
73
+ EXECUTE FUNCTION update_session_count();
74
+
75
+ -- Row Level Security (RLS) policies
76
+ ALTER TABLE users ENABLE ROW LEVEL SECURITY;
77
+ ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
78
+ ALTER TABLE conversation_sessions ENABLE ROW LEVEL SECURITY;
79
+
80
+ -- Policy for users table (allow all operations for now)
81
+ CREATE POLICY "Enable all operations for users" ON users
82
+ FOR ALL USING (true);
83
+
84
+ -- Policy for messages table (allow all operations for now)
85
+ CREATE POLICY "Enable all operations for messages" ON messages
86
+ FOR ALL USING (true);
87
+
88
+ -- Policy for conversation_sessions table (allow all operations for now)
89
+ CREATE POLICY "Enable all operations for conversation_sessions" ON conversation_sessions
90
+ FOR ALL USING (true);
telegram_handlers.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import json
3
+ import html
4
+ import re
5
+ import asyncio
6
+ from config import TELEGRAM_URL, TELEGRAM_TOKEN
7
+ from ai_service import get_ai_response
8
+ from database import db_manager
9
+ from schemas import WebhookData
10
+
11
+ TELEGRAM_IP = "149.154.167.220"
12
+ MAX_TELEGRAM_MESSAGE_LENGTH = 4096
13
+
14
+ def _sanitize_telegram_text(text: str) -> str:
15
+ if text is None:
16
+ return ""
17
+ normalized = str(text).replace("\r\n", "\n").replace("\r", "\n")
18
+ cleaned = "".join(
19
+ ch
20
+ for ch in normalized
21
+ if (ch in ("\n", "\t") or ord(ch) >= 32) and not (0xD800 <= ord(ch) <= 0xDFFF)
22
+ )
23
+ return cleaned.strip()
24
+
25
+ def _format_telegram_message(text: str) -> str:
26
+ if not text:
27
+ return text
28
+ escaped = html.escape(text, quote=False)
29
+ formatted = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', escaped)
30
+ return formatted
31
+
32
+ async def telegram_webhook(data: WebhookData):
33
+ try:
34
+ if not data.message or not data.message.text:
35
+ return {"status": "ok"}
36
+
37
+ telegram_id = data.message.chat.id
38
+ user_text = data.message.text
39
+ username = data.message.chat.username
40
+ first_name = data.message.chat.first_name
41
+
42
+ if db_manager:
43
+ await db_manager.create_or_update_user(telegram_id, username, first_name, data.message.chat.last_name)
44
+
45
+ if TELEGRAM_TOKEN:
46
+ async with httpx.AsyncClient(timeout=40.0, verify=False) as client:
47
+ action_payload = {
48
+ "chat_id": telegram_id,
49
+ "action": "typing",
50
+ }
51
+ action_url = f"https://{TELEGRAM_IP}/bot{TELEGRAM_TOKEN}/sendChatAction"
52
+ headers = {
53
+ "Host": "api.telegram.org",
54
+ "Content-Type": "application/json; charset=utf-8",
55
+ }
56
+ try:
57
+ await client.post(action_url, json=action_payload, headers=headers)
58
+ except Exception:
59
+ pass
60
+
61
+ ai_answer = await get_ai_response(user_text, telegram_id)
62
+ final_response = ai_answer or "Sorry, I couldn't generate a response."
63
+
64
+ prepared_text = _sanitize_telegram_text(final_response)
65
+ formatted_text = _format_telegram_message(prepared_text)
66
+ final_text = formatted_text[:MAX_TELEGRAM_MESSAGE_LENGTH]
67
+
68
+ payload = {
69
+ "chat_id": telegram_id,
70
+ "text": final_text if final_text.strip() else ".",
71
+ "parse_mode": "HTML",
72
+ }
73
+
74
+ forced_ip_url = f"https://{TELEGRAM_IP}/bot{TELEGRAM_TOKEN}/sendMessage"
75
+ headers = {
76
+ "Host": "api.telegram.org",
77
+ "Content-Type": "application/json; charset=utf-8",
78
+ }
79
+
80
+ response = await client.post(forced_ip_url, json=payload, headers=headers)
81
+
82
+ if response.status_code == 200:
83
+ print("--- Success: Telegram message delivered ---")
84
+ if db_manager:
85
+ await asyncio.gather(
86
+ db_manager.save_message(telegram_id, user_text, "user"),
87
+ db_manager.save_message(telegram_id, final_response, "assistant")
88
+ )
89
+ else:
90
+ print(f"--- Telegram Rejected: {response.status_code} - {response.text} ---")
91
+
92
+ return {"status": "ok"}
93
+ except Exception as e:
94
+ print(f"Error in webhook: {str(e)}")
95
+ return {"status": "error", "message": str(e)}
tests/test_telegram_formatting.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+ import html
3
+ import re
4
+ from telegram_handlers import _format_telegram_message
5
+
6
+
7
+ class TestTelegramFormatting(unittest.TestCase):
8
+
9
+ def test_bold_conversion(self):
10
+ """Test that **text** is converted to <b>text</b>."""
11
+ input_text = "This is **bold** text."
12
+ expected = "This is <b>bold</b> text."
13
+ result = _format_telegram_message(input_text)
14
+ self.assertEqual(result, expected)
15
+
16
+ def test_multiple_bold(self):
17
+ """Test multiple bold sections."""
18
+ input_text = "**First** and **second** bold."
19
+ expected = "<b>First</b> and <b>second</b> bold."
20
+ result = _format_telegram_message(input_text)
21
+ self.assertEqual(result, expected)
22
+
23
+ def test_html_escaping(self):
24
+ """Test that HTML characters are escaped."""
25
+ input_text = "Text with <script> and **bold**."
26
+ expected = "Text with &lt;script&gt; and <b>bold</b>."
27
+ result = _format_telegram_message(input_text)
28
+ self.assertEqual(result, expected)
29
+
30
+ def test_no_bold(self):
31
+ """Test text without bold markers."""
32
+ input_text = "Plain text without formatting."
33
+ expected = "Plain text without formatting."
34
+ result = _format_telegram_message(input_text)
35
+ self.assertEqual(result, expected)
36
+
37
+ def test_empty_string(self):
38
+ """Test empty string."""
39
+ input_text = ""
40
+ expected = ""
41
+ result = _format_telegram_message(input_text)
42
+ self.assertEqual(result, expected)
43
+
44
+ def test_nested_asterisks(self):
45
+ """Test that single * are not converted."""
46
+ input_text = "Italic *text* but **bold**."
47
+ expected = "Italic *text* but <b>bold</b>."
48
+ result = _format_telegram_message(input_text)
49
+ self.assertEqual(result, expected)
50
+
51
+
52
+ if __name__ == '__main__':
53
+ unittest.main()
transfers/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from .service import (
2
+ cancel_transfer,
3
+ confirm_transfer,
4
+ get_account_balance,
5
+ get_pending_transfer,
6
+ get_sender_account,
7
+ prepare_transfer,
8
+ )
9
+
transfers/mock_bank_accounts.json ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "accounts": [
3
+ {
4
+ "telegram_id": 12,
5
+ "account_id": "ACC-00012",
6
+ "serial_id": "1001",
7
+ "name": "Test Sender",
8
+ "balance": 1900.0,
9
+ "currency": "YER",
10
+ "is_real": true
11
+ },
12
+ {
13
+ "telegram_id": 991001,
14
+ "account_id": "ACC-991001",
15
+ "serial_id": "2001",
16
+ "name": "Ahmed Salem",
17
+ "balance": 1990.0,
18
+ "currency": "YER",
19
+ "is_real": true
20
+ },
21
+ {
22
+ "telegram_id": 991002,
23
+ "account_id": "ACC-991002",
24
+ "serial_id": "2002",
25
+ "name": "Mona Ali",
26
+ "balance": 800.0,
27
+ "currency": "YER",
28
+ "is_real": true
29
+ },
30
+ {
31
+ "telegram_id": 991003,
32
+ "account_id": "ACC-991003",
33
+ "serial_id": "2003",
34
+ "name": "Khaled Omar",
35
+ "balance": 300.0,
36
+ "currency": "YER",
37
+ "is_real": true
38
+ },
39
+ {
40
+ "telegram_id": 34,
41
+ "account_id": "ACC-34",
42
+ "serial_id": "9034",
43
+ "name": "Test User Ali",
44
+ "balance": 1810.0,
45
+ "currency": "YER",
46
+ "is_real": false
47
+ }
48
+ ],
49
+ "transactions": [
50
+ {
51
+ "transaction_id": "TX-20260410193334671954",
52
+ "telegram_id": 12,
53
+ "sender_serial_id": "1001",
54
+ "receiver_serial_id": "2001",
55
+ "receiver_name": "Ahmed Salem",
56
+ "amount": 200.0,
57
+ "currency": "YER",
58
+ "created_at": "2026-04-10T19:33:34.671995"
59
+ },
60
+ {
61
+ "transaction_id": "TX-20260410193344247493",
62
+ "telegram_id": 12,
63
+ "sender_serial_id": "1001",
64
+ "receiver_serial_id": "2001",
65
+ "receiver_name": "Ahmed Salem",
66
+ "amount": 200.0,
67
+ "currency": "YER",
68
+ "created_at": "2026-04-10T19:33:44.247529"
69
+ },
70
+ {
71
+ "transaction_id": "TX-20260410193354495302",
72
+ "telegram_id": 12,
73
+ "sender_serial_id": "1001",
74
+ "receiver_serial_id": "2001",
75
+ "receiver_name": "Ahmed Salem",
76
+ "amount": 200.0,
77
+ "currency": "YER",
78
+ "created_at": "2026-04-10T19:33:54.495350"
79
+ },
80
+ {
81
+ "transaction_id": "TX-20260420140247475948",
82
+ "telegram_id": 34,
83
+ "sender_serial_id": "9034",
84
+ "receiver_serial_id": "2001",
85
+ "receiver_name": "Ahmed Salem",
86
+ "amount": 190.0,
87
+ "currency": "YER",
88
+ "created_at": "2026-04-20T14:02:47.475998"
89
+ }
90
+ ]
91
+ }
transfers/pending_transfers.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "12": {
3
+ "telegram_id": 12,
4
+ "sender_name": "Test Sender",
5
+ "sender_serial_id": "1001",
6
+ "receiver_name": "Ahmed Salem",
7
+ "receiver_serial_id": "2001",
8
+ "receiver_account_id": "ACC-991001",
9
+ "amount": 200,
10
+ "currency": "YER",
11
+ "created_at": "2026-04-11T05:28:56.362317"
12
+ }
13
+ }
transfers/service.py ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+ from threading import Lock
5
+ from typing import Any, Dict, Optional
6
+
7
+
8
+ DATA_FILE = Path(__file__).with_name("mock_bank_accounts.json")
9
+ PENDING_FILE = Path(__file__).with_name("pending_transfers.json")
10
+ _LOCK = Lock()
11
+
12
+
13
+ def _read_json(path: Path, default: Any) -> Any:
14
+ if not path.exists():
15
+ return default
16
+
17
+ with path.open("r", encoding="utf-8") as file:
18
+ return json.load(file)
19
+
20
+
21
+ def _write_json(path: Path, data: Any) -> None:
22
+ with path.open("w", encoding="utf-8") as file:
23
+ json.dump(data, file, ensure_ascii=True, indent=2)
24
+
25
+
26
+ def _load_bank_data() -> Dict[str, Any]:
27
+ return _read_json(DATA_FILE, {"accounts": [], "transactions": []})
28
+
29
+
30
+ def _save_bank_data(data: Dict[str, Any]) -> None:
31
+ _write_json(DATA_FILE, data)
32
+
33
+
34
+ def _load_pending_transfers() -> Dict[str, Any]:
35
+ return _read_json(PENDING_FILE, {})
36
+
37
+
38
+ def _save_pending_transfers(data: Dict[str, Any]) -> None:
39
+ _write_json(PENDING_FILE, data)
40
+
41
+
42
+ def _find_account_by_telegram_id(accounts: list[Dict[str, Any]], telegram_id: int) -> Optional[Dict[str, Any]]:
43
+ return next((account for account in accounts if account.get("telegram_id") == telegram_id), None)
44
+
45
+
46
+ def _find_account_by_serial_id(accounts: list[Dict[str, Any]], serial_id: str) -> Optional[Dict[str, Any]]:
47
+ normalized_id = str(serial_id).strip()
48
+ return next((account for account in accounts if str(account.get("serial_id")) == normalized_id), None)
49
+
50
+
51
+ def get_sender_account(telegram_id: int, user_name: str = None) -> Dict[str, Any]:
52
+ with _LOCK:
53
+ data = _load_bank_data()
54
+ sender = _find_account_by_telegram_id(data["accounts"], telegram_id)
55
+ if sender:
56
+ is_real = sender.get("is_real", True)
57
+ is_illusion = not is_real or sender.get("name", "").startswith("Test User")
58
+ return {
59
+ "success": True,
60
+ "account": sender,
61
+ "is_real": is_real,
62
+ "is_illusion": is_illusion,
63
+ }
64
+
65
+ # Ask for user name before creating illusion account
66
+ if not user_name:
67
+ return {
68
+ "success": False,
69
+ "need_user_name": True,
70
+ "message": "I need to create an illusion account for testing purposes. What is your name?",
71
+ }
72
+
73
+ # Create illusion account for testing purposes
74
+ new_account = {
75
+ "telegram_id": telegram_id,
76
+ "account_id": f"ACC-{telegram_id}",
77
+ "serial_id": str(9000 + telegram_id % 1000),
78
+ "name": f"Test User {user_name}",
79
+ "balance": 2000.0,
80
+ "currency": "YER",
81
+ "is_real": False
82
+ }
83
+
84
+ data["accounts"].append(new_account)
85
+ _save_bank_data(data)
86
+
87
+ return {
88
+ "success": True,
89
+ "account": new_account,
90
+ "is_real": False,
91
+ "is_illusion": True,
92
+ "message": f"I've created an illusion account for {user_name} with 2000 YER balance to try sending money. This is for testing purposes only."
93
+ }
94
+
95
+
96
+ def get_account_balance(telegram_id: int) -> Dict[str, Any]:
97
+ sender_result = get_sender_account(telegram_id)
98
+ if not sender_result["success"]:
99
+ return sender_result
100
+
101
+ account = sender_result["account"]
102
+ result = {
103
+ "success": True,
104
+ "telegram_id": telegram_id,
105
+ "account_id": account["account_id"],
106
+ "serial_id": account["serial_id"],
107
+ "name": account["name"],
108
+ "balance": account.get("balance", 0.0),
109
+ "currency": account.get("currency", "YER"),
110
+ "is_real": sender_result.get("is_real", True),
111
+ "is_illusion": sender_result.get("is_illusion", False),
112
+ }
113
+
114
+ if result["is_illusion"]:
115
+ result["message"] = "This is an illusion account created for testing purposes with 2000 YER balance."
116
+
117
+ return result
118
+
119
+
120
+ def get_receiver_account_name(receiver_serial_id: str) -> Dict[str, Any]:
121
+ with _LOCK:
122
+ data = _load_bank_data()
123
+ receiver = _find_account_by_serial_id(data["accounts"], receiver_serial_id)
124
+
125
+ if not receiver:
126
+ return {
127
+ "success": False,
128
+ "message": f"No account was found for ID {receiver_serial_id}.",
129
+ }
130
+
131
+ return {
132
+ "success": True,
133
+ "serial_id": receiver["serial_id"],
134
+ "name": receiver["name"],
135
+ "account_id": receiver["account_id"],
136
+ "currency": receiver.get("currency", "YER"),
137
+ }
138
+
139
+
140
+ def prepare_transfer(telegram_id: int, receiver_serial_id: str, amount: Optional[float] = None) -> Dict[str, Any]:
141
+ with _LOCK:
142
+ data = _load_bank_data()
143
+ sender = _find_account_by_telegram_id(data["accounts"], telegram_id)
144
+ if not sender:
145
+ return {
146
+ "success": False,
147
+ "message": "You do not have an account in the bank system. Please visit the bank to create an account first.",
148
+ }
149
+
150
+ receiver = _find_account_by_serial_id(data["accounts"], receiver_serial_id)
151
+ if not receiver:
152
+ return {
153
+ "success": False,
154
+ "message": f"No account was found for ID {receiver_serial_id}.",
155
+ }
156
+
157
+ if receiver["serial_id"] == sender["serial_id"]:
158
+ return {
159
+ "success": False,
160
+ "message": "You cannot transfer money to the same account.",
161
+ }
162
+
163
+ pending_transfers = _load_pending_transfers()
164
+ pending_payload = {
165
+ "telegram_id": telegram_id,
166
+ "sender_name": sender["name"],
167
+ "sender_serial_id": sender["serial_id"],
168
+ "receiver_name": receiver["name"],
169
+ "receiver_serial_id": receiver["serial_id"],
170
+ "receiver_account_id": receiver["account_id"],
171
+ "amount": amount,
172
+ "currency": sender.get("currency", "YER"),
173
+ "created_at": datetime.utcnow().isoformat(),
174
+ }
175
+ pending_transfers[str(telegram_id)] = pending_payload
176
+ _save_pending_transfers(pending_transfers)
177
+
178
+ return {
179
+ "success": True,
180
+ "pending_transfer": pending_payload,
181
+ "message": f"Receiver found: {receiver['name']}. Waiting for user confirmation.",
182
+ }
183
+
184
+
185
+ def get_pending_transfer(telegram_id: int) -> Dict[str, Any]:
186
+ with _LOCK:
187
+ pending_transfers = _load_pending_transfers()
188
+ pending_transfer = pending_transfers.get(str(telegram_id))
189
+
190
+ if not pending_transfer:
191
+ return {
192
+ "success": False,
193
+ "message": "No pending transfer was found for this Telegram user.",
194
+ }
195
+
196
+ return {
197
+ "success": True,
198
+ "pending_transfer": pending_transfer,
199
+ }
200
+
201
+
202
+ def confirm_transfer(telegram_id: int) -> Dict[str, Any]:
203
+ with _LOCK:
204
+ data = _load_bank_data()
205
+ pending_transfers = _load_pending_transfers()
206
+ pending_transfer = pending_transfers.get(str(telegram_id))
207
+
208
+ if not pending_transfer:
209
+ return {
210
+ "success": False,
211
+ "message": "There is no pending transfer to confirm.",
212
+ }
213
+
214
+ sender = _find_account_by_telegram_id(data["accounts"], telegram_id)
215
+ receiver = _find_account_by_serial_id(data["accounts"], pending_transfer["receiver_serial_id"])
216
+
217
+ if not sender or not receiver:
218
+ return {
219
+ "success": False,
220
+ "message": "The sender or receiver account could not be found.",
221
+ }
222
+
223
+ amount = pending_transfer.get("amount")
224
+ if amount is None:
225
+ return {
226
+ "success": False,
227
+ "message": "The transfer amount is missing. Ask the user for the amount before confirming.",
228
+ }
229
+
230
+ try:
231
+ amount_value = float(amount)
232
+ except (TypeError, ValueError):
233
+ return {
234
+ "success": False,
235
+ "message": "The transfer amount is invalid.",
236
+ }
237
+
238
+ if amount_value <= 0:
239
+ return {
240
+ "success": False,
241
+ "message": "The transfer amount must be greater than zero.",
242
+ }
243
+
244
+ if float(sender.get("balance", 0.0)) < amount_value:
245
+ return {
246
+ "success": False,
247
+ "message": f"Insufficient balance. Available balance is {sender.get('balance', 0.0):.2f} {sender.get('currency', 'YER')}.",
248
+ }
249
+
250
+ sender["balance"] = round(float(sender["balance"]) - amount_value, 2)
251
+ receiver["balance"] = round(float(receiver.get("balance", 0.0)) + amount_value, 2)
252
+
253
+ transaction = {
254
+ "transaction_id": f"TX-{datetime.utcnow().strftime('%Y%m%d%H%M%S%f')}",
255
+ "telegram_id": telegram_id,
256
+ "sender_serial_id": sender["serial_id"],
257
+ "receiver_serial_id": receiver["serial_id"],
258
+ "receiver_name": receiver["name"],
259
+ "amount": amount_value,
260
+ "currency": sender.get("currency", "YER"),
261
+ "created_at": datetime.utcnow().isoformat(),
262
+ }
263
+ data.setdefault("transactions", []).append(transaction)
264
+ _save_bank_data(data)
265
+
266
+ pending_transfers.pop(str(telegram_id), None)
267
+ _save_pending_transfers(pending_transfers)
268
+
269
+ # Check if sender is an illusion account using is_real variable
270
+ is_real = sender.get("is_real", True)
271
+ is_illusion = not is_real
272
+
273
+ message = f"Transfer completed successfully to {receiver['name']}."
274
+ if is_illusion:
275
+ message += " ⚠️ Please note: This process was not real - it was for testing purposes only. No actual money was transferred."
276
+
277
+ return {
278
+ "success": True,
279
+ "transaction": transaction,
280
+ "sender_balance": sender["balance"],
281
+ "is_real": is_real,
282
+ "is_illusion": is_illusion,
283
+ "message": message,
284
+ }
285
+
286
+
287
+ def cancel_transfer(telegram_id: int) -> Dict[str, Any]:
288
+ with _LOCK:
289
+ pending_transfers = _load_pending_transfers()
290
+ removed = pending_transfers.pop(str(telegram_id), None)
291
+ _save_pending_transfers(pending_transfers)
292
+
293
+ if not removed:
294
+ return {
295
+ "success": False,
296
+ "message": "There is no pending transfer to cancel.",
297
+ }
298
+
299
+ return {
300
+ "success": True,
301
+ "message": "The pending transfer has been canceled.",
302
+ }
303
+
utils.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import socket
2
+
3
+ async def dns_test():
4
+ try:
5
+ ip = socket.gethostbyname("api.telegram.org")
6
+ return {"resolved_ip": ip}
7
+ except Exception as e:
8
+ return {"error": str(e)}
9
+
10
+ async def test_ai_response(message: str, telegram_id: int):
11
+ try:
12
+ from ai_service import get_ai_response
13
+ response = await get_ai_response(message, telegram_id)
14
+ return {
15
+ "message": message,
16
+ "telegram_id": telegram_id,
17
+ "response": response,
18
+ }
19
+ except Exception as e:
20
+ return {"error": str(e)}