.codex → .env RENAMED
File without changes
.gitignore DELETED
@@ -1,8 +0,0 @@
1
- .env
2
- __pycache__/
3
- *.pyc
4
- .venv/
5
- venv/
6
- .python-version
7
- .DS_Store
8
- rest.http
 
 
 
 
 
 
 
 
 
=3.0.0 DELETED
File without changes
Dockerfile CHANGED
@@ -4,17 +4,14 @@ FROM python:3.11-slim
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
@@ -24,18 +21,20 @@ COPY requirements.txt .
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}"
 
4
  # Set working directory
5
  WORKDIR /app
6
 
7
+ # Set environment variables
8
  ENV PYTHONDONTWRITEBYTECODE=1 \
9
  PYTHONUNBUFFERED=1 \
10
+ PORT=7860
 
 
11
 
12
+ # Install system dependencies
13
  RUN apt-get update && apt-get install -y \
14
  gcc \
 
15
  && rm -rf /var/lib/apt/lists/*
16
 
17
  # Copy requirements first for better caching
 
21
  RUN pip install --no-cache-dir --upgrade pip && \
22
  pip install --no-cache-dir -r requirements.txt
23
 
24
+ # Copy application code
25
+ COPY app.py .
26
 
27
+ # Create non-root user for security
28
+ RUN useradd --create-home --shell /bin/bash app && \
29
+ chown -R app:app /app
30
+ USER app
31
 
32
+ # Expose port
33
+ EXPOSE 7860
34
 
35
  # Health check
36
  HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
37
+ CMD curl -f http://localhost:7860/ || exit 1
38
 
39
+ # Run the application
40
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,36 +1,14 @@
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
  ---
 
11
 
12
- # Customer Service AI Backend
13
-
14
- FastAPI backend for Hadhramout Bank AI customer service system.
15
 
16
- ## Features
17
- - Telegram webhook integration
18
- - AI-powered responses using Hugging Face Inference API
19
- - Database integration with Supabase
20
- - Vector search with Pinecone
21
-
22
- ## API Endpoints
23
- - `GET /` - Health check
24
- - `POST /webhook` - Telegram webhook endpoint
25
- - `POST /test` - Test webhook functionality
26
- - `GET /dns-test` - DNS connectivity test
27
- - `GET /ai-test` - AI response test
28
-
29
- ## Deployment
30
- This application is deployed on Hugging Face Spaces using Docker.
31
 
32
- ## Local Development
33
- ```bash
34
- docker build -t customer-service .
35
- docker run -p 8000:8000 customer-service
36
- ```
 
1
  ---
2
+ title: customer_service
 
 
 
3
  sdk: docker
4
+ emoji: 🚀
5
+ colorFrom: blue
6
+ colorTo: blue
7
+ app_file: app.py
8
  ---
9
+ # customer_service
10
 
 
 
 
11
 
12
+ ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
ai_service.py DELETED
@@ -1,244 +0,0 @@
1
- import re
2
- import json
3
- from config import pc, index, EMBED_MODEL, hf_client, PROMPT, HF_MODEL
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
-
18
- def clean_ai_response(text: str):
19
- print("arriving to clean function: \n")
20
- if not text: return ""
21
- text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)
22
- text = re.sub(r'<br\s*/?>', '\n', text)
23
- text = re.sub(r'<[^>]+>', '', text)
24
- text = re.sub(r'^\|.*\|\s*$', '', text, flags=re.MULTILINE)
25
- text = re.sub(r'^[\s|:-]+$', '', text, flags=re.MULTILINE)
26
- text = re.sub(r'^#{1,6}\s*', '', text, flags=re.MULTILINE)
27
- text = re.sub(r'\n{3,}', '\n\n', text)
28
- return text.strip()
29
-
30
- async def search_bank_knowledge(query: str):
31
-
32
- print("arriving to start embed function: \n")
33
- query_embedding = pc.inference.embed(
34
- model=EMBED_MODEL,
35
- inputs=[query],
36
- parameters={"input_type": "query"}
37
- )
38
- print("arriving to index query function: \n")
39
- search_results = index.query(
40
- vector=query_embedding[0].values,
41
- top_k=3,
42
- include_metadata=True
43
- )
44
-
45
- return "\n".join([res.metadata['original_text'] for res in search_results.matches])
46
-
47
- TOOLS = [
48
- {
49
- "type": "function",
50
- "function": {
51
- "name": "search_bank_knowledge",
52
- "description": "Use this tool to search the official Hadhramout Bank profile for accurate information about services, organizational structure, capital, and policies.",
53
- "parameters": {
54
- "type": "object",
55
- "properties": {
56
- "query": {
57
- "type": "string",
58
- "description": "The search query (e.g., 'What is Hadhramout Bank capital?' or 'individual services')."
59
- }
60
- },
61
- "required": ["query"]
62
- }
63
- }
64
- },
65
- {
66
- "type": "function",
67
- "function": {
68
- "name": "check_account_balance",
69
- "description": "Use this tool when the user wants to check their own account balance. The server identifies the current user from request context.",
70
- "parameters": {
71
- "type": "object",
72
- "properties": {},
73
- "required": []
74
- }
75
- }
76
- },
77
- {
78
- "type": "function",
79
- "function": {
80
- "name": "prepare_money_transfer",
81
- "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.",
82
- "parameters": {
83
- "type": "object",
84
- "properties": {
85
- "receiver_serial_id": {
86
- "type": "string",
87
- "description": "The serial ID of the receiver account."
88
- },
89
- "amount": {
90
- "type": "number",
91
- "description": "Amount to transfer. Ask the user for it if missing."
92
- }
93
- },
94
- "required": ["receiver_serial_id"]
95
- }
96
- }
97
- },
98
- {
99
- "type": "function",
100
- "function": {
101
- "name": "confirm_money_transfer",
102
- "description": "Use this tool only after the user confirms that the receiver account is correct and the transfer should proceed.",
103
- "parameters": {
104
- "type": "object",
105
- "properties": {},
106
- "required": []
107
- }
108
- }
109
- },
110
- {
111
- "type": "function",
112
- "function": {
113
- "name": "cancel_money_transfer",
114
- "description": "Use this tool when the user says the receiver is wrong or wants to stop a pending money transfer.",
115
- "parameters": {
116
- "type": "object",
117
- "properties": {},
118
- "required": []
119
- }
120
- }
121
- },
122
- {
123
- "type": "function",
124
- "function": {
125
- "name": "get_pending_money_transfer",
126
- "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.",
127
- "parameters": {
128
- "type": "object",
129
- "properties": {},
130
- "required": []
131
- }
132
- }
133
- },
134
- {
135
- "type": "function",
136
- "function": {
137
- "name": "create_illusion_account",
138
- "description": "Use this tool when the user needs an illusion account created for testing purposes. Requires the user's name.",
139
- "parameters": {
140
- "type": "object",
141
- "properties": {
142
- "user_name": {
143
- "type": "string",
144
- "description": "The name of the user for the illusion account."
145
- }
146
- },
147
- "required": ["user_name"]
148
- }
149
- }
150
- }
151
- ]
152
-
153
-
154
- async def run_tool(tool_name: str, args: dict, telegram_id: int):
155
- print("arriving to calling tools: \n")
156
- if tool_name == "search_bank_knowledge":
157
- return await search_bank_knowledge(args["query"])
158
- if tool_name == "check_account_balance":
159
- return json.dumps(get_account_balance(telegram_id), ensure_ascii=False)
160
- if tool_name == "prepare_money_transfer":
161
- return json.dumps(
162
- prepare_transfer(
163
- telegram_id=telegram_id,
164
- receiver_serial_id=args["receiver_serial_id"],
165
- amount=args.get("amount"),
166
- ),
167
- ensure_ascii=False,
168
- )
169
- if tool_name == "confirm_money_transfer":
170
- return json.dumps(confirm_transfer(telegram_id), ensure_ascii=False)
171
- if tool_name == "cancel_money_transfer":
172
- return json.dumps(cancel_transfer(telegram_id), ensure_ascii=False)
173
- if tool_name == "get_pending_money_transfer":
174
- return json.dumps(get_pending_transfer(telegram_id), ensure_ascii=False)
175
- if tool_name == "create_illusion_account":
176
- return json.dumps(get_sender_account(telegram_id, args["user_name"]), ensure_ascii=False)
177
- return json.dumps({"success": False, "message": f"Unknown tool: {tool_name}"}, ensure_ascii=False)
178
-
179
- async def get_ai_response(user_query: str, telegram_id: int):
180
- conversation_history = []
181
- if db_manager:
182
- print("arriving to get history messages: \n")
183
- raw_history = await db_manager.get_conversation_history(telegram_id, limit=6)
184
- raw_history.reverse()
185
- for msg in raw_history:
186
- if msg.get('message_text'):
187
- role = "user" if msg['message_type'] == 'user' else "assistant"
188
- conversation_history.append({"role": role, "content": msg['message_text']})
189
-
190
- transfer_instructions = (
191
- f"Current user telegram_id is {telegram_id}. "
192
- "The model must never choose, guess, extract, or override any telegram_id for tool calls. "
193
- "Always act only for the current authenticated user from server-side request context. "
194
- "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. "
195
- "If the user asks for their balance, call check_account_balance. "
196
- "For money transfers, first collect the receiver account serial ID and the amount if it is missing. "
197
- "Then call prepare_money_transfer to fetch the receiver name and store the pending transfer. "
198
- "Show the receiver name back to the user and ask for explicit confirmation. "
199
- "Only call confirm_money_transfer after the user clearly agrees. "
200
- "If the user rejects the receiver or wants to stop, call cancel_money_transfer. "
201
- "Never claim a transfer is completed unless confirm_money_transfer returns success."
202
- "If a tool result returns 'need_user_name': true, ask the user for their name and then call create_illusion_account with their name. "
203
- "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. "
204
- "If confirm_money_transfer returns a result with 'is_illusion': true, make sure to display the testing disclaimer message to the user."
205
- )
206
- messages = [{"role": "system", "content": f"{BASE_PROMPT}\n\n{transfer_instructions}"}] + conversation_history + [{"role": "user", "content": user_query}]
207
-
208
-
209
- import asyncio
210
- loop = asyncio.get_event_loop()
211
-
212
-
213
- def call_hf(msgs):
214
- print("arriving to call hugging face: \n")
215
- return hf_client.chat.completions.create(
216
- model=MODEL_NAME,
217
- messages=msgs,
218
- tools=TOOLS,
219
- tool_choice="auto",
220
- temperature=0.1,
221
- max_tokens=800
222
- )
223
-
224
- completion = await loop.run_in_executor(None, lambda: call_hf(messages))
225
- response_message = completion.choices[0].message
226
-
227
- for _ in range(4):
228
- if not response_message.tool_calls:
229
- break
230
-
231
- messages.append(response_message)
232
- for tool_call in response_message.tool_calls:
233
- args = json.loads(tool_call.function.arguments or "{}")
234
- tool_result = await run_tool(tool_call.function.name, args, telegram_id)
235
- messages.append({
236
- "role": "tool",
237
- "tool_call_id": tool_call.id,
238
- "content": tool_result
239
- })
240
-
241
- completion = await loop.run_in_executor(None, lambda: call_hf(messages))
242
- response_message = completion.choices[0].message
243
-
244
- return clean_ai_response(response_message.content if response_message.content else "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from fastapi import FastAPI, Request
3
+ from pinecone import Pinecone
4
+ from groq import Groq
5
+ import httpx
6
+ import re
7
+
8
+ # 1. Configuration & Clients
9
+ # Use Hugging Face Secrets for these!
10
+ PINECONE_API_KEY = os.environ.get("PINECONE_API_KEY")
11
+ GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
12
+ TELEGRAM_TOKEN = os.environ.get("TELEGRAM_TOKEN")
13
+ TELEGRAM_URL = f"https://149.154.167.220/bot{TELEGRAM_TOKEN}/sendMessage"
14
+
15
+ EMBED_MODEL= os.environ.get("EMBED_MODEL")
16
+ GROQ_MODEL = os.environ.get("GROQ_MODEL")
17
+ PROMPT = os.environ.get("PROMPT")
18
+
19
+ pc = Pinecone(api_key=PINECONE_API_KEY)
20
+ index = pc.Index("customerserviceindex")
21
+ groq_client = Groq(api_key=GROQ_API_KEY)
22
+
23
+ app = FastAPI()
24
+
25
+ def clean_ai_response(text: str):
26
+ # إزالة كل ما بين وسمي <think> و </think> بما في ذلك الوسوم نفسها
27
+ cleaned_text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)
28
+ return cleaned_text.strip()
29
+
30
+ # 2. The Core AI Logic
31
+ async def get_ai_response(user_query: str):
32
+ # Vectorize query using Pinecone Inference
33
+ query_embedding = pc.inference.embed(
34
+ model=EMBED_MODEL,
35
+ inputs=[user_query],
36
+ parameters={"input_type": "query"}
37
+ )
38
+
39
+ # Search Pinecone for Bank Context
40
+ search_results = index.query(
41
+ vector=query_embedding[0].values,
42
+ top_k=3,
43
+ include_metadata=True
44
+ )
45
+
46
+ retrieved_context = "\n".join([res.metadata['original_text'] for res in search_results.matches])
47
+
48
+
49
+ prompt = f"""
50
+ {PROMPT}
51
+
52
+ Message:{user_query}
53
+
54
+ Retrieved Context:{retrieved_context}
55
+
56
+ Final Answer:
57
+ """
58
+
59
+ completion = groq_client.chat.completions.create(
60
+ messages=[{"role": "user", "content": prompt}],
61
+ model=GROQ_MODEL,
62
+ temperature=0.1,
63
+ max_completion_tokens=800,
64
+ top_p=0.9,
65
+ )
66
+ ai_response = completion.choices[0].message.content
67
+ return clean_ai_response(ai_response)
68
+
69
+ # 3. The Webhook Endpoint
70
+ @app.post("/webhook")
71
+ async def telegram_webhook(request: Request):
72
+ data = await request.json()
73
+
74
+ if "message" in data:
75
+ chat_id = data["message"]["chat"]["id"]
76
+ user_text = data["message"].get("text", "")
77
+
78
+ if user_text:
79
+ # Get the intelligent response
80
+ ai_answer = await get_ai_response(user_text)
81
+ # Send back to Telegram
82
+ async with httpx.AsyncClient(verify=False) as client:
83
+ await client.post(
84
+ TELEGRAM_URL,
85
+ headers={"Host": "api.telegram.org"},
86
+ json={
87
+ "chat_id": chat_id,
88
+ "text": ai_answer,
89
+ "parse_mode": "Markdown"
90
+ }
91
+ )
92
+
93
+ return {"status": "ok"}
94
+
95
+ @app.get("/")
96
+ async def root():
97
+ return {"message": "Hadhramout Bank AI Backend is Live"}
98
+
99
+ @app.post("/test")
100
+ async def test_webhook(request: Request):
101
+ data = await request.json()
102
+ response = await get_ai_response(data["message"]["text"])
103
+ return {"response": response}
104
+
105
+ import socket
106
+
107
+ @app.get("/dns-test")
108
+ async def dns_test():
109
+ try:
110
+ ip = socket.gethostbyname("api.telegram.org")
111
+ return {"resolved_ip": ip}
112
+ except Exception as e:
113
+ return {"error": str(e)}
config.py DELETED
@@ -1,47 +0,0 @@
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
- HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HF_API_KEY")
12
- TELEGRAM_TOKEN = os.environ.get("TELEGRAM_TOKEN")
13
- TELEGRAM_WEBHOOK_SECRET = os.environ.get("TELEGRAM_WEBHOOK_SECRET")
14
- SUPABASE_URL = os.environ.get("SUPABASE_URL")
15
- SUPABASE_KEY = os.environ.get("SUPABASE_KEY")
16
- TELEGRAM_DOMAIN = os.environ.get("TELEGRAM_DOMAIN", "https://api.telegram.org").rstrip("/")
17
-
18
- # Only create TELEGRAM_URL if token exists
19
- TELEGRAM_URL = f"{TELEGRAM_DOMAIN}/bot{TELEGRAM_TOKEN}/sendMessage" if TELEGRAM_TOKEN and TELEGRAM_DOMAIN else None
20
-
21
- EMBED_MODEL = os.environ.get("EMBED_MODEL", "multilingual-e5-large")
22
- HF_MODEL = os.environ.get(
23
- "HF_MODEL",
24
- "dphn/Dolphin-Mistral-24B-Venice-Edition",
25
- )
26
- PROMPT = os.environ.get("PROMPT")
27
-
28
- # Initialize clients only if API keys are available
29
- pc = None
30
- if PINECONE_API_KEY:
31
- pc = Pinecone(api_key=PINECONE_API_KEY)
32
-
33
- hf_client = None
34
- if HF_TOKEN:
35
- try:
36
- hf_client = OpenAI(
37
- base_url="https://router.huggingface.co/v1",
38
- api_key=HF_TOKEN,
39
- )
40
- except Exception as e:
41
- print(f"Warning: Failed to initialize Hugging Face OpenAI client: {e}")
42
- hf_client = None
43
-
44
- # Initialize index only if Pinecone client is available
45
- index = None
46
- if pc:
47
- index = pc.Index("customerserviceindex")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
database.py DELETED
@@ -1,104 +0,0 @@
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 DELETED
@@ -1,39 +0,0 @@
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 CHANGED
@@ -1,7 +1,6 @@
1
  fastapi
2
  uvicorn
3
  pinecone
 
4
  httpx
5
  python-dotenv
6
- supabase
7
- openai
 
1
  fastapi
2
  uvicorn
3
  pinecone
4
+ groq
5
  httpx
6
  python-dotenv
 
 
rest.http DELETED
@@ -1,22 +0,0 @@
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 DELETED
@@ -1,22 +0,0 @@
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 DELETED
@@ -1,54 +0,0 @@
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 DELETED
@@ -1,90 +0,0 @@
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 DELETED
@@ -1,86 +0,0 @@
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
- # 1. تحديث المستخدم (Async)
43
- if db_manager:
44
- await db_manager.create_or_update_user(telegram_id, username, first_name, data.message.chat.last_name)
45
-
46
- # 2. الحصول على رد الـ AI
47
- ai_answer = await get_ai_response(user_text, telegram_id)
48
- final_response = ai_answer or "Sorry, I couldn't generate a response."
49
-
50
- # 3. إرسال الرسالة باستخدام IP مباشر لتجنب تأخير DNS
51
- if TELEGRAM_TOKEN:
52
- async with httpx.AsyncClient(timeout=40.0, verify=False) as client:
53
- prepared_text = _sanitize_telegram_text(final_response)
54
- formatted_text = _format_telegram_message(prepared_text)
55
- final_text = formatted_text[:MAX_TELEGRAM_MESSAGE_LENGTH]
56
-
57
- payload = {
58
- "chat_id": telegram_id,
59
- "text": final_text if final_text.strip() else ".",
60
- "parse_mode": "HTML",
61
- }
62
-
63
- # رابط مباشر لتجاوز DNS
64
- forced_ip_url = f"https://{TELEGRAM_IP}/bot{TELEGRAM_TOKEN}/sendMessage"
65
- headers = {
66
- "Host": "api.telegram.org",
67
- "Content-Type": "application/json; charset=utf-8",
68
- }
69
-
70
- response = await client.post(forced_ip_url, json=payload, headers=headers)
71
-
72
- if response.status_code == 200:
73
- print("--- Success: Telegram message delivered ---")
74
- # 4. حفظ المحادثة بعد التأكد من وصول الرسالة
75
- if db_manager:
76
- await asyncio.gather(
77
- db_manager.save_message(telegram_id, user_text, "user"),
78
- db_manager.save_message(telegram_id, final_response, "assistant")
79
- )
80
- else:
81
- print(f"--- Telegram Rejected: {response.status_code} - {response.text} ---")
82
-
83
- return {"status": "ok"}
84
- except Exception as e:
85
- print(f"Error in webhook: {str(e)}")
86
- return {"status": "error", "message": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
transfers/__init__.py DELETED
@@ -1,9 +0,0 @@
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 DELETED
@@ -1,91 +0,0 @@
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 DELETED
@@ -1,13 +0,0 @@
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 DELETED
@@ -1,303 +0,0 @@
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 DELETED
@@ -1,20 +0,0 @@
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)}