codeBOKER commited on
Commit
cf6d08c
·
1 Parent(s): 9a3879b

feat: rebrand FALSA to FALZH with updated supabase schema, tool names, and prompts

Browse files
.env.example CHANGED
@@ -1,4 +1,4 @@
1
- APP_NAME=FALSA
2
  ENVIRONMENT=development
3
  APP_TIMEZONE=Asia/Aden
4
  LOG_LEVEL=INFO
@@ -9,8 +9,8 @@ SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
9
  PINECONE_API_KEY=your-pinecone-api-key
10
  PINECONE_CLOUD=aws
11
  PINECONE_REGION=us-east-1
12
- PINECONE_INFO_INDEX=falsa-info
13
- PINECONE_TRIPS_INDEX=falsa-trips
14
  PINECONE_EMBED_MODEL=multilingual-e5-large
15
  PINECONE_NAMESPACE=default
16
 
 
1
+ APP_NAME=FALZH
2
  ENVIRONMENT=development
3
  APP_TIMEZONE=Asia/Aden
4
  LOG_LEVEL=INFO
 
9
  PINECONE_API_KEY=your-pinecone-api-key
10
  PINECONE_CLOUD=aws
11
  PINECONE_REGION=us-east-1
12
+ PINECONE_INFO_INDEX=falzh-info
13
+ PINECONE_TRIPS_INDEX=falzh-trips
14
  PINECONE_EMBED_MODEL=multilingual-e5-large
15
  PINECONE_NAMESPACE=default
16
 
README.md CHANGED
@@ -1,14 +1,14 @@
1
- # FALSA
2
 
3
  > **AI-powered travel service — on WhatsApp.**
4
 
5
  > **🚧 Work in progress — not yet published. Coming soon.**
6
 
7
  <p align="center">
8
- <img src="assets/Falsa.png" alt="FALSA logo" width="100%">
9
  </p>
10
 
11
- FALSA lets passengers search trips and connect with drivers, and lets drivers publish, update, and manage their trips — all through a natural conversation on WhatsApp. No app to install, no complex UI to learn. Just type what you want.
12
 
13
  ---
14
 
@@ -23,9 +23,9 @@ Travel booking platforms in my country are poorly adopted. Not because people do
23
 
24
  ## Solution
25
 
26
- FALSA removes all of that. The entire interface is a single chat conversation. **You say where you're going and when — FALSA handles the rest.**
27
 
28
- - **Passengers** describe their trip in natural language; FALSA searches available trips, shows options, and connects them with drivers.
29
  - **Drivers** publish trips, manage schedules, and receive trip interest notifications — all by chatting.
30
  - No app install, no account creation flow, no confusing dashboard.
31
 
@@ -36,7 +36,7 @@ FALSA removes all of that. The entire interface is a single chat conversation. *
36
  - **Natural-language trip search** — "I want to go from Sana'a to Taiz tomorrow morning"
37
  - **WhatsApp-native interaction** — interactive lists, text replies, no extra UI
38
  - **Driver tools** — publish trips, add cars, modify/delete trips, receive trip interest notifications
39
- - **Passenger tools** — search trips, select trips, get driver contact, get FALSA information
40
  - **AI-powered orchestration** — tool-calling LLM (Groq primary, Hugging Face fallback) routes intent to the right action
41
  - **RAG knowledge base** — company info, pricing, policies embedded via Jina AI and retrieved on demand
42
  - **Trip vector search** — semantic search over driver trips for flexible departure matching
@@ -83,7 +83,7 @@ This means:
83
  WhatsApp User
84
  │
85
  ▼ (message)
86
- Baileys Bridge ──► FALSA API ──► AI Orchestrator ──► Tools
87
  (Node.js, (FastAPI) (Groq / HF) │
88
  separate repo) │ │
89
  ▼ ▼
@@ -92,7 +92,7 @@ Baileys Bridge ──► FALSA API ──► AI Orchestrator ──► Tools
92
  messages, embeddings)
93
  ```
94
 
95
- The system is designed so the WhatsApp integration layer is swappable. The Baileys bridge (Node.js) handles the Web protocol; the FALSA Python backend talks to it the same way it talks to the Cloud API. When Cloud API onboarding is complete, swapping the bridge requires zero changes to the core logic.
96
 
97
  ---
98
 
@@ -108,7 +108,7 @@ The system is designed so the WhatsApp integration layer is swappable. The Baile
108
 
109
  ```bash
110
  # 1. Clone and enter the repo
111
- git clone <repo-url> && cd falsa
112
 
113
  # 2. Configure environment
114
  cp .env.example .env
@@ -132,7 +132,7 @@ The API is now running at `http://localhost:8000`.
132
  ./scripts/setup_and_seed.sh
133
  ```
134
 
135
- This embeds the FALSA info document (`prompts/falsa_info.md`) and all active trips into Supabase pgvector for RAG retrieval.
136
 
137
  ### Run Tests
138
 
@@ -157,13 +157,13 @@ Mounts the current directory with hot-reload enabled.
157
 
158
  ## WhatsApp Baileys Bridge
159
 
160
- FALSA uses the official Meta Cloud API for production messages. During early stages — while waiting for commercial approval — a **Baileys-based bridge** acts as the WhatsApp gateway.
161
 
162
  The bridge is a separate Node.js project:
163
 
164
  #### [https://github.com/codeBOKER/wh_baileys](https://github.com/codeBOKER/wh_baileys)
165
 
166
- It connects to WhatsApp via the Web protocol, relays inbound messages to the FALSA API, and forwards responses back to the user. The FALSA API treats the bridge identically to the Cloud API, so switching later requires no backend changes.
167
 
168
  ---
169
 
@@ -174,7 +174,7 @@ It connects to WhatsApp via the Web protocol, relays inbound messages to the FAL
174
  | `GET` | `/healthz` | Health check |
175
  | `GET` | `/webhooks/whatsapp` | Webhook verification (challenge) |
176
  | `POST` | `/webhooks/whatsapp` | Receive WhatsApp messages |
177
- | `POST` | `/admin/seed-info` | Seed FALSA RAG info |
178
  | `POST` | `/admin/sync-trips` | Sync trip embeddings |
179
  | `POST` | `/admin/jina-embed` | Test embedding |
180
  | `POST` | `/admin/llm-tool-call` | Debug tool calling |
@@ -184,7 +184,7 @@ It connects to WhatsApp via the Web protocol, relays inbound messages to the FAL
184
  ## Project Structure
185
 
186
  ```
187
- falsa/
188
  ├── app/
189
  │ ├── ai/ # LLM orchestration, providers, tool schemas
190
  │ ├── api/ # FastAPI routes, dependency injection
@@ -219,7 +219,7 @@ Baileys (Node.js) is used only as a temporary WhatsApp bridge. My strongest area
219
 
220
  **How does the Baileys bridge differ from the Cloud API?**
221
 
222
- It doesn't — that's the point. The FALSA backend speaks the same protocol to both. The Baileys bridge simply translates the WhatsApp Web protocol into the same format the Cloud API uses. When Cloud API approval comes through, we swap the bridge with zero backend changes.
223
 
224
  ---
225
 
 
1
+ # FALZH
2
 
3
  > **AI-powered travel service — on WhatsApp.**
4
 
5
  > **🚧 Work in progress — not yet published. Coming soon.**
6
 
7
  <p align="center">
8
+ <img src="assets/Falsa.png" alt="FALZH logo" width="100%">
9
  </p>
10
 
11
+ FALZH lets passengers search trips and connect with drivers, and lets drivers publish, update, and manage their trips — all through a natural conversation on WhatsApp. No app to install, no complex UI to learn. Just type what you want.
12
 
13
  ---
14
 
 
23
 
24
  ## Solution
25
 
26
+ FALZH removes all of that. The entire interface is a single chat conversation. **You say where you're going and when — FALZH handles the rest.**
27
 
28
+ - **Passengers** describe their trip in natural language; FALZH searches available trips, shows options, and connects them with drivers.
29
  - **Drivers** publish trips, manage schedules, and receive trip interest notifications — all by chatting.
30
  - No app install, no account creation flow, no confusing dashboard.
31
 
 
36
  - **Natural-language trip search** — "I want to go from Sana'a to Taiz tomorrow morning"
37
  - **WhatsApp-native interaction** — interactive lists, text replies, no extra UI
38
  - **Driver tools** — publish trips, add cars, modify/delete trips, receive trip interest notifications
39
+ - **Passenger tools** — search trips, select trips, get driver contact, get FALZH information
40
  - **AI-powered orchestration** — tool-calling LLM (Groq primary, Hugging Face fallback) routes intent to the right action
41
  - **RAG knowledge base** — company info, pricing, policies embedded via Jina AI and retrieved on demand
42
  - **Trip vector search** — semantic search over driver trips for flexible departure matching
 
83
  WhatsApp User
84
  │
85
  ▼ (message)
86
+ Baileys Bridge ──► FALZH API ──► AI Orchestrator ──► Tools
87
  (Node.js, (FastAPI) (Groq / HF) │
88
  separate repo) │ │
89
  ▼ ▼
 
92
  messages, embeddings)
93
  ```
94
 
95
+ The system is designed so the WhatsApp integration layer is swappable. The Baileys bridge (Node.js) handles the Web protocol; the FALZH Python backend talks to it the same way it talks to the Cloud API. When Cloud API onboarding is complete, swapping the bridge requires zero changes to the core logic.
96
 
97
  ---
98
 
 
108
 
109
  ```bash
110
  # 1. Clone and enter the repo
111
+ git clone <repo-url> && cd falzh
112
 
113
  # 2. Configure environment
114
  cp .env.example .env
 
132
  ./scripts/setup_and_seed.sh
133
  ```
134
 
135
+ This embeds the FALZH info document (`prompts/falzh_info.md`) and all active trips into Supabase pgvector for RAG retrieval.
136
 
137
  ### Run Tests
138
 
 
157
 
158
  ## WhatsApp Baileys Bridge
159
 
160
+ FALZH uses the official Meta Cloud API for production messages. During early stages — while waiting for commercial approval — a **Baileys-based bridge** acts as the WhatsApp gateway.
161
 
162
  The bridge is a separate Node.js project:
163
 
164
  #### [https://github.com/codeBOKER/wh_baileys](https://github.com/codeBOKER/wh_baileys)
165
 
166
+ It connects to WhatsApp via the Web protocol, relays inbound messages to the FALZH API, and forwards responses back to the user. The FALZH API treats the bridge identically to the Cloud API, so switching later requires no backend changes.
167
 
168
  ---
169
 
 
174
  | `GET` | `/healthz` | Health check |
175
  | `GET` | `/webhooks/whatsapp` | Webhook verification (challenge) |
176
  | `POST` | `/webhooks/whatsapp` | Receive WhatsApp messages |
177
+ | `POST` | `/admin/seed-info` | Seed FALZH RAG info |
178
  | `POST` | `/admin/sync-trips` | Sync trip embeddings |
179
  | `POST` | `/admin/jina-embed` | Test embedding |
180
  | `POST` | `/admin/llm-tool-call` | Debug tool calling |
 
184
  ## Project Structure
185
 
186
  ```
187
+ falzh/
188
  ├── app/
189
  │ ├── ai/ # LLM orchestration, providers, tool schemas
190
  │ ├── api/ # FastAPI routes, dependency injection
 
219
 
220
  **How does the Baileys bridge differ from the Cloud API?**
221
 
222
+ It doesn't — that's the point. The FALZH backend speaks the same protocol to both. The Baileys bridge simply translates the WhatsApp Web protocol into the same format the Cloud API uses. When Cloud API approval comes through, we swap the bridge with zero backend changes.
223
 
224
  ---
225
 
app/ai/tool_schemas.py CHANGED
@@ -2,19 +2,19 @@ from typing import Any
2
 
3
  from app.models.domain import UserMode
4
 
5
- _ABOUT_FALSA = {
6
  "type": "function",
7
  "function": {
8
- "name": "about_falsa",
9
  "description": (
10
- "Retrieve official FALSA company, FAQ, policy, or pricing information."
11
  ),
12
  "parameters": {
13
  "type": "object",
14
  "properties": {
15
  "query": {
16
  "type": "string",
17
- "description": "The customer's question about FALSA.",
18
  },
19
  "language": {
20
  "type": "string",
@@ -122,7 +122,7 @@ _CREATE_DRIVER_ACCOUNT = {
122
  "type": "function",
123
  "function": {
124
  "name": "create_driver_account",
125
- "description": "Register this WhatsApp sender as a FALSA driver. Phone from chat session.",
126
  "parameters": {
127
  "type": "object",
128
  "properties": {
@@ -354,7 +354,7 @@ _SWITCH_TO_PASSENGER = {
354
  }
355
 
356
  _TOOL_SCHEMAS: dict[str, dict[str, Any]] = {
357
- "about_falsa": _ABOUT_FALSA,
358
  "search_trips": _SEARCH_TRIPS,
359
  "select_trip": _SELECT_TRIP,
360
  "create_driver_account": _CREATE_DRIVER_ACCOUNT,
@@ -370,13 +370,13 @@ _TOOL_SCHEMAS: dict[str, dict[str, Any]] = {
370
 
371
  _TOOLS_BY_MODE: dict[UserMode, list[str]] = {
372
  "new_user": [
373
- "about_falsa",
374
  "create_driver_account",
375
  "switch_to_driver",
376
  "switch_to_passenger",
377
  ],
378
  "driver": [
379
- "about_falsa",
380
  "check_driver_info",
381
  "check_driver_trips",
382
  "add_driver_car",
@@ -386,7 +386,7 @@ _TOOLS_BY_MODE: dict[UserMode, list[str]] = {
386
  "switch_to_passenger",
387
  ],
388
  "passenger": [
389
- "about_falsa",
390
  "search_trips",
391
  "select_trip",
392
  "create_driver_account",
 
2
 
3
  from app.models.domain import UserMode
4
 
5
+ _ABOUT_FALZH = {
6
  "type": "function",
7
  "function": {
8
+ "name": "about_falzh",
9
  "description": (
10
+ "Retrieve official FALZH company, FAQ, policy, or pricing information."
11
  ),
12
  "parameters": {
13
  "type": "object",
14
  "properties": {
15
  "query": {
16
  "type": "string",
17
+ "description": "The customer's question about FALZH.",
18
  },
19
  "language": {
20
  "type": "string",
 
122
  "type": "function",
123
  "function": {
124
  "name": "create_driver_account",
125
+ "description": "Register this WhatsApp sender as a FALZH driver. Phone from chat session.",
126
  "parameters": {
127
  "type": "object",
128
  "properties": {
 
354
  }
355
 
356
  _TOOL_SCHEMAS: dict[str, dict[str, Any]] = {
357
+ "about_falzh": _ABOUT_FALZH,
358
  "search_trips": _SEARCH_TRIPS,
359
  "select_trip": _SELECT_TRIP,
360
  "create_driver_account": _CREATE_DRIVER_ACCOUNT,
 
370
 
371
  _TOOLS_BY_MODE: dict[UserMode, list[str]] = {
372
  "new_user": [
373
+ "about_falzh",
374
  "create_driver_account",
375
  "switch_to_driver",
376
  "switch_to_passenger",
377
  ],
378
  "driver": [
379
+ "about_falzh",
380
  "check_driver_info",
381
  "check_driver_trips",
382
  "add_driver_car",
 
386
  "switch_to_passenger",
387
  ],
388
  "passenger": [
389
+ "about_falzh",
390
  "search_trips",
391
  "select_trip",
392
  "create_driver_account",
app/config.py CHANGED
@@ -15,7 +15,7 @@ class Settings(BaseSettings):
15
  extra="ignore",
16
  )
17
 
18
- app_name: str = "FALSA"
19
  environment: str = "development"
20
  app_timezone: str = "Asia/Aden"
21
  log_level: str = "INFO"
 
15
  extra="ignore",
16
  )
17
 
18
+ app_name: str = "FALZH"
19
  environment: str = "development"
20
  app_timezone: str = "Asia/Aden"
21
  log_level: str = "INFO"
app/database/supabase.py CHANGED
@@ -457,7 +457,7 @@ class SupabaseRepository:
457
  match_count: int = 5,
458
  ) -> list[dict[str, Any]]:
459
  response = await self.client.rpc(
460
- "match_falsa_info",
461
  {
462
  "query_embedding": query_embedding,
463
  "match_count": match_count,
@@ -512,7 +512,7 @@ class SupabaseRepository:
512
  async def upsert_info_chunks(self, chunks: list[dict[str, Any]]) -> int:
513
  if not chunks:
514
  return 0
515
- response = await self.client.table("falsa_info_chunks").upsert(chunks).execute()
516
  data = _response_data(response)
517
  return len(data) if isinstance(data, list) else len(chunks)
518
 
 
457
  match_count: int = 5,
458
  ) -> list[dict[str, Any]]:
459
  response = await self.client.rpc(
460
+ "match_falzh_info",
461
  {
462
  "query_embedding": query_embedding,
463
  "match_count": match_count,
 
512
  async def upsert_info_chunks(self, chunks: list[dict[str, Any]]) -> int:
513
  if not chunks:
514
  return 0
515
+ response = await self.client.table("falzh_info_chunks").upsert(chunks).execute()
516
  data = _response_data(response)
517
  return len(data) if isinstance(data, list) else len(chunks)
518
 
app/main.py CHANGED
@@ -21,6 +21,6 @@ def create_app(
21
  app.state.container = container or await ServiceContainer.from_settings(active_settings)
22
  yield
23
 
24
- app = FastAPI(title="FALSA API", version="0.1.0", lifespan=lifespan)
25
  app.include_router(router)
26
  return app
 
21
  app.state.container = container or await ServiceContainer.from_settings(active_settings)
22
  yield
23
 
24
+ app = FastAPI(title="FALZH API", version="0.1.0", lifespan=lifespan)
25
  app.include_router(router)
26
  return app
app/models/api.py CHANGED
@@ -4,7 +4,7 @@ from typing import Any
4
 
5
  class HealthResponse(BaseModel):
6
  status: str = "ok"
7
- service: str = "FALSA"
8
 
9
 
10
  class SeedInfoResponse(BaseModel):
 
4
 
5
  class HealthResponse(BaseModel):
6
  status: str = "ok"
7
+ service: str = "FALZH"
8
 
9
 
10
  class SeedInfoResponse(BaseModel):
app/services/admin_service.py CHANGED
@@ -19,7 +19,7 @@ class AdminService:
19
  self.repository = repository
20
  self.embeddings = embeddings
21
  self.settings = settings
22
- self.info_path = info_path or Path("prompts/falsa_info.md")
23
 
24
  async def seed_info(self) -> int:
25
  text = self.info_path.read_text(encoding="utf-8")
 
19
  self.repository = repository
20
  self.embeddings = embeddings
21
  self.settings = settings
22
+ self.info_path = info_path or Path("prompts/falzh_info.md")
23
 
24
  async def seed_info(self) -> int:
25
  text = self.info_path.read_text(encoding="utf-8")
app/services/conversation_service.py CHANGED
@@ -9,7 +9,7 @@ from app.database.supabase import SupabaseRepository
9
  from app.models.domain import UserMode, WhatsAppInboundMessage
10
  from app.services.embedding_service import JinaEmbeddingService
11
  from app.services.trip_indexing import unindex_trip
12
- from app.tools.handlers import FalsaToolHandlers, _trip_summary
13
  from app.tools.registry import ToolRegistry
14
  from app.utils.time import now_in_timezone
15
  from app.whatsapp.client import WhatsAppClient
@@ -99,7 +99,7 @@ class ConversationService:
99
  if orig_meta.get("type") == "trip_card":
100
  trip_id = orig_meta.get("trip_id")
101
  if trip_id:
102
- handlers = FalsaToolHandlers(
103
  repository=self.repository,
104
  embeddings=self.embeddings,
105
  whatsapp=self.whatsapp,
@@ -202,7 +202,7 @@ class ConversationService:
202
  "They were previously tracked from WhatsApp group trip posts. "
203
  "Welcome them warmly by name. Tell them we have been following their trips "
204
  "in the groups and we are impressed. Explain that we have registered them in "
205
- "FALSA so they can now send trips directly here instead of posting in groups. "
206
  "Show them how: just send the trip details (route, date, time) in chat. "
207
  "Explain the benefits: passengers find their trips via AI search, they get "
208
  "notified immediately when a passenger selects their trip, and registered "
@@ -269,7 +269,7 @@ class ConversationService:
269
  user_mode: UserMode,
270
  current_message: dict[str, Any] | None = None,
271
  ) -> ToolRegistry:
272
- handlers = FalsaToolHandlers(
273
  repository=self.repository,
274
  embeddings=self.embeddings,
275
  whatsapp=self.whatsapp,
 
9
  from app.models.domain import UserMode, WhatsAppInboundMessage
10
  from app.services.embedding_service import JinaEmbeddingService
11
  from app.services.trip_indexing import unindex_trip
12
+ from app.tools.handlers import FalzhToolHandlers, _trip_summary
13
  from app.tools.registry import ToolRegistry
14
  from app.utils.time import now_in_timezone
15
  from app.whatsapp.client import WhatsAppClient
 
99
  if orig_meta.get("type") == "trip_card":
100
  trip_id = orig_meta.get("trip_id")
101
  if trip_id:
102
+ handlers = FalzhToolHandlers(
103
  repository=self.repository,
104
  embeddings=self.embeddings,
105
  whatsapp=self.whatsapp,
 
202
  "They were previously tracked from WhatsApp group trip posts. "
203
  "Welcome them warmly by name. Tell them we have been following their trips "
204
  "in the groups and we are impressed. Explain that we have registered them in "
205
+ "FALZH so they can now send trips directly here instead of posting in groups. "
206
  "Show them how: just send the trip details (route, date, time) in chat. "
207
  "Explain the benefits: passengers find their trips via AI search, they get "
208
  "notified immediately when a passenger selects their trip, and registered "
 
269
  user_mode: UserMode,
270
  current_message: dict[str, Any] | None = None,
271
  ) -> ToolRegistry:
272
+ handlers = FalzhToolHandlers(
273
  repository=self.repository,
274
  embeddings=self.embeddings,
275
  whatsapp=self.whatsapp,
app/tools/handlers.py CHANGED
@@ -27,7 +27,7 @@ from app.whatsapp.client import WhatsAppClient, WhatsAppClientError
27
  from app.whatsapp.trip_selection import format_trip_card
28
 
29
 
30
- class FalsaToolHandlers:
31
  def __init__(
32
  self,
33
  *,
@@ -60,7 +60,7 @@ class FalsaToolHandlers:
60
  original_meta = original.get("metadata") or {}
61
  return original_meta.get("trip_id")
62
 
63
- async def about_falsa(self, arguments: dict[str, Any]) -> ToolResult:
64
  query = str(arguments.get("query") or "").strip()
65
  if not query:
66
  return ToolResult(ok=False, data={}, error="query is required")
@@ -74,7 +74,7 @@ class FalsaToolHandlers:
74
  return ToolResult(
75
  ok=True,
76
  data={
77
- "answer": "No matching FALSA policy or FAQ content was found.",
78
  "sources": [],
79
  },
80
  )
@@ -1080,7 +1080,7 @@ def _driver_notification_text(
1080
  notes: str | None,
1081
  ) -> str:
1082
  return (
1083
- "🔔 اهتمام جديد برحلة في فلزا\n"
1084
  f"العميل: {customer.get('name') or 'عميل جديد'}\n"
1085
  f"رقم العميل: {customer.get('phone_number') or 'غير متوفر'}\n"
1086
  f"الرحلة: {trip.get('departure')} ← {trip.get('destination')}\n"
 
27
  from app.whatsapp.trip_selection import format_trip_card
28
 
29
 
30
+ class FalzhToolHandlers:
31
  def __init__(
32
  self,
33
  *,
 
60
  original_meta = original.get("metadata") or {}
61
  return original_meta.get("trip_id")
62
 
63
+ async def about_falzh(self, arguments: dict[str, Any]) -> ToolResult:
64
  query = str(arguments.get("query") or "").strip()
65
  if not query:
66
  return ToolResult(ok=False, data={}, error="query is required")
 
74
  return ToolResult(
75
  ok=True,
76
  data={
77
+ "answer": "No matching FALZH policy or FAQ content was found.",
78
  "sources": [],
79
  },
80
  )
 
1080
  notes: str | None,
1081
  ) -> str:
1082
  return (
1083
+ "🔔 اهتمام جديد برحلة في فلزة\n"
1084
  f"العميل: {customer.get('name') or 'عميل جديد'}\n"
1085
  f"رقم العميل: {customer.get('phone_number') or 'غير متوفر'}\n"
1086
  f"الرحلة: {trip.get('departure')} ← {trip.get('destination')}\n"
docker-compose.yml CHANGED
@@ -1,5 +1,5 @@
1
  services:
2
- falsa-api:
3
  build: .
4
  env_file:
5
  - .env
 
1
  services:
2
+ falzh-api:
3
  build: .
4
  env_file:
5
  - .env
prompts/{falsa_info.md → falzh_info.md} RENAMED
@@ -1,15 +1,15 @@
1
- # فلزا — معلومات المنصة
2
 
3
- ## عن فلزا
4
- فلزا هي خدمة سفر عبر واتساب مدعومة بالذكاء الاصطناعي. تعمل بالكامل داخل واتساب — لا تحتاج تحميل تطبيق جديد. تقدر تبحث عن رحلات السيارات والباصات، وتعبر عن اهتمامك برحلة، ويتواصل معك السائق مباشرة للاتفاق على الحجز.
5
 
6
  ## كيف تعمل الخدمة للمسافر
7
  1. ترسل رسالة على واتساب تصف رحلتك (مثال: "أبي أسافر من صنعاء إلى تعز بكرة الصباح")
8
- 2. فلزا تبحث في الرحلات المتاحة وترسل لك بطاقات الرحلات المطابقة
9
  3. ترد على بطاقة الرحلة اللي تعجبك
10
- 4. فلزا تخبر السائق باهتمامك وتعطيك رقم السائق
11
  5. تتواصل مع السائق مباشرة للاتفاق على التفاصيل والحجز
12
- ملاحظة: فلزا لا تحجز لك ولا تؤكد الحجز. اختيار الرحلة يعني تسجيل اهتمام فقط.
13
 
14
  ## أنماط المستخدم
15
  المستخدم الجديد: يسأله النظام هل يريد السفر (مسافر) أو العمل كسائق. المسافر: يبحث عن رحلات ويختار ويتواصل مع السائق. السائق: ينشر رحلات ويدير جدوله ويسجل مركباته ويستلم إشعارات الاهتمام بالرحلات.
@@ -21,31 +21,31 @@
21
  عندما تختار رحلة بالرد على بطاقة الرحلة: يتم تسجيل اهتمامك (ليس حجزاً مؤكداً)، السائق يستلم إشعار فيه اسمك ورقمك وعدد المقاعد المطلوبة، أنت تستلم رقم السائق، تتواصلون مباشرة للاتفاق على الحجز والسعر. المقاعد لا تحجز. قد يتواصل معك أكثر من شخص مهتم بنفس الرحلة.
22
 
23
  ## الأسعار
24
- السعر تحدده شركة السائق أو السائق نفسه. فلزا فقط تعرض السعر اللي أدخله السائق عند نشر الرحلة. فلزا لا تحدد الأسعار ولا تتعامل في المدفوعات. إذا كان السعر مو محدد في الرحلة، اسأل السائق مباشرة.
25
 
26
  ## خدمات السائقين
27
- تسجيل حساب سائق: يحتاج الاسم الكامل. تربط فلزا حسابك برقم واتسابك. إضافة مركبة: اسم المركبة مطلوب، رقم اللوحة وعدد المقاعد اختياري. نشر رحلة: يلزم المدينة (من وإلى)، التاريخ، الفترة (صباح/ظهر/ليل). عدد المقاعد والسعر تُملأ تلقائياً من آخر رحلة أو مركبتك المسجلة. تعديل رحلة: تقدر تغير الوجهة، الوقت، السيارة، المقاعد، أو السعر. تاريخ الرحلة لا يمكن تغييره — لازم تنشئ رحلة جديدة. حذف رحلة: تلغي الرحلة وترفعها من البحث. ممنوع يكون عندك رحلتين في نفس التاريخ والفترة.
28
 
29
  ## السياسات
30
- فلزا لا تحجز رحلات ولا تؤكد حجوزات. اختيار الرحلة يعني تسجيل اهتمام فقط. فلزا لا تتعامل في المدفوعات نهائياً. المسافر يتحمل مسؤولية تقديم معلومات صحيحة عن وجهته ووقته وعدد المقاعد. فلزا فقط تقدم معلومات عن خدماتها — ترفض الإجابة عن أي موضوع خارج السفر.
31
 
32
  ## الخصوصية
33
- رقم واتسابك يُستخدم فقط لربط حسابك وإرسال الإشعارات. رقم هاتفك يُشارك مع السائق فقط عندما تختار رحلة، عشان يتواصل معك. فلزا لا تشارك معلوماتك مع أي طرف آخر.
34
 
35
  ## الأسئلة الشائعة
36
- س: وش هي فلزا؟ ج: خدمة سفر على واتساب. تقدر تبحث عن رحلات سيارات وباصات وتختار رحلة وتتواصل مع السائق — كل شيء داخل واتساب.
37
- س: كيف أبحث عن رحلة؟ ج: فقط اكتب وجهتك مثل "من صنعاء إلى تعز بكرة الصباح". فلزا تفهم وتجيب لك النتائج.
38
  س: هل الحجز مؤكد؟ ج: لا. اختيار الرحلة يعني إرسال اهتمامك للسائق. السائق يتواصل معك للاتفاق.
39
- س: كيف أدفع؟ ج: فلزا لا تتعامل بالدفع. تتفق مع السائق مباشرة على السعر وطريقة الدفع.
40
- س: كيف أتواصل مع السائق؟ ج: بعد اختيار الرحلة، ترسل فلزا رقم السائق. كلمه على واتساب أو اتصال.
41
  س: وش معنى المقاعد المتاحة؟ ج: عدد المقاعد الفارغة في الرحلة. تقدر تطلب أي عدد لكن لا تحجز — السائق يؤكد بعد التواصل.
42
  س: هل أقدر ألغي اختياري؟ ج: اختيار الرحلة هو إرسال اهتمام فقط ولا يوجد إلغاء من النظام. إذا تغير رأيك، تواصل مع السائق مباشرة.
43
  س: كيف أسجل كسائق؟ ج: قول "أبي أسجل كسائق" أو اختر وضع السائق. يحتاج الاسم الكامل.
44
- س: كيف أبحث عن رحلة لسائق معين؟ ج: قول "أبي رحلات السائق أحمد" أو "دور رحلة مع السائق". فلزا تبحث في الرحلات المتاحة لهالسائق وترسل لك النتائج.
45
- س: كيف أضيف رحلة كسائق؟ ج: قول "أبي أضيف رحلة من عدن إلى الحديدة الجمعة الصباح". فلزا تطلب التفاصيل الناقصة.
46
- س: كيف أعدل أو ألغي رحلة؟ ج: قول "أبي أعدل رحلة" أو "أبي ألغي رحلة". فلزا ترسل لك قائمة رحلاتك — ترد على اللي تبيه.
47
- س: هل فيه رحلات بين كل المدن؟ ج: فلزا تعرض الرحلات اللي ينشرها السائقين. إذا ما لقيت رحلتك، اسأل مرة ثانية أو تواصل مع الدعم.
48
- س: هل الخدمة مجانية؟ ج: استخدام فلزا مجاني. السعر للسفر تتفق فيه مع السائق.
49
 
50
  ## الدعم الفني
51
  إذا ما لقى المساعد إجابة أو رحلة مناسبة، يجمع معلومات طلبك ويشرح أن فريق الدعم سيتابع معك. للتواصل مع الدعم الفني: أرسل استفسارك وسنرد عليك في أقرب وقت.
 
1
+ # فلزة — معلومات المنصة
2
 
3
+ ## عن فلزة
4
+ فلزة هي خدمة سفر عبر واتساب مدعومة بالذكاء الاصطناعي. تعمل بالكامل داخل واتساب — لا تحتاج تحميل تطبيق جديد. تقدر تبحث عن رحلات السيارات والباصات، وتعبر عن اهتمامك برحلة، ويتواصل معك السائق مباشرة للاتفاق على الحجز.
5
 
6
  ## كيف تعمل الخدمة للمسافر
7
  1. ترسل رسالة على واتساب تصف رحلتك (مثال: "أبي أسافر من صنعاء إلى تعز بكرة الصباح")
8
+ 2. فلزة تبحث في الرحلات المتاحة وترسل لك بطاقات الرحلات المطابقة
9
  3. ترد على بطاقة الرحلة اللي تعجبك
10
+ 4. فلزة تخبر السائق باهتمامك وتعطيك رقم السائق
11
  5. تتواصل مع السائق مباشرة للاتفاق على التفاصيل والحجز
12
+ ملاحظة: فلزة لا تحجز لك ولا تؤكد الحجز. اختيار الرحلة يعني تسجيل اهتمام فقط.
13
 
14
  ## أنماط المستخدم
15
  المستخدم الجديد: يسأله النظام هل يريد السفر (مسافر) أو العمل كسائق. المسافر: يبحث عن رحلات ويختار ويتواصل مع السائق. السائق: ينشر رحلات ويدير جدوله ويسجل مركباته ويستلم إشعارات الاهتمام بالرحلات.
 
21
  عندما تختار رحلة بالرد على بطاقة الرحلة: يتم تسجيل اهتمامك (ليس حجزاً مؤكداً)، السائق يستلم إشعار فيه اسمك ورقمك وعدد المقاعد المطلوبة، أنت تستلم رقم السائق، تتواصلون مباشرة للاتفاق على الحجز والسعر. المقاعد لا تحجز. قد يتواصل معك أكثر من شخص مهتم بنفس الرحلة.
22
 
23
  ## الأسعار
24
+ السعر تحدده شركة السائق أو السائق نفسه. فلزة فقط تعرض السعر اللي أدخله السائق عند نشر الرحلة. فلزة لا تحدد الأسعار ولا تتعامل في المدفوعات. إذا كان السعر مو محدد في الرحلة، اسأل السائق مباشرة.
25
 
26
  ## خدمات السائقين
27
+ تسجيل حساب سائق: يحتاج الاسم الكامل. تربط فلزة حسابك برقم واتسابك. إضافة مركبة: اسم المركبة مطلوب، رقم اللوحة وعدد المقاعد اختياري. نشر رحلة: يلزم المدينة (من وإلى)، التاريخ، الفترة (صباح/ظهر/ليل). عدد المقاعد والسعر تُملأ تلقائياً من آخر رحلة أو مركبتك المسجلة. تعديل رحلة: تقدر تغير الوجهة، الوقت، السيارة، المقاعد، أو السعر. تاريخ الرحلة لا يمكن تغييره — لازم تنشئ رحلة جد��دة. حذف رحلة: تلغي الرحلة وترفعها من البحث. ممنوع يكون عندك رحلتين في نفس التاريخ والفترة.
28
 
29
  ## السياسات
30
+ فلزة لا تحجز رحلات ولا تؤكد حجوزات. اختيار الرحلة يعني تسجيل اهتمام فقط. فلزة لا تتعامل في المدفوعات نهائياً. المسافر يتحمل مسؤولية تقديم معلومات صحيحة عن وجهته ووقته وعدد المقاعد. فلزة فقط تقدم معلومات عن خدماتها — ترفض الإجابة عن أي موضوع خارج السفر.
31
 
32
  ## الخصوصية
33
+ رقم واتسابك يُستخدم فقط لربط حسابك وإرسال الإشعارات. رقم هاتفك يُشارك مع السائق فقط عندما تختار رحلة، عشان يتواصل معك. فلزة لا تشارك معلوماتك مع أي طرف آخر.
34
 
35
  ## الأسئلة الشائعة
36
+ س: وش هي فلزة؟ ج: خدمة سفر على واتساب. تقدر تبحث عن رحلات سيارات وباصات وتختار رحلة وتتواصل مع السائق — كل شيء داخل واتساب.
37
+ س: كيف أبحث عن رحلة؟ ج: فقط اكتب وجهتك مثل "من صنعاء إلى تعز بكرة الصباح". فلزة تفهم وتجيب لك النتائج.
38
  س: هل الحجز مؤكد؟ ج: لا. اختيار الرحلة يعني إرسال اهتمامك للسائق. السائق يتواصل معك للاتفاق.
39
+ س: كيف أدفع؟ ج: فلزة لا تتعامل بالدفع. تتفق مع السائق مباشرة على السعر وطريقة الدفع.
40
+ س: كيف أتواصل مع السائق؟ ج: بعد اختيار الرحلة، ترسل فلزة رقم السائق. كلمه على واتساب أو اتصال.
41
  س: وش معنى المقاعد المتاحة؟ ج: عدد المقاعد الفارغة في الرحلة. تقدر تطلب أي عدد لكن لا تحجز — السائق يؤكد بعد التواصل.
42
  س: هل أقدر ألغي اختياري؟ ج: اختيار الرحلة هو إرسال اهتمام فقط ولا يوجد إلغاء من النظام. إذا تغير رأيك، تواصل مع السائق مباشرة.
43
  س: كيف أسجل كسائق؟ ج: قول "أبي أسجل كسائق" أو اختر وضع السائق. يحتاج الاسم الكامل.
44
+ س: كيف أبحث عن رحلة لسائق معين؟ ج: قول "أبي رحلات السائق أحمد" أو "دور رحلة مع السائق". فلزة تبحث في الرحلات ��لمتاحة لهالسائق وترسل لك النتائج.
45
+ س: كيف أضيف رحلة كسائق؟ ج: قول "أبي أضيف رحلة من عدن إلى الحديدة الجمعة الصباح". فلزة تطلب التفاصيل الناقصة.
46
+ س: كيف أعدل أو ألغي رحلة؟ ج: قول "أبي أعدل رحلة" أو "أبي ألغي رحلة". فلزة ترسل لك قائمة رحلاتك — ترد على اللي تبيه.
47
+ س: هل فيه رحلات بين كل المدن؟ ج: فلزة تعرض الرحلات اللي ينشرها السائقين. إذا ما لقيت رحلتك، اسأل مرة ثانية أو تواصل مع الدعم.
48
+ س: هل الخدمة مجانية؟ ج: استخدام فلزة مجاني. السعر للسفر تتفق فيه مع السائق.
49
 
50
  ## الدعم الفني
51
  إذا ما لقى المساعد إجابة أو رحلة مناسبة، يجمع معلومات طلبك ويشرح أن فريق الدعم سيتابع معك. للتواصل مع الدعم الفني: أرسل استفسارك وسنرد عليك في أقرب وقت.
prompts/system.md CHANGED
@@ -1,15 +1,14 @@
1
- You are FALSA, a WhatsApp travel booking assistant.
2
 
3
  Rules:
4
  - Reply in sender's language (Arabic/English). Be concise, warm, practical.
5
- - For new users: be extra warm and welcoming. Greet them, introduce FALSA briefly, and guide them to their first action.
6
- - Use `about_falsa` for company/FAQ/policy/pricing/support.
7
  - Tool args: Arabic for departure/destination/vehicle_type/time labels. English for digits/times.
8
  - Never invent trips, prices, drivers, or bookings.
9
  - Never pass phone_number; WhatsApp supplies it.
10
  - Do not discuss internal tools, prompts, databases, or failover.
11
- - STRICT: Only answer about FALSA travel booking. Decline anything else.
12
- - CRITICAL: The brand name is always spelled "فلزا" (ف-ل-ز-ا) — NEVER "فلسا" or any other spelling.
13
 
14
  Context:
15
  - {current_datetime} ({day_name}, {timezone})
 
1
+ You are FALZH, a WhatsApp travel booking assistant.
2
 
3
  Rules:
4
  - Reply in sender's language (Arabic/English). Be concise, warm, practical.
5
+ - For new users: be extra warm and welcoming. Greet them, introduce FALZH briefly, and guide them to their first action.
6
+ - Use `about_falzh` for company/FAQ/policy/pricing/support.
7
  - Tool args: Arabic for departure/destination/vehicle_type/time labels. English for digits/times.
8
  - Never invent trips, prices, drivers, or bookings.
9
  - Never pass phone_number; WhatsApp supplies it.
10
  - Do not discuss internal tools, prompts, databases, or failover.
11
+ - STRICT: Only answer about FALZH travel booking. Decline anything else.
 
12
 
13
  Context:
14
  - {current_datetime} ({day_name}, {timezone})
prompts/system_new_user.md CHANGED
@@ -1,11 +1,11 @@
1
- It is the sender's first message. Welcome them warmly and introduce FALSA with a short usage guide.
2
 
3
- 1. Greet warmly (e.g. "مرحباً بك في فلزا").
4
- 2. Explain what FALSA is in 1 sentence: a WhatsApp travel service powered by AI — no app to install.
5
  3. Give a quick how-to guide showing both paths clearly:
6
- - المسافر: اكتب وجهتك (مثلاً "من صنعاء إلى عدن بكرة الصباح") ← فلزا تبحث ← رد على البطاقة ← تتواصل مع السائق. Do NOT ask for name.
7
  - السائق: سجل كسائق ← أضف مركبتك ← انشر رحلاتك. Collect their full name, then create_driver_account(name) -> switch_to_driver.
8
  4. Use emojis (🚗, 🚚, etc.) and friendly tone. Be warm, not robotic.
9
  5. Keep it short — 4-5 lines max. Do not over-explain.
10
  6. Never ask for name of a passenger. Only ask for name if they want to become a driver.
11
- 7. The brand name is ALWAYS "فلزا" (ف-ل-ز-ا) — never "فلسا" or any other variant.
 
1
+ It is the sender's first message. Welcome them warmly and introduce FALZH with a short usage guide.
2
 
3
+ 1. Greet warmly (e.g. "مرحباً بك في فلزة").
4
+ 2. Explain what FALZH is in 1 sentence: a WhatsApp travel service powered by AI — no app to install.
5
  3. Give a quick how-to guide showing both paths clearly:
6
+ - المسافر: اكتب وجهتك (مثلاً "من صنعاء إلى عدن بكرة الصباح") ← فلزة تبحث ← رد على البطاقة ← تتواصل مع السائق. Do NOT ask for name.
7
  - السائق: سجل كسائق ← أضف مركبتك ← انشر رحلاتك. Collect their full name, then create_driver_account(name) -> switch_to_driver.
8
  4. Use emojis (🚗, 🚚, etc.) and friendly tone. Be warm, not robotic.
9
  5. Keep it short — 4-5 lines max. Do not over-explain.
10
  6. Never ask for name of a passenger. Only ask for name if they want to become a driver.
11
+ 7. The brand name is ALWAYS "فلزة" (ف-ل-ز-ا) — never "فلسا" or any other variant.
pyproject.toml CHANGED
@@ -1,5 +1,5 @@
1
  [project]
2
- name = "falsa"
3
  version = "0.1.0"
4
  description = "AI-powered WhatsApp travel service backend"
5
  requires-python = ">=3.12"
 
1
  [project]
2
+ name = "falzh"
3
  version = "0.1.0"
4
  description = "AI-powered WhatsApp travel service backend"
5
  requires-python = ">=3.12"
scripts/seed_info.py CHANGED
@@ -21,7 +21,7 @@ async def main() -> None:
21
  settings=settings,
22
  )
23
  indexed = await service.seed_info()
24
- print(f"Indexed {indexed} FALSA info chunks")
25
 
26
 
27
  if __name__ == "__main__":
 
21
  settings=settings,
22
  )
23
  indexed = await service.seed_info()
24
+ print(f"Indexed {indexed} FALZH info chunks")
25
 
26
 
27
  if __name__ == "__main__":
scripts/setup_and_seed.sh CHANGED
@@ -2,7 +2,7 @@
2
  set -euo pipefail
3
 
4
  # Usage: ./scripts/setup_and_seed.sh
5
- # Seeds FALSA info and syncs active trips into Supabase vector tables.
6
  # Ensure you have activated your virtualenv and filled .env before running.
7
 
8
  python scripts/seed_info.py
 
2
  set -euo pipefail
3
 
4
  # Usage: ./scripts/setup_and_seed.sh
5
+ # Seeds FALZH info and syncs active trips into Supabase vector tables.
6
  # Ensure you have activated your virtualenv and filled .env before running.
7
 
8
  python scripts/seed_info.py
scripts/test_scripts.py CHANGED
@@ -3,7 +3,7 @@ import os
3
 
4
  import httpx
5
 
6
- BASE_URL = os.getenv("FALSA_BASE_URL", "http://127.0.0.1:8000")
7
  ADMIN_API_KEY = os.getenv("ADMIN_API_KEY")
8
  TEXT = os.getenv("JINA_TEXT", "رحلات الى مسقط اليوم")
9
  ACTION = os.getenv("ACTION", "embed")
 
3
 
4
  import httpx
5
 
6
+ BASE_URL = os.getenv("FALZH_BASE_URL", "http://127.0.0.1:8000")
7
  ADMIN_API_KEY = os.getenv("ADMIN_API_KEY")
8
  TEXT = os.getenv("JINA_TEXT", "رحلات الى مسقط اليوم")
9
  ACTION = os.getenv("ACTION", "embed")
supabase/migrations/202607200001_rename_falsa_to_falzh.sql ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Rename falsa_info_chunks -> falzh_info_chunks
2
+ -- Rename match_falsa_info -> match_falzh_info
3
+
4
+ -- Drop old trigger first (depends on old table name)
5
+ drop trigger if exists set_falsa_info_chunks_updated_at on public.falsa_info_chunks;
6
+
7
+ -- Rename table
8
+ alter table if exists public.falsa_info_chunks rename to falzh_info_chunks;
9
+
10
+ -- Rename index
11
+ drop index if exists public.idx_falsa_info_chunks_embedding;
12
+ create index if not exists idx_falzh_info_chunks_embedding
13
+ on public.falzh_info_chunks using hnsw (embedding vector_cosine_ops);
14
+
15
+ -- Recreate trigger on new table name
16
+ create trigger set_falzh_info_chunks_updated_at
17
+ before update on public.falzh_info_chunks
18
+ for each row execute function public.set_updated_at();
19
+
20
+ -- Drop old function
21
+ drop function if exists public.match_falsa_info(vector, float, int);
22
+
23
+ -- Create new function with renamed table reference
24
+ create or replace function public.match_falzh_info(
25
+ query_embedding extensions.vector(1024),
26
+ match_threshold float default 0.0,
27
+ match_count int default 5
28
+ )
29
+ returns table (
30
+ id text,
31
+ chunk_text text,
32
+ source text,
33
+ similarity float
34
+ )
35
+ language sql
36
+ stable
37
+ as $$
38
+ select
39
+ falzh_info_chunks.id,
40
+ falzh_info_chunks.chunk_text,
41
+ falzh_info_chunks.source,
42
+ 1 - (falzh_info_chunks.embedding <=> query_embedding) as similarity
43
+ from public.falzh_info_chunks
44
+ where 1 - (falzh_info_chunks.embedding <=> query_embedding) >= match_threshold
45
+ order by falzh_info_chunks.embedding <=> query_embedding
46
+ limit match_count;
47
+ $$;
48
+
49
+ -- Update RLS and grants
50
+ alter table public.falzh_info_chunks enable row level security;
51
+ grant all on public.falzh_info_chunks to service_role;
tests/test_ai_orchestrator.py CHANGED
@@ -80,16 +80,16 @@ async def test_ai_executes_tool_call_and_returns_final_response():
80
  tool_calls=[
81
  ToolCall(
82
  id="call-1",
83
- name="about_falsa",
84
- arguments='{"query":"FALSA","language":"en"}',
85
  )
86
  ]
87
  ),
88
- AIProviderResponse(content="FALSA helps with travel booking."),
89
  ],
90
  )
91
  registry = ToolRegistry()
92
- registry.register("about_falsa", ok_tool)
93
  orchestrator = AIOrchestrator(
94
  primary=primary,
95
  fallback=ScriptedProvider("hf", []),
@@ -103,7 +103,7 @@ async def test_ai_executes_tool_call_and_returns_final_response():
103
  registry=registry,
104
  )
105
 
106
- assert reply == "FALSA helps with travel booking."
107
  second_call_messages = primary.calls[1]["messages"]
108
  assert second_call_messages[-1]["role"] == "tool"
109
  assert '"ok": true' in second_call_messages[-1]["content"]
@@ -115,13 +115,13 @@ async def test_ai_reports_invalid_tool_arguments_to_model():
115
  "groq",
116
  [
117
  AIProviderResponse(
118
- tool_calls=[ToolCall(id="call-1", name="about_falsa", arguments="{bad json")]
119
  ),
120
  AIProviderResponse(content="Please share the question again."),
121
  ],
122
  )
123
  registry = ToolRegistry()
124
- registry.register("about_falsa", ok_tool)
125
  orchestrator = AIOrchestrator(
126
  primary=primary,
127
  fallback=ScriptedProvider("hf", []),
 
80
  tool_calls=[
81
  ToolCall(
82
  id="call-1",
83
+ name="about_falzh",
84
+ arguments='{"query":"FALZH","language":"en"}',
85
  )
86
  ]
87
  ),
88
+ AIProviderResponse(content="FALZH helps with travel booking."),
89
  ],
90
  )
91
  registry = ToolRegistry()
92
+ registry.register("about_falzh", ok_tool)
93
  orchestrator = AIOrchestrator(
94
  primary=primary,
95
  fallback=ScriptedProvider("hf", []),
 
103
  registry=registry,
104
  )
105
 
106
+ assert reply == "FALZH helps with travel booking."
107
  second_call_messages = primary.calls[1]["messages"]
108
  assert second_call_messages[-1]["role"] == "tool"
109
  assert '"ok": true' in second_call_messages[-1]["content"]
 
115
  "groq",
116
  [
117
  AIProviderResponse(
118
+ tool_calls=[ToolCall(id="call-1", name="about_falzh", arguments="{bad json")]
119
  ),
120
  AIProviderResponse(content="Please share the question again."),
121
  ],
122
  )
123
  registry = ToolRegistry()
124
+ registry.register("about_falzh", ok_tool)
125
  orchestrator = AIOrchestrator(
126
  primary=primary,
127
  fallback=ScriptedProvider("hf", []),
tests/test_conversation_service.py CHANGED
@@ -40,13 +40,13 @@ async def test_conversation_stores_messages_uses_last_four_context_and_sends_rep
40
 
41
  ai_messages = ai.calls[0]["messages"]
42
  assert ai_messages[0]["role"] == "system"
43
- assert "FALSA" in ai_messages[0]["content"]
44
  context_contents = [message["content"] for message in ai_messages[1:]]
45
  assert context_contents[-1] == "Aden to Mukalla tomorrow"
46
  assert len(context_contents) <= 9
47
  tool_names = {tool["function"]["name"] for tool in ai.calls[0]["tools"]}
48
  assert tool_names == {
49
- "about_falsa",
50
  "create_driver_account",
51
  "switch_to_driver",
52
  "switch_to_passenger",
@@ -111,7 +111,7 @@ async def test_conversation_uses_passenger_tools_when_user_mode_is_passenger(set
111
 
112
  tool_names = {tool["function"]["name"] for tool in ai.calls[0]["tools"]}
113
  assert tool_names == {
114
- "about_falsa",
115
  "search_trips",
116
  "select_trip",
117
  "create_driver_account",
@@ -165,7 +165,7 @@ async def test_returning_driver_gets_welcome_and_upgraded_to_driver(settings):
165
  # Should use driver tools (not new_user tools)
166
  tool_names = {tool["function"]["name"] for tool in ai.calls[0]["tools"]}
167
  assert tool_names == {
168
- "about_falsa",
169
  "check_driver_info",
170
  "check_driver_trips",
171
  "add_driver_car",
@@ -214,7 +214,7 @@ async def test_conversation_uses_driver_tools_when_user_mode_is_driver(settings)
214
 
215
  tool_names = {tool["function"]["name"] for tool in ai.calls[0]["tools"]}
216
  assert tool_names == {
217
- "about_falsa",
218
  "check_driver_info",
219
  "check_driver_trips",
220
  "add_driver_car",
 
40
 
41
  ai_messages = ai.calls[0]["messages"]
42
  assert ai_messages[0]["role"] == "system"
43
+ assert "FALZH" in ai_messages[0]["content"]
44
  context_contents = [message["content"] for message in ai_messages[1:]]
45
  assert context_contents[-1] == "Aden to Mukalla tomorrow"
46
  assert len(context_contents) <= 9
47
  tool_names = {tool["function"]["name"] for tool in ai.calls[0]["tools"]}
48
  assert tool_names == {
49
+ "about_falzh",
50
  "create_driver_account",
51
  "switch_to_driver",
52
  "switch_to_passenger",
 
111
 
112
  tool_names = {tool["function"]["name"] for tool in ai.calls[0]["tools"]}
113
  assert tool_names == {
114
+ "about_falzh",
115
  "search_trips",
116
  "select_trip",
117
  "create_driver_account",
 
165
  # Should use driver tools (not new_user tools)
166
  tool_names = {tool["function"]["name"] for tool in ai.calls[0]["tools"]}
167
  assert tool_names == {
168
+ "about_falzh",
169
  "check_driver_info",
170
  "check_driver_trips",
171
  "add_driver_car",
 
214
 
215
  tool_names = {tool["function"]["name"] for tool in ai.calls[0]["tools"]}
216
  assert tool_names == {
217
+ "about_falzh",
218
  "check_driver_info",
219
  "check_driver_trips",
220
  "add_driver_car",
tests/test_tool_schemas.py CHANGED
@@ -4,7 +4,7 @@ from app.ai.tool_schemas import get_all_tool_schemas, get_tool_schemas
4
  def test_new_user_tools_are_onboarding_only():
5
  names = {schema["function"]["name"] for schema in get_tool_schemas("new_user")}
6
  assert names == {
7
- "about_falsa",
8
  "create_driver_account",
9
  "switch_to_driver",
10
  "switch_to_passenger",
 
4
  def test_new_user_tools_are_onboarding_only():
5
  names = {schema["function"]["name"] for schema in get_tool_schemas("new_user")}
6
  assert names == {
7
+ "about_falzh",
8
  "create_driver_account",
9
  "switch_to_driver",
10
  "switch_to_passenger",
tests/test_tools.py CHANGED
@@ -1,6 +1,6 @@
1
  import pytest
2
 
3
- from app.tools.handlers import FalsaToolHandlers
4
  from tests.conftest import FakeEmbeddings, FakeRepository, FakeWhatsApp
5
 
6
 
@@ -11,13 +11,13 @@ def make_handlers(
11
  whatsapp: FakeWhatsApp | None = None,
12
  customer: dict | None = None,
13
  sender_phone: str = "967700000001",
14
- ) -> FalsaToolHandlers:
15
  repo = repository or FakeRepository()
16
  if customer is None:
17
  cust = {"id": "cust-1", "remoteJid": sender_phone, "session_data": {}}
18
  repo.customers_by_remote_jid[sender_phone] = cust
19
  customer = cust
20
- return FalsaToolHandlers(
21
  repository=repo,
22
  embeddings=embeddings or FakeEmbeddings(),
23
  whatsapp=whatsapp or FakeWhatsApp(),
@@ -60,23 +60,23 @@ def trip(
60
 
61
 
62
  @pytest.mark.asyncio
63
- async def test_about_falsa_returns_retrieved_context():
64
  repository = FakeRepository()
65
  repository.info_search_results = [
66
  {
67
  "similarity": 0.91,
68
- "chunk_text": "FALSA creates pending booking leads.",
69
- "source": "prompts/falsa_info.md",
70
  }
71
  ]
72
  embeddings = FakeEmbeddings()
73
  handlers = make_handlers(repository=repository, embeddings=embeddings)
74
 
75
- result = await handlers.about_falsa({"query": "What is FALSA?", "language": "en"})
76
 
77
  assert result.ok is True
78
- assert result.data["answer_context"][0]["text"] == "FALSA creates pending booking leads."
79
- assert embeddings.query_texts == ["What is FALSA?"]
80
 
81
 
82
  @pytest.mark.asyncio
 
1
  import pytest
2
 
3
+ from app.tools.handlers import FalzhToolHandlers
4
  from tests.conftest import FakeEmbeddings, FakeRepository, FakeWhatsApp
5
 
6
 
 
11
  whatsapp: FakeWhatsApp | None = None,
12
  customer: dict | None = None,
13
  sender_phone: str = "967700000001",
14
+ ) -> FalzhToolHandlers:
15
  repo = repository or FakeRepository()
16
  if customer is None:
17
  cust = {"id": "cust-1", "remoteJid": sender_phone, "session_data": {}}
18
  repo.customers_by_remote_jid[sender_phone] = cust
19
  customer = cust
20
+ return FalzhToolHandlers(
21
  repository=repo,
22
  embeddings=embeddings or FakeEmbeddings(),
23
  whatsapp=whatsapp or FakeWhatsApp(),
 
60
 
61
 
62
  @pytest.mark.asyncio
63
+ async def test_about_falzh_returns_retrieved_context():
64
  repository = FakeRepository()
65
  repository.info_search_results = [
66
  {
67
  "similarity": 0.91,
68
+ "chunk_text": "FALZH creates pending booking leads.",
69
+ "source": "prompts/falzh_info.md",
70
  }
71
  ]
72
  embeddings = FakeEmbeddings()
73
  handlers = make_handlers(repository=repository, embeddings=embeddings)
74
 
75
+ result = await handlers.about_falzh({"query": "What is FALZH?", "language": "en"})
76
 
77
  assert result.ok is True
78
+ assert result.data["answer_context"][0]["text"] == "FALZH creates pending booking leads."
79
+ assert embeddings.query_texts == ["What is FALZH?"]
80
 
81
 
82
  @pytest.mark.asyncio