feat: rebrand FALSA to FALZH with updated supabase schema, tool names, and prompts
Browse files- .env.example +3 -3
- README.md +15 -15
- app/ai/tool_schemas.py +9 -9
- app/config.py +1 -1
- app/database/supabase.py +2 -2
- app/main.py +1 -1
- app/models/api.py +1 -1
- app/services/admin_service.py +1 -1
- app/services/conversation_service.py +4 -4
- app/tools/handlers.py +4 -4
- docker-compose.yml +1 -1
- prompts/{falsa_info.md → falzh_info.md} +19 -19
- prompts/system.md +4 -5
- prompts/system_new_user.md +5 -5
- pyproject.toml +1 -1
- scripts/seed_info.py +1 -1
- scripts/setup_and_seed.sh +1 -1
- scripts/test_scripts.py +1 -1
- supabase/migrations/202607200001_rename_falsa_to_falzh.sql +51 -0
- tests/test_ai_orchestrator.py +7 -7
- tests/test_conversation_service.py +5 -5
- tests/test_tool_schemas.py +1 -1
- tests/test_tools.py +9 -9
.env.example
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
APP_NAME=
|
| 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=
|
| 13 |
-
PINECONE_TRIPS_INDEX=
|
| 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 |
-
#
|
| 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="
|
| 9 |
</p>
|
| 10 |
|
| 11 |
-
|
| 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 |
-
|
| 27 |
|
| 28 |
-
- **Passengers** describe their trip in natural language;
|
| 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
|
| 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 ──►
|
| 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
|
| 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
|
| 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
|
| 136 |
|
| 137 |
### Run Tests
|
| 138 |
|
|
@@ -157,13 +157,13 @@ Mounts the current directory with hot-reload enabled.
|
|
| 157 |
|
| 158 |
## WhatsApp Baileys Bridge
|
| 159 |
|
| 160 |
-
|
| 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
|
| 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
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 6 |
"type": "function",
|
| 7 |
"function": {
|
| 8 |
-
"name": "
|
| 9 |
"description": (
|
| 10 |
-
"Retrieve official
|
| 11 |
),
|
| 12 |
"parameters": {
|
| 13 |
"type": "object",
|
| 14 |
"properties": {
|
| 15 |
"query": {
|
| 16 |
"type": "string",
|
| 17 |
-
"description": "The customer's question about
|
| 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
|
| 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 |
-
"
|
| 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 |
-
"
|
| 374 |
"create_driver_account",
|
| 375 |
"switch_to_driver",
|
| 376 |
"switch_to_passenger",
|
| 377 |
],
|
| 378 |
"driver": [
|
| 379 |
-
"
|
| 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 |
-
"
|
| 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 = "
|
| 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 |
-
"
|
| 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("
|
| 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="
|
| 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 = "
|
| 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/
|
| 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
|
| 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 =
|
| 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 |
-
"
|
| 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 =
|
| 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
|
| 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
|
| 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
|
| 78 |
"sources": [],
|
| 79 |
},
|
| 80 |
)
|
|
@@ -1080,7 +1080,7 @@ def _driver_notification_text(
|
|
| 1080 |
notes: str | None,
|
| 1081 |
) -> str:
|
| 1082 |
return (
|
| 1083 |
-
"🔔 اهتمام جديد برحلة في فلز
|
| 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 |
-
|
| 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
|
| 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
|
| 6 |
-
- Use `
|
| 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
|
| 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
|
| 2 |
|
| 3 |
-
1. Greet warmly (e.g. "مرحباً بك في فلز
|
| 4 |
-
2. Explain what
|
| 5 |
3. Give a quick how-to guide showing both paths clearly:
|
| 6 |
-
- المسافر: اكتب وجهتك (مثلاً "من صنعاء إلى عدن بكرة الصباح") ← فلز
|
| 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 "فلز
|
|
|
|
| 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 = "
|
| 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}
|
| 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
|
| 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("
|
| 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="
|
| 84 |
-
arguments='{"query":"
|
| 85 |
)
|
| 86 |
]
|
| 87 |
),
|
| 88 |
-
AIProviderResponse(content="
|
| 89 |
],
|
| 90 |
)
|
| 91 |
registry = ToolRegistry()
|
| 92 |
-
registry.register("
|
| 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 == "
|
| 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="
|
| 119 |
),
|
| 120 |
AIProviderResponse(content="Please share the question again."),
|
| 121 |
],
|
| 122 |
)
|
| 123 |
registry = ToolRegistry()
|
| 124 |
-
registry.register("
|
| 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 "
|
| 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 |
-
"
|
| 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 |
-
"
|
| 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 |
-
"
|
| 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 |
-
"
|
| 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 |
-
"
|
| 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
|
| 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 |
-
) ->
|
| 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
|
| 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
|
| 64 |
repository = FakeRepository()
|
| 65 |
repository.info_search_results = [
|
| 66 |
{
|
| 67 |
"similarity": 0.91,
|
| 68 |
-
"chunk_text": "
|
| 69 |
-
"source": "prompts/
|
| 70 |
}
|
| 71 |
]
|
| 72 |
embeddings = FakeEmbeddings()
|
| 73 |
handlers = make_handlers(repository=repository, embeddings=embeddings)
|
| 74 |
|
| 75 |
-
result = await handlers.
|
| 76 |
|
| 77 |
assert result.ok is True
|
| 78 |
-
assert result.data["answer_context"][0]["text"] == "
|
| 79 |
-
assert embeddings.query_texts == ["What is
|
| 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
|