From 598c157c90d12dcb20ad1ebc11b6207a83fc3216 Mon Sep 17 00:00:00 2001 From: Stephen Nwankwo Date: Sat, 4 Jul 2026 17:55:13 +0100 Subject: [PATCH 1/9] setup root dir dockerfile --- .dockerignore | 36 ++- Dockerfile | 95 ++++++ bloats/SUBSCRIPTION_PLAN.md | 366 ++++++++++++++++++++++++ bloats/add_column.py | 30 ++ bloats/api_docs.md | 253 ++++++++++++++++ bloats/business_plan.md | 36 +++ bloats/check_business.py | 19 ++ bloats/check_tx.py | 27 ++ bloats/drop_chat_session.py | 57 ++++ bloats/emergent_ventures_application.md | 90 ++++++ bloats/emergent_ventures_proposal.md | 69 +++++ bloats/populate_plans.py | 49 ++++ bloats/reindex_vectors.py | 71 +++++ bloats/reset_password.py | 34 +++ bloats/reset_vector_db.py | 27 ++ bloats/run_alter.py | 13 + bloats/security_incident_report.md | 48 ++++ bloats/simulate_webhook.py | 44 +++ bloats/test.txt | 1 + bloats/test_output.txt | 52 ++++ bloats/test_upload.txt | 1 + bloats/update_tiers.py | 27 ++ bloats/verify_fix_startup.py | 23 ++ bloats/verify_subscription.py | 69 +++++ docker-compose.yml | 33 ++- 25 files changed, 1556 insertions(+), 14 deletions(-) create mode 100644 Dockerfile create mode 100644 bloats/SUBSCRIPTION_PLAN.md create mode 100644 bloats/add_column.py create mode 100644 bloats/api_docs.md create mode 100644 bloats/business_plan.md create mode 100644 bloats/check_business.py create mode 100644 bloats/check_tx.py create mode 100644 bloats/drop_chat_session.py create mode 100644 bloats/emergent_ventures_application.md create mode 100644 bloats/emergent_ventures_proposal.md create mode 100644 bloats/populate_plans.py create mode 100644 bloats/reindex_vectors.py create mode 100644 bloats/reset_password.py create mode 100644 bloats/reset_vector_db.py create mode 100644 bloats/run_alter.py create mode 100644 bloats/security_incident_report.md create mode 100644 bloats/simulate_webhook.py create mode 100644 bloats/test.txt create mode 100644 bloats/test_output.txt create mode 100644 bloats/test_upload.txt create mode 100644 bloats/update_tiers.py create mode 100644 bloats/verify_fix_startup.py create mode 100644 bloats/verify_subscription.py diff --git a/.dockerignore b/.dockerignore index 97156f2..2ff924c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,38 @@ +# Dependencies **/node_modules **/.venv -**/.git -**/__pycache__ + +# Build outputs **/.next +**/__pycache__ +**/*.pyc +**/*.pyo + +# Version control +**/.git +**/.gitignore + +# Test artifacts **/.pytest_cache +**/tests +**/test_*.py +**/*_test.py +**/.coverage + +# Dev-only data +**/chroma_db +**/uploads +**/sql_app.db + +# Secrets — pass at runtime via env_file or environment +**/.env +**/.env.* + +# Misc +**/bloats +**/.vscode +**/.claude +**/postman_collection.json +**/lint_results*.txt +**/test_output.txt +**/test.txt diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f450f2c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,95 @@ +# ============================================================================= +# FRONTEND — build stages +# ============================================================================= + +FROM node:20-alpine AS frontend-deps +WORKDIR /app +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci + +FROM frontend-deps AS frontend-builder +WORKDIR /app +COPY frontend/ . + +ARG NEXT_PUBLIC_ENVIRONMENT +ARG NEXT_PUBLIC_API_URL +ARG NEXT_PUBLIC_BACKEND_URL_PROD +ARG NEXT_PUBLIC_FRONTEND_URL_PROD +ARG NEXT_PUBLIC_BACKEND_URL_STAGING +ARG NEXT_PUBLIC_FRONTEND_URL_STAGING +ARG NEXT_PUBLIC_BACKEND_URL_DEV +ARG NEXT_PUBLIC_FRONTEND_URL_DEV + +ENV NEXT_PUBLIC_ENVIRONMENT=$NEXT_PUBLIC_ENVIRONMENT \ + NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \ + NEXT_PUBLIC_BACKEND_URL_PROD=$NEXT_PUBLIC_BACKEND_URL_PROD \ + NEXT_PUBLIC_FRONTEND_URL_PROD=$NEXT_PUBLIC_FRONTEND_URL_PROD \ + NEXT_PUBLIC_BACKEND_URL_STAGING=$NEXT_PUBLIC_BACKEND_URL_STAGING \ + NEXT_PUBLIC_FRONTEND_URL_STAGING=$NEXT_PUBLIC_FRONTEND_URL_STAGING \ + NEXT_PUBLIC_BACKEND_URL_DEV=$NEXT_PUBLIC_BACKEND_URL_DEV \ + NEXT_PUBLIC_FRONTEND_URL_DEV=$NEXT_PUBLIC_FRONTEND_URL_DEV + +RUN npm run build + +FROM node:20-alpine AS frontend +WORKDIR /app + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +COPY --from=frontend-builder /app/public ./public +COPY --from=frontend-builder /app/.next/standalone ./ +COPY --from=frontend-builder /app/.next/static ./.next/static + +RUN chown -R nextjs:nodejs /app +USER nextjs + +ENV NODE_ENV=production \ + PORT=3000 \ + HOSTNAME="0.0.0.0" + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1 + +CMD ["node", "server.js"] + + +# ============================================================================= +# BACKEND — build stages +# ============================================================================= + +FROM python:3.10-slim AS backend-builder +WORKDIR /app + +RUN pip install --no-cache-dir uv +ENV UV_HTTP_TIMEOUT=300 + +COPY backend/pyproject.toml backend/uv.lock ./ +RUN uv sync --frozen --no-cache + +FROM python:3.10-slim AS backend +WORKDIR /app + +RUN pip install --no-cache-dir uv + +COPY --from=backend-builder /app/.venv /app/.venv + +ENV PATH="/app/.venv/bin:$PATH" \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 + +COPY backend/ . + +RUN adduser --disabled-password --gecos '' appuser && \ + chown -R appuser:appuser /app +USER appuser + +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", \ + "--workers", "1", "--proxy-headers", "--forwarded-allow-ips", "*"] diff --git a/bloats/SUBSCRIPTION_PLAN.md b/bloats/SUBSCRIPTION_PLAN.md new file mode 100644 index 0000000..b831895 --- /dev/null +++ b/bloats/SUBSCRIPTION_PLAN.md @@ -0,0 +1,366 @@ +To set up a subscription API for your FastAPI project using Paystack, you should follow a "Transaction-First" flow. This is the most reliable way to handle the initial payment and automatic subscription. + +### 1. High-Level Flow +1. **Selection:** User selects a plan (`nexus` or `flux`). +2. **Initialization:** Your API calls Paystack's **Initialize Transaction** endpoint with the specific `plan_code`. +3. **Payment:** The user is redirected to Paystack to pay. Upon successful payment, Paystack automatically creates a **Subscription** and a **Customer**. +4. **Notification:** Paystack sends a `subscription.create` and `charge.success` event to your **Webhook**. +5. **Provisioning:** Your webhook updates your database to grant the user access based on the plan. + +--- + +### 2. Implementation Steps + +#### Step 1: Configuration & Plan Mapping +Store your Paystack secret key and map your internal plan names to the Paystack `plan_code` you created in your dashboard. + +```python +# .env +PAYSTACK_SECRET_KEY=sk_test_xxxxxx +PAYSTACK_WEBHOOK_SECRET=xxxxxx # For signature verification + +# config.py +PLAN_MAPPING = { + "nexus": "PLN_nexus_code_here", + "flux": "PLN_flux_code_here" +} +``` + +#### Step 2: Database Models +You need to track the subscription status and the Paystack unique identifiers. + +```python +class User(Base): + __tablename__ = "users" + id = Column(Integer, primary_key=True) + email = Column(String, unique=True) + paystack_customer_code = Column(String, nullable=True) + subscription_code = Column(String, nullable=True) + subscription_status = Column(String, default="inactive") # active, non-renewing, attention, cancelled + plan_type = Column(String, nullable=True) # nexus, flux +``` + +#### Step 3: Paystack Service (FastAPI Utility) +Use `httpx` for asynchronous requests to Paystack. + +```python +import httpx +from .config import PAYSTACK_SECRET_KEY + +async def initialize_subscription(email: str, plan_code: str): + url = "https://api.paystack.co/transaction/initialize" + headers = {"Authorization": f"Bearer {PAYSTACK_SECRET_KEY}"} + payload = { + "email": email, + "plan": plan_code, + "callback_url": "https://yourfrontend.com/payment-verify" + } + async with httpx.AsyncClient() as client: + response = await client.post(url, json=payload, headers=headers) + return response.json() +``` + +#### Step 4: The Initialization Endpoint +This is what the user clicks to start their subscription. + +```python +@app.post("/subscribe/{plan_name}") +async def start_subscription(plan_name: str, current_user: User = Depends(get_current_user)): + if plan_name not in PLAN_MAPPING: + raise HTTPException(status_code=400, detail="Invalid plan") + + plan_code = PLAN_MAPPING[plan_name] + res = await initialize_subscription(current_user.email, plan_code) + + if res["status"]: + # Return the authorization_url to the frontend to redirect the user + return {"checkout_url": res["data"]["authorization_url"]} + raise HTTPException(status_code=400, detail="Could not initialize payment") +``` + +#### Step 5: Webhook Handler (Crucial for Security) +Paystack will notify your backend when payments occur or subscriptions are created. **You must verify the signature.** + +```python +import hmac +import hashlib + +@app.post("/paystack/webhook") +async def paystack_webhook(request: Request, x_paystack_signature: str = Header(None)): + # 1. Verify Signature + body = await request.body() + computed_signature = hmac.new( + PAYSTACK_WEBHOOK_SECRET.encode(), + body, + hashlib.sha512 + ).hexdigest() + + if computed_signature != x_paystack_signature: + raise HTTPException(status_code=401, detail="Invalid signature") + + payload = await request.json() + event = payload["event"] + data = payload["data"] + + # 2. Handle Events + if event == "subscription.create": + # Store the subscription_code and customer_code in DB + user_email = data["customer"]["email"] + # Update user record: set subscription_code, status='active' + + elif event == "charge.success": + # Initial or recurring payment succeeded + # Ensure user access is extended + + elif event == "subscription.disable" or event == "invoice.payment_failed": + # Revoke access or notify user + pass + + return {"status": "success"} +``` + +#### Step 6: Subscription Management (Cancellation) +To cancel, you need the `subscription_code` and `email_token` (returned by Paystack during the `subscription.create` event). + +```python +async def cancel_subscription(subscription_code: str, email_token: str): + url = "https://api.paystack.co/subscription/disable" + payload = {"code": subscription_code, "token": email_token} + headers = {"Authorization": f"Bearer {PAYSTACK_SECRET_KEY}"} + async with httpx.AsyncClient() as client: + response = await client.post(url, json=payload, headers=headers) + return response.status_code == 200 +``` + +#### Step 7: Subscription Management (Renewal) + + +### 3. Key Tips for Your Setup +* **Don't rely on the frontend callback:** The user might close their browser before the callback triggers. Always rely on the **Webhook** to update the subscription status in your database. +* **Customer Creation:** You don't need to manually create a customer first. Calling the `initialize` endpoint with an email automatically creates or retrieves the customer on Paystack's end. +* **Metadata:** You can pass `metadata: {"user_id": 123}` in the initialization request. This metadata will be sent back in the webhook payload, making it easier to link the payment to your local user ID. +* **Testing:** Use Paystack's test cards (e.g., the "Success" card) to trigger the `subscription.create` webhook locally using a tool like **ngrok** to expose your FastAPI server. + + +Subscription Renewal & Upgrade Implementation + +Since Paystack handles renewals automatically, your primary job is to listen for success/failure events and provide a flow for users to switch between your nexus and flux plans. + +1. Handling Automatic Renewals + +Paystack manages the billing cycle for you.[1][2] You only need to respond to the webhooks to keep your database in sync. + +Webhook Logic for Renewals + +When a renewal occurs, Paystack sends a charge.success event.[1][2][3] The payload will contain the plan code and the subscription_code. + +code +Python +download +content_copy +expand_less +# Inside your webhook handler +if event == "charge.success": + data = payload["data"] + # Check if this charge belongs to a subscription + subscription_code = data.get("plan", {}).get("subscription_code") + + if subscription_code: + # This is a renewal payment + user = db.query(User).filter(User.subscription_code == subscription_code).first() + if user: + user.subscription_status = "active" + user.last_payment_date = datetime.utcnow() + db.commit() + +elif event == "invoice.payment_failed": + # Renewal failed (e.g., insufficient funds) + subscription_code = data["subscription"]["subscription_code"] + user = db.query(User).filter(User.subscription_code == subscription_code).first() + if user: + user.subscription_status = "attention" # User has access but needs to pay + db.commit() +2. Plan Upgrades (e.g., Nexus → Flux) + +Paystack does not have a "Change Plan" button for an existing subscription ID.[3] To upgrade a user, you must disable the old subscription and create a new one. + +Strategy A: Immediate Upgrade (Charge Now) + +Use this if you want the user to pay the full price of the new plan immediately. + +Initialize Transaction: Call the initialize endpoint with the new plan_code.[1][3] + +Verify & Cleanup: Once the user pays, disable the old nexus subscription and store the new flux subscription code. + +Strategy B: Seamless Upgrade (Use Existing Card) + +If you already have the user's authorization_code (from their first payment), you can create the new subscription via API without the user typing their card details again. + +code +Python +download +content_copy +expand_less +async def upgrade_user_plan(user: User, new_plan_name: str): + # 1. Get the new plan code + new_plan_code = PLAN_MAPPING[new_plan_name] + + # 2. Disable current subscription first + # (Optional: You can wait for the new one to succeed first) + await cancel_subscription(user.subscription_code, user.email_token) + + # 3. Create new subscription using saved authorization + url = "https://api.paystack.co/subscription" + headers = {"Authorization": f"Bearer {PAYSTACK_SECRET_KEY}"} + payload = { + "customer": user.paystack_customer_code, + "plan": new_plan_code, + "authorization": user.last_authorization_code # Stored during first payment + } + + async with httpx.AsyncClient() as client: + res = await client.post(url, json=payload, headers=headers) + data = res.json() + if data["status"]: + user.subscription_code = data["data"]["subscription_code"] + user.plan_type = new_plan_name + # Paystack will charge the card immediately for the new plan + return {"message": "Upgraded successfully"} +3. Plan Downgrades + +Downgrades are usually handled at the end of the current billing cycle to avoid "loosing" money the user already paid. + +Request received: User clicks "Downgrade to Nexus".[3] + +Set to Cancel: Call the Paystack Disable Subscription endpoint, but only at the end of the period or use the "non-renewing" status logic. + +New Subscription: On the day the old plan expires, your system (via a background task or the subscription.disable webhook) creates the new lower-tier subscription. + +4. Important Considerations + +Proration: Paystack does not support native proration (e.g., charging only the $5 difference for half a month). Most developers either: + +Charge Full Price: Start the new plan immediately (simplest). + +Delay Upgrade: Wait until the current month ends, then switch. + +Retry Logic: Paystack does not retry failed subscription renewals.[1][2][3] If a renewal fails, the status becomes attention.[6] You should email the user with a payment link to reactivate.[1][7] + +Authorization Storage: Always save the authorization_code from the first successful charge.success event. You need this code to perform manual charges or programmatically create new subscriptions for that user later.[3] + + +Yes, you absolutely should. + +While you could hardcode the plan codes in your config.py, saving them to a local database table is the professional approach. Here is why it’s better and how to implement it: + +1. Why you should save them locally + +Performance: You can display your pricing page instantly without waiting for a Paystack API response. + +Feature Mapping: Paystack only stores the "price" and "interval." Your database can store the features (e.g., "nexus" gets 10 projects, "flux" gets unlimited). + +Data Integrity: You can create a foreign key relationship between your Subscriptions table and your Plans table. + +Dynamic Updates: If you decide to add a third plan (e.g., "spark") later, you can just click a "Sync Plans" button in your admin panel instead of redeploying code. + +2. Proposed Database Schema + +You should create a Plan model that mirrors the Paystack data but adds your own application-specific fields. + +code +Python +download +content_copy +expand_less +class Plan(Base): + __tablename__ = "plans" + + id = Column(Integer, primary_key=True) + name = Column(String, unique=True) # nexus, flux, spark + plan_code = Column(String, unique=True) # PLN_wfc5nh... + amount = Column(Integer) # Store in kobo (e.g., 1500000) + interval = Column(String) # monthly, annually + currency = Column(String, default="NGN") + + # App-specific fields (Not in Paystack) + is_active = Column(Boolean, default=True) + features = Column(JSON) # ["Unlimited Storage", "Priority Support"] + max_users = Column(Integer) # Internal limit for your logic +3. Implementation Plan: The "Sync" Script + +Create a utility function or a background task to keep your local database in sync with Paystack. + +code +Python +download +content_copy +expand_less +import httpx + +async def sync_paystack_plans(db: Session): + url = "https://api.paystack.co/plan" + headers = {"Authorization": f"Bearer {PAYSTACK_SECRET_KEY}"} + + async with httpx.AsyncClient() as client: + response = await client.get(url, headers=headers) + paystack_plans = response.json()["data"] + + for p in paystack_plans: + # Check if plan exists + db_plan = db.query(Plan).filter(Plan.plan_code == p["plan_code"]).first() + + if db_plan: + # Update existing plan (price/name might have changed) + db_plan.amount = p["amount"] + db_plan.name = p["name"] + else: + # Create new plan + new_plan = Plan( + name=p["name"], + plan_code=p["plan_code"], + amount=p["amount"], + interval=p["interval"], + currency=p["currency"] + # set default features here + ) + db.add(new_plan) + + db.commit() +4. How this changes your Subscription Flow + +Now, instead of passing a string to your initialization endpoint, you use the database ID. + +Before: POST /subscribe/nexus +After: POST /subscribe/{plan_id} + +code +Python +download +content_copy +expand_less +@app.post("/subscribe/{plan_id}") +async def start_subscription(plan_id: int, db: Session = Depends(get_db)): + plan = db.query(Plan).filter(Plan.id == plan_id).first() + if not plan: + raise HTTPException(status_code=404, detail="Plan not found") + + # Use the plan_code from the DB to initialize Paystack + res = await initialize_subscription(current_user.email, plan.plan_code) + return res +5. Important Warning: Price Changes + +If you change a plan's price on Paystack: + +Existing subscribers are not affected. They will continue to pay the old price they signed up with. + +New subscribers will pay the new price. + +Local DB: Your local plans table should be updated (via the sync script) so that your pricing page shows the correct new price to potential customers. + +Recommendation: + +Create the table. + +Add a "Sync" endpoint (protected for admins only). + +Manually add "features" to the rows in the database after syncing, or create a simple Admin UI to manage the extra metadata. diff --git a/bloats/add_column.py b/bloats/add_column.py new file mode 100644 index 0000000..6de8b78 --- /dev/null +++ b/bloats/add_column.py @@ -0,0 +1,30 @@ +import sqlite3 +import os + +db_path = 'sql_app.db' +print(f"Connecting to {db_path}...") + +if not os.path.exists(db_path): + print(f"Error: {db_path} not found!") + exit(1) + +try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + # Check if column exists first to avoid error if run multiple times + cursor.execute("PRAGMA table_info(chat_sessions)") + columns = [info[1] for info in cursor.fetchall()] + + if 'sentiment_score' in columns: + print("Column 'sentiment_score' already exists.") + else: + print("Adding column 'sentiment_score'...") + cursor.execute("ALTER TABLE chat_sessions ADD COLUMN sentiment_score FLOAT") + conn.commit() + print("Column added successfully.") + +except Exception as e: + print(f"Migration Error: {e}") +finally: + if conn: + conn.close() diff --git a/bloats/api_docs.md b/bloats/api_docs.md new file mode 100644 index 0000000..f7ee152 --- /dev/null +++ b/bloats/api_docs.md @@ -0,0 +1,253 @@ +# API Documentation + +Base URL: `http://localhost:8000` (or `http://localhost:8100` if running via Docker default in Makefile) + +## Authentication +All endpoints except authentication routes require a valid JWT token in the Authorization header: +``` +Authorization: Bearer +``` + +## Endpoints + +### Authentication + +#### 1. Sign Up +- **URL**: `/auth/signup` +- **Method**: `POST` +- **Content-Type**: `application/json` + +**Request Body**: +```json +{ + "email": "user@example.com", + "password": "securepassword", + "name": "John Doe" +} +``` + +#### 2. Login +- **URL**: `/auth/login` +- **Method**: `POST` +- **Content-Type**: `application/json` + +**Request Body**: +```json +{ + "email": "user@example.com", + "password": "securepassword" +} +``` + +**Response**: +```json +{ + "status": "success", + "data": { + "access_token": "eyJ...", + "refresh_token": "eyJ...", + "token_type": "bearer" + } +} +``` + +### Business Management + +#### 3. Create Business Profile +Create a business profile with custom agent configuration. + +- **URL**: `/business` +- **Method**: `POST` +- **Content-Type**: `application/json` +- **Auth**: Required + +**Request Body**: +```json +{ + "business_name": "Acme Support", + "description": "Customer support for Acme products", + "website": "https://acme.com", + "custom_agent_instruction": "Always be professional and mention our 24/7 support availability." +} +``` + +**Response**: +```json +{ + "status": "success", + "message": "Business profile created successfully", + "data": { + "id": "uuid", + "user_id": "uuid", + "business_name": "Acme Support", + "description": "Customer support for Acme products", + "website": "https://acme.com", + "custom_agent_instruction": "Always be professional and mention our 24/7 support availability.", + "created_at": "2025-12-08T00:00:00", + "updated_at": "2025-12-08T00:00:00" + } +} +``` + +#### 4. Get Business Profile +Retrieve the current user's business profile. + +- **URL**: `/business` +- **Method**: `GET` +- **Auth**: Required + +**Response**: +```json +{ + "status": "success", + "data": { + "id": "uuid", + "user_id": "uuid", + "business_name": "Acme Support", + "description": "Customer support for Acme products", + "website": "https://acme.com", + "custom_agent_instruction": "Always be professional and mention our 24/7 support availability.", + "created_at": "2025-12-08T00:00:00", + "updated_at": "2025-12-08T00:00:00" + } +} +``` + +#### 5. Update Business Profile +Update business profile fields. + +- **URL**: `/business` +- **Method**: `PUT` +- **Content-Type**: `application/json` +- **Auth**: Required + +**Request Body** (all fields optional): +```json +{ + "business_name": "Updated Name", + "description": "New description", + "website": "https://newsite.com", + "custom_agent_instruction": "New instructions for the agent" +} +``` + +### Document Management + +#### 6. Upload Documents +Uploads one or more files to the staging area. + +- **URL**: `/documents/upload` +- **Method**: `POST` +- **Content-Type**: `multipart/form-data` +- **Auth**: Required + +**Request Body**: +- `files`: List of files to upload (Binary) + +**Response**: +```json +{ + "status": "success", + "message": "Files uploaded successfully", + "data": { + "files": [ + "document1.pdf", + "notes.txt" + ] + } +} +``` + +#### 7. List Documents +Get all documents for the current user. + +- **URL**: `/documents` +- **Method**: `GET` +- **Auth**: Required + +**Response**: +```json +{ + "status": "success", + "data": [ + "document1.pdf", + "notes.txt" + ] +} +``` + +#### 8. Process Documents +Triggers the processing of uploaded documents (text splitting, embedding, and indexing). + +- **URL**: `/rag/process` +- **Method**: `POST` +- **Auth**: Required + +**Response**: +```json +{ + "status": "success", + "data": [ + { + "filename": "document1.pdf", + "chunks_created": 15, + "status": "success" + }, + { + "filename": "notes.txt", + "chunks_created": 8, + "status": "success" + } + ] +} +``` + +### Chat + +#### 9. Chat with Agent +Interact with the AI agent which uses your business configuration and processed documents as context. + +> **Note**: You must create a business profile before using this endpoint. + +- **URL**: `/chat` +- **Method**: `POST` +- **Content-Type**: `application/json` +- **Auth**: Required + +**Request Body**: +```json +{ + "message": "What are your support hours?" +} +``` + +**Response**: +```json +{ + "status": "success", + "data": { + "response": "We offer 24/7 support availability for all our customers. You can reach us anytime...", + "sources": [] + } +} +``` + +## Key Features + +- **User-Scoped Data**: Each user's documents and business configuration are isolated +- **Dynamic Agent**: Agent is created with your business name and custom instructions +- **Context-Aware**: Agent only accesses your uploaded documents +- **Session Management**: Conversation history is maintained per user + +## Running the API + +You can access the interactive Swagger UI at `/docs` (e.g., `http://localhost:8000/docs`) to test endpoints directly in the browser. + +## Workflow + +1. **Sign up** or **Login** to get authentication tokens +2. **Create a business profile** with your business details and custom agent instructions +3. **Upload documents** related to your business +4. **Process documents** to make them searchable +5. **Chat with the agent** - it will use your business context and documents to provide relevant responses + diff --git a/bloats/business_plan.md b/bloats/business_plan.md new file mode 100644 index 0000000..0094566 --- /dev/null +++ b/bloats/business_plan.md @@ -0,0 +1,36 @@ +# Taimako: Intelligent Customer Engagement for African SMEs + +## Executive Summary +Taimako is a next-generation customer engagement platform tailored specifically for the unique needs of Small and Medium-sized Enterprises (SMEs) in Africa. We democratize access to enterprise-grade Artificial Intelligence, enabling local businesses to automate customer support, capture valuable leads 24/7, and gain deep insights into their market—without the complexity or cost of traditional enterprise software. + +## The Problem +As African SMEs digitize, they face critical challenges in scaling their operations: +1. **Missed Opportunities**: Business owners cannot be online 24/7. Inquiries that come in after hours or during peak times often go unanswered, resulting in lost sales. +2. **Lack of Customer Insight**: Most SMEs rely on gut feeling rather than data. They know *how many* sales they made, but they rarely know *who* visited their site, *where* they came from, or *why* they didn't buy. +3. **Fragmented Communication**: Managing inquiries across WhatsApp, phone calls, and website forms is chaotic and inefficient. + +## The Solution: An Agent That Works for You +Taimako provides an "always-on" AI employee that lives on the business's website. It is not just a chatbot; it is a context-aware sales and support agent. + +### 1. Intelligent Automated Support +Our embedded widget acts as the first line of defense. It instantly answers customer questions about pricing, services, and business hours by "reading" the customized knowledge base provided by the business owner. It handles routine queries so the business owner can focus on high-value work. + +### 2. Actionable Business Intelligence +We turn passive website traffic into actionable data. Our dashboard provides clear, non-technical insights: +* **Visitor Intent**: Understand exactly *why* people are visiting (e.g., "70% of visitors asked about Delivery Pricing"). +* **Geographic Insights**: See where potential customers are located to better target marketing efforts. +* **Traffic Sources**: Identify whether customers are coming from Google, Instagram, or direct referrals. + +### 3. Lead Capture & Conversion +The AI proactively identifies high-intent visitors and captures their contact details (Name, Phone, Email), turning anonymous traffic into concrete leads. + +## Technology Foundation +To ensure reliability, speed, and scalability, Taimako is built on a modern, high-performance technology stack: +* **Backend**: Powered by **FastAPI**, ensuring that our AI processing is lightning-fast and capable of handling thousands of simultaneous conversations without lag. +* **Frontend**: Built with **Next.js**, delivering a seamless, app-like experience for both the business dashboard and the customer-facing widget, ensuring smooth performance on all devices, especially mobile, which is critical for the African market. + +## Market Opportunity +The African digital economy is exploding, with millions of SMEs coming online. These businesses are leapfrogging traditional desktop-first tools and demanding mobile-centric, automated, and intelligent solutions. Taimako is positioned to be the operating system for this new wave of digital commerce, providing the essential tools for growth in a competitive manufacturing and service landscape. + +## Conclusion +Taimako is more than software; it is a growth partner. By automating the "busy work" of customer service and illuminating the "blind spots" of customer behavior, we empower African entrepreneurs to scale their businesses faster and smarter. diff --git a/bloats/check_business.py b/bloats/check_business.py new file mode 100644 index 0000000..0eaf14b --- /dev/null +++ b/bloats/check_business.py @@ -0,0 +1,19 @@ +import os +import sys + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from app.db.session import engine +from sqlalchemy.orm import sessionmaker + +from app.models.business import Business + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +db = SessionLocal() + +businesses = db.query(Business).all() +for b in businesses: + print(f"Business: {b.business_name} | Email: {b.user.email if b.user else 'No User'}") + print(f"Tier: {b.subscription_tier}") + print(f"AI Responses: {b.allocated_ai_responses}") + print(f"Daily Sessions: {b.allocated_daily_sessions}") + print("-" * 40) diff --git a/bloats/check_tx.py b/bloats/check_tx.py new file mode 100644 index 0000000..6a29ad5 --- /dev/null +++ b/bloats/check_tx.py @@ -0,0 +1,27 @@ +import sys +import os +import json + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from app.db.session import engine +from sqlalchemy.orm import sessionmaker + +from app.models.payment import PaymentTransaction + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +db = SessionLocal() + +txs = db.query(PaymentTransaction).order_by(PaymentTransaction.created_at.desc()).limit(5).all() +for tx in txs: + print(f"TX {tx.id} - TYPE: {tx.transaction_type} - REF: {tx.reference} - CREATED_AT: {tx.created_at}") + if tx.transaction_metadata: + meta = tx.transaction_metadata + if isinstance(meta, str): + try: + meta = json.loads(meta) + except (ValueError, TypeError): + pass + + data = meta.get('data', {}) if isinstance(meta, dict) else {} + print("Metadata inside payload:", dict(data.get('metadata', {}))) + print("-" * 40) diff --git a/bloats/drop_chat_session.py b/bloats/drop_chat_session.py new file mode 100644 index 0000000..10b940b --- /dev/null +++ b/bloats/drop_chat_session.py @@ -0,0 +1,57 @@ +import sqlite3 + +def clean_db(): + conn = sqlite3.connect('sql_app.db') + cursor = conn.cursor() + + try: + print("Dropping chat_sessions table...") + cursor.execute("DROP TABLE IF EXISTS chat_sessions") + print("Dropped chat_sessions.") + except Exception as e: + print(f"Error dropping chat_sessions: {e}") + + # Dropping column in sqlite is tricky, usually requires recreating table. + # But since Alembic batch operations handle this, and the column might not exist if previous run failed halfway. + # Actually, if previous run failed on FK creation, the column might be there. + # But we can't easily drop column in pure sqlite without new table. + # However, if we just drop chat_sessions, the FK constraint from guest_messages might linger if it wasn't named? + # No, FK is on the guest_messages table. + # If I don't drop the column `session_id` from `guest_messages`, Alembic might fail saying column exists or similar. + # Alembic's `add_column` will fail if column exists. + + # Let's inspect if column exists. + cursor.execute("PRAGMA table_info(guest_messages)") + columns = [info[1] for info in cursor.fetchall()] + if 'session_id' in columns: + print("Column session_id exists in guest_messages.") + # We must remove it. + # SQLite way: create new table without col, copy, drop old, rename. + # Too risky to do manually? + # Maybe I should just manually modify the migration file to skip adding column if exists? + # NO, that's hacking. + # Let's try to remove it using sqlite approach in this script. + + print("Recreating guest_messages without session_id...") + # Get schema + cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='guest_messages'") + cursor.fetchone()[0] + # create_sql has the session_id definition? maybe. + # Easier: Rename table to _old, create new table (how to get schema?), copy, drop _old. + # Actually, let's just use the fact that I can use python to do this cleanly if I had the Model. + # But I don't want to load app stack. + + # Alternative: Just use alembic downgrade? + # I can try `alembic downgrade e2cd2430b4a5` (the base revision). + # But downgrading requires the migration to have been marked as done? It failed, so it's not marked? + # Or maybe it IS marked but failed? + # No, if it exits with 1, it probably didn't update alembic_version. + pass + else: + print("Column session_id does not exist.") + + conn.commit() + conn.close() + +if __name__ == "__main__": + clean_db() diff --git a/bloats/emergent_ventures_application.md b/bloats/emergent_ventures_application.md new file mode 100644 index 0000000..26cdc94 --- /dev/null +++ b/bloats/emergent_ventures_application.md @@ -0,0 +1,90 @@ +# Emergent Ventures Grant Application - TaimakoAI + +## 1. About You +**Instructions:** *The first part of the proposal should be about you. Tell us your personal story, and how it relates to what you wish to do. We probably don't care much about your formal education, credentials, or awards, unless they're particularly germane to who you are or your idea. Do tell us your background briefly, but credentials are not what will impress us.* + +[USER TO COMPLETE: Insert your personal story here. Focus on...] +* **Motivation:** Why do you care about African SMEs? Did you grow up in a family of small business owners? Have you seen the struggle of digitization first-hand? +* **The Spark:** What specific moment or experience led you to build Taimako? +* **Agency:** Examples of times you’ve taken initiative or built something from scratch. + +> **Drafting Tip:** "I grew up watching local businesses struggle not because they lacked quality products, but because they lacked the tools to communicate effectively at scale. My background in software engineering allows me to bridge this gap, but my drive comes from seeing the potential of the African digital economy unlocked." + +--- + +## 2. Consensus View +**Instructions:** *Second, what is one mainstream or "consensus" view that you absolutely agree with? (This is our version of a "trick" question, reversing the now-fashionable contrarianism.)* + +**Proposed Answer:** +"I absolutely agree with the consensus view that **talent is equally distributed, but opportunity is not.**" + +*Why this works:* This aligns perfectly with the mission of TaimakoAI (democratizing access to enterprise-grade AI for African SMEs). It positions your startup not just as a tech product, but as a mechanism for correcting this imbalance by providing the "opportunity" (tools) to the "talent" (SME owners). + +*Alternative Option:* +"I agree with the consensus that **software is eating the world**, but with the caveat that it hasn't finished its meal yet—especially in emerging markets where the 'last mile' of digitization is still being built." + +--- + +## 3. The Idea +**Instructions:** *The third part should be about your idea. Convince us that this is a great idea worth investing in, and tell us what is new or unusual in your vision and understanding. What's the problem you intend to solve?* + +**The Problem: The "Always-On" Gap in African Commerce** +As African SMEs digitize, they face a critical bottleneck: scaling human attention. Business owners are overwhelmed by inquiries across fragmented channels (WhatsApp, phone, Instagram), often missing sales that come in after hours or during peak times. They rely on gut feeling rather than data, lacking visibility into visitor intent ("70% asked about delivery") or traffic sources. Existing solutions are either too expensive (Enterprise SaaS) or too simple (basic FAQ bots). + +**The Solution: Taimako - The AI Employee for African SMEs** +Taimako is not just a chatbot; it is an intelligent, context-aware "AI employee" that lives on a business's website. It democratizes enterprise-grade AI for the African market by: + +1. **Intelligent Automation:** Instantly answering customer queries about pricing, services, and hours by "reading" the business's unique knowledge base. It handles the 80% of routine queries so owners can focus on high-value work. +2. **Actionable Intelligence:** Turning passive traffic into deep insights. Our dashboard visualizes visitor intent, sentiment, and geographic data, replacing "gut feeling" with data-driven decision making. +3. **Proactive Lead Capture:** Identifying high-intent visitors and capturing their contact details (Name, Phone, Email), turning anonymous browsers into concrete leads. +4. **Multi-Channel Integration:** Taimako meets customers where they are. Beyond the website widget, we provide seamless integration with **WhatsApp, Telegram, and Instagram**, allowing businesses to manage all their customer interactions from a single, unified dashboard. + +**What is New/Unusual?** +Most AI tools are built for the West and ported to Africa. Taimako is built *for* the African context, prioritizing: +* **Mobile-First Experiences:** Critical for a region where most internet access is mobile. +* **WhatsApp-Style Simplicity:** Mirroring the interfaces users already know and trust. +* **Lightweight Integration:** Optimized for slower connections and older devices. +We aren't just selling software; we are building the "operating system" for the next generation of African digital commerce. + +--- + +## Bonus: Taimako in a Tweet +**Option 1 (Focus on Problem/Solution):** +"African SMEs lose millions in sales because they can't be online 24/7. Taimako is the 'AI Employee' that lives on their site—answering queries, capturing leads, and closing sales while the owner sleeps. Enterprise-grade AI, built for the African context. 🚀🌍 #AfricanTech #AI #SME" + +**Option 2 (Focus on Empowerment):** +"Democratizing AI for African commerce. Taimako isn't just a chatbot; it's a context-aware sales agent that turns passive website traffic into revenue for SMEs. From WhatsApp to Web, we handle the busy work so entrepreneurs can scale. 📈🤖" + +--- + +## 4. Budget (Ballpark) +**Instructions:** *If you have a ballpark budget (with revenue sources and expenses), let us know the bare basics now; we won't hold you to it strictly.* + +**Estimated Monthly Run Rate (Year 1): $2,000 - $3,000** + +* **Infrastructure (AWS/DigitalOcean):** $500/mo (Hosting FastAPI backend, Next.js frontend, Postgres DB). +* **AI Inference (OpenAI/Anthropic/Local LLMs):** $1,000/mo (Scaling with user base). +* **Third-party Services (Email/SMS/Auth):** $200/mo. +* **Marketing & Outreach:** $500/mo. + +**Ask:** A grant of **$10,000 - $25,000** would allow us to: +1. Cover infrastructure costs for the first 12 months. +2. Aggressively onboard the first 100 beta users without charging them, accelerating our feedback loop. +3. Fine-tune our proprietary models on local dialects and commerce patterns. + +--- + +## 5. Project Status +**Instructions:** *Also (if applicable) tell us how long you have been working on this project or idea, whether you will be working on it full time or part time...* + +**Status:** +I have been working on TaimakoAI for [X months/weeks]. The MVP is currently **live in development**, featuring: +* Use of **FastAPI** for a high-performance backend. +* **Next.js** for a responsive, mobile-first frontend. +* Core features implemented: Customizable widgets, real-time analytics dashboard, and automated escalation workflows. + +**Commitment:** +I am working on this [Full-time / Part-time]. + +**Partners/Support:** +[Mention any co-founders or early advisors here. If solo, emphasize your ability to execute across the full stack.] diff --git a/bloats/emergent_ventures_proposal.md b/bloats/emergent_ventures_proposal.md new file mode 100644 index 0000000..6a3f526 --- /dev/null +++ b/bloats/emergent_ventures_proposal.md @@ -0,0 +1,69 @@ +# Emergent Ventures Grant Application - TaimakoAI + +## 1. About You +I am a software engineer deeply passionate about unlocking the potential of the African digital economy. Growing up, I watched brilliant local entrepreneurs with incredible products struggle to scale simply because they lacked the tools to communicate effectively and manage their operations efficiently. While the West debates the nuances of AGI, African SMEs are still trying to figure out how to answer customer queries at 2 AM without hiring a night shift. + +My technical background allows me to bridge this gap, but my drive comes from a conviction that technology should be an equalizer, not a gatekeeper. I built Taimako not as an academic exercise, but as a direct response to the "always-on" problem I saw friends and family facing in their businesses. I have a bias for action and a history of building practical solutions to real-world problems, and I am committed to making enterprise-grade AI accessible to the millions of small business owners who are the backbone of our economy. + +--- + +## 2. Consensus View +I absolutely agree with the consensus view that **talent is equally distributed, but opportunity is not.** + +This isn't just a philosophical stance; it is the core thesis of TaimakoAI. We are building tools to distribute opportunity. By giving a small fashion vendor in Lagos the same 24/7 customer service capabilities as a global e-commerce giant, we are leveling the playing field and allowing talent to rise based on merit, not infrastructure. + +--- + +## 3. The Idea +**The Problem: The "Always-On" Gap in African Commerce** +As African SMEs digitize, they face a critical bottleneck: scaling human attention. Business owners are overwhelmed by inquiries across fragmented channels (WhatsApp, phone, Instagram), often missing sales that come in after hours or during peak times. They rely on gut feeling rather than data, lacking visibility into visitor intent ("70% asked about delivery") or traffic sources. Existing solutions are either too expensive (Enterprise SaaS) or too simple (basic FAQ bots). + +**The Solution: Taimako - The AI Employee for African SMEs** +Taimako is an intelligent, context-aware "AI employee" that lives on a business's website and communication channels. It democratizes enterprise-grade AI for the African market by: + +1. **Intelligent Automation:** Instantly answering customer queries about pricing, services, and hours by "reading" the business's unique knowledge base. It handles the 80% of routine queries so owners can focus on high-value work. +2. **Actionable Intelligence:** Turning passive traffic into deep insights. Our dashboard visualizes visitor intent, sentiment, and geographic data, replacing "gut feeling" with data-driven decision making. +3. **Proactive Lead Capture:** Identifying high-intent visitors and capturing their contact details (Name, Phone, Email), turning anonymous browsers into concrete leads. +4. **Multi-Channel Integration:** Taimako meets customers where they are. We provide seamless integration with **WhatsApp, Telegram, and Instagram**, allowing businesses to manage all their customer interactions from a single, unified dashboard. + +**What is New/Unusual?** +Most AI tools are built for the West and ported to Africa. Taimako is built *for* the African context from day one. We prioritize **mobile-first experiences** (critical for a region where most internet access is mobile), **WhatsApp-style simplicity** (mirroring the interfaces users already know and trust), and **lightweight integration** optimized for slower connections and older devices. We aren't just selling software; we are building the "operating system" for the next generation of African digital commerce. + +--- + +## Bonus: Taimako in a Tweet +"African SMEs lose millions in sales because they can't be online 24/7. Taimako is the 'AI Employee' that lives on their site—answering queries, capturing leads, and closing sales while the owner sleeps. Enterprise-grade AI, built for the African context. 🚀🌍 #AfricanTech #AI #SME" + +--- + +## 4. Budget (Ballpark) +I am requesting a grant of **$25,000** to cover our runway for the next 12 months. + +**Estimated Monthly Run Rate (Year 1): ~$2,000** +* **Infrastructure (AWS/DigitalOcean):** $500/mo (Hosting FastAPI backend, Next.js frontend, Postgres DB). +* **AI Inference (OpenAI/Anthropic/Local LLMs):** $1,000/mo (Scaling with user base). +* **Third-party Services (Email/SMS/Auth):** $200/mo. +* **Marketing & Outreach:** $500/mo. + +**Impact of Funding:** +This grant will allow us to: +1. Cover infrastructure costs for the first year, removing financial pressure. +2. Aggressively onboard the first 100 beta users for free, accelerating our feedback loop without friction. +3. Fine-tune our proprietary models on local dialects and commerce patterns to increase accuracy and trust. + +--- + +## 5. Project Status +**Status:** +I have been working on TaimakoAI full-time for the past **3 months**. The MVP is currently **live in development** and functioning. + +**Key Features Implemented:** +* High-performance backend built with **FastAPI**. +* Responsive, mobile-first frontend built with **Next.js**. +* Customizable chat widgets, real-time analytics dashboard, and automated escalation workflows are fully operational. + +**Commitment:** +I am working on this project **Full-time**. + +**Partners/Support:** +I am currently a solo founder executing across the full stack (Backend, Frontend, AI Engineering). I am actively building a network of early adopters and advisors within the local SME community to guide product development. diff --git a/bloats/populate_plans.py b/bloats/populate_plans.py new file mode 100644 index 0000000..f84cbdf --- /dev/null +++ b/bloats/populate_plans.py @@ -0,0 +1,49 @@ +import sys +import os + +# Ensure app is in python path +sys.path.append(os.getcwd()) + +from app.db.session import SessionLocal +from app.models.plan import Plan +from app.core.subscription import TIER_LIMITS + +def populate_plans(): + db = SessionLocal() + try: + print("Starting plan population...") + for tier_name, features in TIER_LIMITS.items(): + plan_code = tier_name + name = tier_name.capitalize() + description = features.get("description", "") + + # Check if plan exists + existing_plan = db.query(Plan).filter(Plan.plan_code == plan_code).first() + if existing_plan: + print(f"Updating plan: {name}") + existing_plan.name = name + existing_plan.description = description + existing_plan.features = features + # Keep existing price/currency if set manually, otherwise defaults apply for new + else: + print(f"Creating plan: {name}") + new_plan = Plan( + plan_code=plan_code, + name=name, + description=description, + features=features, + price=0, + currency="NGN" + ) + db.add(new_plan) + + db.commit() + print("Plan population completed successfully.") + except Exception as e: + print(f"Error populating plans: {e}") + db.rollback() + finally: + db.close() + +if __name__ == "__main__": + populate_plans() diff --git a/bloats/reindex_vectors.py b/bloats/reindex_vectors.py new file mode 100644 index 0000000..c20f2e1 --- /dev/null +++ b/bloats/reindex_vectors.py @@ -0,0 +1,71 @@ +import sys +import os + +# Add backend directory to sys.path to allow imports +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from app.db.session import SessionLocal +from app.services.vector_db import vector_db +from app.services.rag_service import rag_service +from sqlalchemy import text + +def reindex(): + print("Starting Re-indexing Process...") + + # 1. Reset Vector DB + try: + print("Deleting existing vector collection 'rag_documents'...") + try: + vector_db.client.delete_collection("rag_documents") + print("✅ Collection deleted.") + except Exception as e: + # It might complain if it doesn't exist + print(f"⚠️ Delete collection message (proceeding): {e}") + + print("Recreating collection 'rag_documents'...") + # This updates the reference in the singleton vector_db instance too + vector_db.collection = vector_db.client.create_collection("rag_documents") + print("✅ Collection recreated.") + + except Exception as e: + print(f"❌ Error resetting vector DB: {e}") + return + + db = SessionLocal() + try: + # 2. Reset Document Status in Postgres + print("Resetting all document statuses to 'pending' in Postgres...") + + # Using SQLAlchemy Core for bulk update + db.execute(text("UPDATE documents SET status = 'pending', error_message = NULL")) + db.commit() + print("✅ Documents marked as pending.") + + # 3. Trigger Processing + print("Triggering processing for all users...") + + # Get all distinct user_ids with documents + # We need to process per user because RAG service expects user_id + result = db.execute(text("SELECT DISTINCT user_id FROM documents")) + user_ids = [row[0] for row in result] + + print(f"Found {len(user_ids)} users with documents.") + + for user_id in user_ids: + print(f"Processing documents for user: {user_id}...") + # process_documents handles fetching the API key via Business model + results = rag_service.process_documents(user_id, db) + + success_count = sum(1 for r in results if r.status == "success") + error_count = len(results) - success_count + print(f" User {user_id}: {success_count} succeeded, {error_count} failed.") + + print("\n✅ Re-indexing Complete!") + + except Exception as e: + print(f"\n❌ Error during re-indexing: {e}") + finally: + db.close() + +if __name__ == "__main__": + reindex() diff --git a/bloats/reset_password.py b/bloats/reset_password.py new file mode 100644 index 0000000..1ac5ec2 --- /dev/null +++ b/bloats/reset_password.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Quick script to reset a user's password.""" +import sys + +# Add the app directory to the path +sys.path.insert(0, '/app') + +from app.db.session import SessionLocal +from app.models.user import User +from app.core.security import get_password_hash + +def reset_password(email: str, new_password: str): + db = SessionLocal() + try: + user = db.query(User).filter(User.email == email).first() + if not user: + print(f"User with email {email} not found!") + return False + + user.hashed_password = get_password_hash(new_password) + db.commit() + print(f"Password reset successfully for {email}") + return True + finally: + db.close() + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python reset_password.py ") + sys.exit(1) + + email = sys.argv[1] + new_password = sys.argv[2] + reset_password(email, new_password) diff --git a/bloats/reset_vector_db.py b/bloats/reset_vector_db.py new file mode 100644 index 0000000..3c5e659 --- /dev/null +++ b/bloats/reset_vector_db.py @@ -0,0 +1,27 @@ +import chromadb +from app.core.config import settings + +def reset_vector_db(): + print(f"Connecting to ChromaDB at {settings.CHROMA_DB_DIR}...") + try: + client = chromadb.PersistentClient(path=settings.CHROMA_DB_DIR) + + # Try to get the collection to see if it exists + try: + client.get_collection(name="rag_documents") + print("Found existing collection 'rag_documents'. Deleting...") + client.delete_collection(name="rag_documents") + print("✅ Collection 'rag_documents' deleted.") + except Exception as e: + print(f"Collection 'rag_documents' not found or could not be accessed: {e}") + + print("Recreating collection 'rag_documents'...") + # We recreate it to ensure it's fresh and ready for new embeddings + client.create_collection(name="rag_documents") + print("✅ Collection 'rag_documents' created successfully.") + + except Exception as e: + print(f"❌ Error resetting vector DB: {e}") + +if __name__ == "__main__": + reset_vector_db() diff --git a/bloats/run_alter.py b/bloats/run_alter.py new file mode 100644 index 0000000..13534ee --- /dev/null +++ b/bloats/run_alter.py @@ -0,0 +1,13 @@ +import sys +import os +from sqlalchemy import text + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from app.db.session import engine + +with engine.begin() as conn: + try: + conn.execute(text("ALTER TABLE payment_transactions ADD COLUMN raw_webhook_payload JSON;")) + print("Successfully added column raw_webhook_payload to payment_transactions") + except Exception as e: + print(f"Error adding column: {e}") diff --git a/bloats/security_incident_report.md b/bloats/security_incident_report.md new file mode 100644 index 0000000..4f1b7e9 --- /dev/null +++ b/bloats/security_incident_report.md @@ -0,0 +1,48 @@ +# Security Incident Report +**Date:** January 31, 2026 +**Project:** TaimakoAI +**Severity:** Critical +**Status:** Resolved (Monitoring Required) + +## Executive Summary +On January 31, 2026, during routine maintenance, a **Critical Remote Code Execution (RCE)** vulnerability was detected in the frontend application. The vulnerability allowed unauthorized actors to execute system commands on the server. The attack vector was identified as a known security flaw in **Next.js 16.0.3**. Immediate remediation was performed by patching the software and restricting network configurations. + +## 1. Incident Details +- **Component:** `taimako_frontend` (Next.js Application) +- **Vulnerability Type:** Remote Code Execution (RCE) via Deserialization (CVE-2025-55182 / CVE-2025-66478) +- **Affected Version:** Next.js `16.0.3` +- **Detected:** January 31, 2026, 17:21 PM (local time) based on 502 Bad Gateway investigation. + +## 2. Root Cause Analysis +The application was running an outdated version of Next.js (`16.0.3`) which contained a critical vulnerability in the React Server Components (RSC) payload handling. +- **Mechanism:** Attackers sent maliciously crafted HTTP requests that the server deserialized, resulting in arbitrary shell command execution. +- **Exploitation:** Logs confirmed active exploitation where attackers ran commands to list directories and print environment variables. + +## 3. Detection & Evidence +The incident was discovered while investigating `502 Bad Gateway` errors. Review of the Docker logs (`docker logs taimako_frontend`) revealed: +- **Abnormal Error Dumps**: `NEXT_REDIRECT` errors containing output of system commands. +- **Command Execution**: + - `ls -la /var/www/.env*` (Attempting to locate secret files) + - `id`, `uname` (System reconnaissance) + - `base64` verification logic. +- **Environment Leak**: Error stack traces displayed the contents of environment variables, including configuration keys. + +## 4. Resolution & Mitigation +The following corrective actions were taken immediately: +1. **Software Patch**: Upgraded `next` dependency from `16.0.3` to `^16.0.7` (Current installed: `16.1.6`). +2. **Configuration Hardening**: + - Refactored Backend configuration to enforce **Strict CORS** policies in production. + - Centralized middleware management. +3. **Secret Rotation (Required User Action)**: + - Initiated rotation protocol for `POSTGRES_PASSWORD`, `JWT_SECRET`, and `GOOGLE_CLIENT_SECRET`. + +## 5. Impact Assessment +- **Data Confidentiality**: **High Risk**. Environment variables were exposed in logs. Secrets must be assumed compromised. +- **Data Integrity**: **Medium Risk**. Attackers had shell access, but no evidence of database deletion was found in the limited log window. +- **Availability**: **High Impact**. The attack caused the frontend service to crash repeatedly (502 errors). + +## 6. Recommendations & Next Steps +1. **Immediate**: Complete the rotation of all production secrets (Database, JWT, API Keys). +2. **Deployment**: Re-deploy all services with the patched Docker images. +3. **Monitoring**: Monitor logs for the next 48 hours for any "NEXT_REDIRECT" anomalies or suspicious IP activity. +4. **Process**: Implement a dependency scanning tool (e.g., Dependabot or Snyk) to catch upstream vulnerabilities earlier. diff --git a/bloats/simulate_webhook.py b/bloats/simulate_webhook.py new file mode 100644 index 0000000..d324a46 --- /dev/null +++ b/bloats/simulate_webhook.py @@ -0,0 +1,44 @@ +import os +import hmac +import hashlib +import json +import httpx +import sys + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from app.core.config import settings + +SECRET = settings.PAYSTACK_SECRET_KEY +if not SECRET: + SECRET = "sk_test_fake" + +payload = { + "event": "charge.success", + "data": { + "reference": "fake_ref_12345", + "amount": 5000, + "customer": { + "email": "test@venco.africa" + }, + "metadata": { + "is_upgrade": True, + "tier": "flux" + } + } +} + +body = json.dumps(payload).encode('utf-8') +signature = hmac.new( + key=SECRET.encode('utf-8'), + msg=body, + digestmod=hashlib.sha512 +).hexdigest() + +headers = { + "x-paystack-signature": signature, + "Content-Type": "application/json" +} + +resp = httpx.post("http://localhost:8000/webhooks/paystack", content=body, headers=headers) +print("Status:", resp.status_code) +print("Response:", resp.text) diff --git a/bloats/test.txt b/bloats/test.txt new file mode 100644 index 0000000..d670460 --- /dev/null +++ b/bloats/test.txt @@ -0,0 +1 @@ +test content diff --git a/bloats/test_output.txt b/bloats/test_output.txt new file mode 100644 index 0000000..56fc9f1 --- /dev/null +++ b/bloats/test_output.txt @@ -0,0 +1,52 @@ +============================= test session starts ============================== +platform darwin -- Python 3.13.2, pytest-9.0.1, pluggy-1.6.0 +rootdir: /Users/vencomacbook/Desktop/sten/TaimakoAI/backend +configfile: pyproject.toml +plugins: anyio-4.11.0, Faker-38.2.0 +collected 2 items + +tests/test_escalation_unit.py .. [100%] + +=============================== warnings summary =============================== +app/db/base.py:3 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/db/base.py:3: MovedIn20Warning: The ``declarative_base()`` function is now available as sqlalchemy.orm.declarative_base(). (deprecated since: 2.0) (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9) + Base = declarative_base() + +app/schemas/user.py:23 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/schemas/user.py:23: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ + class UserResponse(UserBase): + +.venv/lib/python3.13/site-packages/google/cloud/aiplatform/models.py:52 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/.venv/lib/python3.13/site-packages/google/cloud/aiplatform/models.py:52: FutureWarning: Support for google-cloud-storage < 3.0.0 will be removed in a future version of google-cloud-aiplatform. Please upgrade to google-cloud-storage >= 3.0.0. + from google.cloud.aiplatform.utils import gcs_utils + +.venv/lib/python3.13/site-packages/litellm/types/llms/anthropic.py:531 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/.venv/lib/python3.13/site-packages/litellm/types/llms/anthropic.py:531: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ + class AnthropicResponseContentBlockToolUse(BaseModel): + +.venv/lib/python3.13/site-packages/litellm/types/rag.py:181 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/.venv/lib/python3.13/site-packages/litellm/types/rag.py:181: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ + class RAGIngestRequest(BaseModel): + +app/services/agent_system/tool_schemas.py:7 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/services/agent_system/tool_schemas.py:7: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ + class GetContextInput(BaseModel): + +app/services/agent_system/tool_schemas.py:24 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/services/agent_system/tool_schemas.py:24: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ + class SayHelloInput(BaseModel): + +app/services/agent_system/tool_schemas.py:47 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/services/agent_system/tool_schemas.py:47: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ + class ContextOutput(BaseModel): + +app/services/agent_system/tool_schemas.py:67 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/services/agent_system/tool_schemas.py:67: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ + class GreetingOutput(BaseModel): + +app/services/agent_system/tool_schemas.py:82 + /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/services/agent_system/tool_schemas.py:82: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ + class FarewellOutput(BaseModel): + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +======================== 2 passed, 10 warnings in 0.07s ======================== diff --git a/bloats/test_upload.txt b/bloats/test_upload.txt new file mode 100644 index 0000000..d670460 --- /dev/null +++ b/bloats/test_upload.txt @@ -0,0 +1 @@ +test content diff --git a/bloats/update_tiers.py b/bloats/update_tiers.py new file mode 100644 index 0000000..d16917e --- /dev/null +++ b/bloats/update_tiers.py @@ -0,0 +1,27 @@ + +from app.db.session import SessionLocal +from app.models.plan import Plan + +def main(): + db = SessionLocal() + tiers_map = { + "spark": 1, + "ignite": 2, + "blaze": 3, + "inferno": 4 + } + + plans = db.query(Plan).all() + for plan in plans: + name = plan.name.lower() + if name in tiers_map: + plan.tier = tiers_map[name] + else: + plan.tier = 0 + + db.commit() + db.close() + print("Tiers updated successfully.") + +if __name__ == "__main__": + main() diff --git a/bloats/verify_fix_startup.py b/bloats/verify_fix_startup.py new file mode 100644 index 0000000..000b7cd --- /dev/null +++ b/bloats/verify_fix_startup.py @@ -0,0 +1,23 @@ + +import sys +import os + +# Add the project root to the python path +sys.path.append(os.getcwd()) + +try: + print("Attempting to import app.main...") + print("Successfully imported app.main without AttributeError.") +except AttributeError as e: + print(f"Caught expected AttributeError: {e}") + sys.exit(1) +except Exception as e: + print(f"Caught unexpected exception: {e}") + # We only care about the specific AttributeError for now, other errors might be due to missing env vars etc. + # But if it's the specific error we fixed, it shouldn't happen. + if "type object 'User' has no attribute 'is_model'" in str(e): + print("Verification FAILED: The specific AttributeError still exists.") + sys.exit(1) + else: + print("Verification PASSED (ignoring unrelated errors).") + sys.exit(0) diff --git a/bloats/verify_subscription.py b/bloats/verify_subscription.py new file mode 100644 index 0000000..0f13313 --- /dev/null +++ b/bloats/verify_subscription.py @@ -0,0 +1,69 @@ +from app.models.business import Business +from app.models.user import User +from app.db.session import SessionLocal + +# Mock the database session +db = SessionLocal() + +def verify_subscription_defaults(): + print("Verifying Subscription Defaults...") + # Clean up previous test + existing = db.query(Business).filter(Business.business_name == "Subscription Test").first() + if existing: + db.delete(existing) + db.commit() + + # Create User for test if needed + user = db.query(User).filter(User.email == "test@sub.com").first() + if not user: + user = User(email="test@sub.com") + db.add(user) + db.commit() + db.refresh(user) + + # Simulate Logic from API (renewing a spark plan) + business = Business( + user_id=user.id, + business_name="Subscription Test", + subscription_tier="spark", + allocated_ai_responses=100, + used_ai_responses=0, + allocated_escalations=5, + used_escalations=0, + allocated_messages_per_session=20, + allocated_daily_sessions=50, + allocated_whitelisted_domains=1 + ) + db.add(business) + db.commit() + db.refresh(business) + + assert business.subscription_tier == "spark" + assert business.allocated_ai_responses == 100 + assert business.used_ai_responses == 0 + assert business.allocated_escalations == 5 + print("✅ Defaults Verified") + return business + +def verify_responses_used(business_id): + print("Verifying AI Responses Deduction logic...") + + b = db.query(Business).filter(Business.id == business_id).first() + initial_used = b.used_ai_responses + b.used_ai_responses += 1 # simulated chat message sent + db.commit() + + b_refreshed = db.query(Business).filter(Business.id == business_id).first() + assert b_refreshed.used_ai_responses == initial_used + 1 + assert (b_refreshed.allocated_ai_responses - b_refreshed.used_ai_responses) == 99 + print("✅ AI Responses Used DB Persistence Verified") + +try: + business = verify_subscription_defaults() + verify_responses_used(business.id) + print("ALL TESTS PASSED") +except Exception as e: + print(f"TEST FAILED: {e}") +finally: + db.close() + diff --git a/docker-compose.yml b/docker-compose.yml index cb71698..0c5ac5a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,32 +18,41 @@ services: retries: 5 backend: - build: ./backend + build: + context: . + dockerfile: Dockerfile + target: backend container_name: taimako_backend + restart: unless-stopped ports: - "8000:8000" - volumes: - - ./backend:/app - - /app/.venv + env_file: .env environment: - - PYTHONUNBUFFERED=1 - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:${POSTGRES_PORT}/${POSTGRES_DB} depends_on: postgres: condition: service_healthy frontend: - build: ./frontend + build: + context: . + dockerfile: Dockerfile + target: frontend + args: + NEXT_PUBLIC_ENVIRONMENT: ${NEXT_PUBLIC_ENVIRONMENT:-production} + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-} + NEXT_PUBLIC_BACKEND_URL_PROD: ${NEXT_PUBLIC_BACKEND_URL_PROD:-} + NEXT_PUBLIC_FRONTEND_URL_PROD: ${NEXT_PUBLIC_FRONTEND_URL_PROD:-} + NEXT_PUBLIC_BACKEND_URL_STAGING: ${NEXT_PUBLIC_BACKEND_URL_STAGING:-} + NEXT_PUBLIC_FRONTEND_URL_STAGING: ${NEXT_PUBLIC_FRONTEND_URL_STAGING:-} + NEXT_PUBLIC_BACKEND_URL_DEV: ${NEXT_PUBLIC_BACKEND_URL_DEV:-} + NEXT_PUBLIC_FRONTEND_URL_DEV: ${NEXT_PUBLIC_FRONTEND_URL_DEV:-} container_name: taimako_frontend + restart: unless-stopped ports: - "3000:3000" - volumes: - - ./frontend:/app - - /app/node_modules - stdin_open: true - tty: true depends_on: - backend volumes: - postgres_data: \ No newline at end of file + postgres_data: From ce213eea0b9c1fb60af7b77a3b3fd03f223692c5 Mon Sep 17 00:00:00 2001 From: Stephen Nwankwo Date: Sat, 4 Jul 2026 18:18:20 +0100 Subject: [PATCH 2/9] priotise db url --- backend/app/db/session.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/app/db/session.py b/backend/app/db/session.py index 4dfa0a3..4554ab7 100644 --- a/backend/app/db/session.py +++ b/backend/app/db/session.py @@ -10,15 +10,15 @@ POSTGRES_PORT = os.getenv("POSTGRES_PORT", "5432") POSTGRES_DB = os.getenv("POSTGRES_DB") -# Build PostgreSQL URL if all required parts are present -if all([POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB]): +# DATABASE_URL takes priority — cloud platforms (Render, Railway, Heroku) provide this directly +if os.getenv("DATABASE_URL"): + SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL") +# Build from individual vars when DATABASE_URL is absent (docker-compose local dev) +elif all([POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB]): SQLALCHEMY_DATABASE_URL = ( f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}" ) -# Allow direct DATABASE_URL override (common on Heroku, Render, Railway, etc.) -elif os.getenv("DATABASE_URL"): - SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL") -# Final fallback: SQLite for quick local development, cause why not, eh? +# Final fallback: SQLite for quick local development else: DATABASE_PATH = os.getenv("DATABASE_PATH", "./sql_app.db") SQLALCHEMY_DATABASE_URL = f"sqlite:///{DATABASE_PATH}" From 01b2074fc9ae05602029c8d12715e2e3dd783128 Mon Sep 17 00:00:00 2001 From: Stephen Nwankwo Date: Sat, 4 Jul 2026 19:11:28 +0100 Subject: [PATCH 3/9] setup staging env url factory --- backend/app/core/config.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 96868fe..6412b64 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -54,19 +54,32 @@ class LocalConfig(BaseConfig): ] CORS_ALLOW_ORIGIN_REGEX: str = ".*" +class StagingConfig(BaseConfig): + FRONTEND_URI: str = "https://taimakoai.onrender.com" + FRONTEND_REDIRECT_URI: str = "https://taimakoai.onrender.com/auth/callback" + + CORS_ORIGINS: List[str] = [ + "https://taimakoai.onrender.com", + ] + + ALLOWED_HOSTS: List[str] = [ + "taimako.onrender.com", + "taimakoai.onrender.com", + ] + class ProductionConfig(BaseConfig): FRONTEND_URI: str = os.getenv("FRONTEND_LIVE_URI", "https://taimako.dubem.xyz") FRONTEND_REDIRECT_URI: str = f"{os.getenv('FRONTEND_LIVE_URI', 'https://taimako.dubem.xyz')}/auth/callback" - + CORS_ORIGINS: List[str] = [ - "https://taimako.dubem.xyz", - "https://www.taimako.dubem.xyz" + "https://taimako.dubem.xyz", + "https://www.taimako.dubem.xyz", ] - + ALLOWED_HOSTS: List[str] = [ - "taimako.dubem.xyz", + "taimako.dubem.xyz", "api.taimako.dubem.xyz", - "*.taimako.dubem.xyz" + "*.taimako.dubem.xyz", ] # USE_HTTPS_REDIRECT: bool = True # Uncomment if handling SSL termination directly @@ -76,8 +89,7 @@ def get_settings(): if env == "production": return ProductionConfig() elif env == "staging": - # Treating staging similar to prod for now, or can be its own class - return ProductionConfig() + return StagingConfig() return LocalConfig() settings = get_settings() From cef2e6c3613d01e514838136b4966e4a80e596a4 Mon Sep 17 00:00:00 2001 From: Stephen Nwankwo Date: Sat, 4 Jul 2026 19:23:15 +0100 Subject: [PATCH 4/9] update staging URLs on frontend --- frontend/src/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/config.ts b/frontend/src/config.ts index ffc1802..9b318cd 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -8,7 +8,7 @@ const getBackendUrl = (): string => { case 'production': return process.env.NEXT_PUBLIC_BACKEND_URL_PROD || 'https://api.taimako.dubem.xyz'; case 'staging': - return process.env.NEXT_PUBLIC_BACKEND_URL_STAGING || 'https://api.staging.taimako.ai'; + return process.env.NEXT_PUBLIC_BACKEND_URL_STAGING || 'https://taimako.onrender.com'; case 'dev': return process.env.NEXT_PUBLIC_BACKEND_URL_DEV || 'https://api.dev.taimako.ai'; case 'local': @@ -22,7 +22,7 @@ const getFrontendUrl = (): string => { case 'production': return process.env.NEXT_PUBLIC_FRONTEND_URL_PROD || 'https://taimako.dubem.xyz'; case 'staging': - return process.env.NEXT_PUBLIC_FRONTEND_URL_STAGING || 'https://app.staging.taimako.ai'; + return process.env.NEXT_PUBLIC_FRONTEND_URL_STAGING || 'https://taimakoai.onrender.com'; case 'dev': return process.env.NEXT_PUBLIC_FRONTEND_URL_DEV || 'https://app.dev.taimako.ai'; case 'local': From e22dcb97b16994b7d4bbbd7b51310e022f8db12d Mon Sep 17 00:00:00 2001 From: Stephen Nwankwo Date: Sat, 4 Jul 2026 19:59:07 +0100 Subject: [PATCH 5/9] feat: Add start script for backend and update Dockerfile to use it --- Dockerfile | 6 +++--- backend/start.sh | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 backend/start.sh diff --git a/Dockerfile b/Dockerfile index f450f2c..b3ad10a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -83,7 +83,8 @@ ENV PATH="/app/.venv/bin:$PATH" \ COPY backend/ . RUN adduser --disabled-password --gecos '' appuser && \ - chown -R appuser:appuser /app + chown -R appuser:appuser /app && \ + chmod +x /app/start.sh USER appuser EXPOSE 8000 @@ -91,5 +92,4 @@ EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1 -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", \ - "--workers", "1", "--proxy-headers", "--forwarded-allow-ips", "*"] +CMD ["/app/start.sh"] diff --git a/backend/start.sh b/backend/start.sh new file mode 100644 index 0000000..1b64f76 --- /dev/null +++ b/backend/start.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -e +alembic upgrade head +exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 1 --proxy-headers --forwarded-allow-ips '*' From 11af3ea67dd8d7661d96085c8774f2322ab76d93 Mon Sep 17 00:00:00 2001 From: Stephen Nwankwo Date: Sat, 4 Jul 2026 20:16:15 +0100 Subject: [PATCH 6/9] fix: Simplify DATABASE_URL handling in Alembic env.py --- backend/alembic/env.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/backend/alembic/env.py b/backend/alembic/env.py index ebdb23b..1b67a80 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -24,14 +24,11 @@ POSTGRES_PORT = os.getenv("POSTGRES_PORT", "5432") POSTGRES_DB = os.getenv("POSTGRES_DB") -if all([POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB]): +if os.getenv("DATABASE_URL"): + config.set_main_option("sqlalchemy.url", os.getenv("DATABASE_URL")) +elif all([POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB]): database_url = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}" config.set_main_option("sqlalchemy.url", database_url) -else: - # Fallback: if individual vars are missing, try the full DATABASE_URL (e.g., from Heroku, Render, Railway) - fallback_url = os.getenv("DATABASE_URL") - if fallback_url: - config.set_main_option("sqlalchemy.url", fallback_url) # Logging setup if config.config_file_name is not None: From 4db2dd0ff5ac738cfceca48de96c4961030f0253 Mon Sep 17 00:00:00 2001 From: Stephen Nwankwo Date: Sat, 4 Jul 2026 20:30:12 +0100 Subject: [PATCH 7/9] run lint --- backend/alembic/env.py | 15 +- .../2ca6c95dd95b_added_is_active_column.py | 1 - backend/app/api/analytics.py | 17 +-- backend/app/api/escalation.py | 1 - backend/app/api/routes.py | 2 - backend/app/api/widget.py | 21 ++- backend/app/auth/router.py | 16 +-- backend/app/core/exception_handler.py | 1 - backend/app/core/security_headers.py | 2 +- backend/app/core/security_utils.py | 1 - backend/app/main copy.py | 111 --------------- backend/app/main.py | 19 +-- backend/app/models/chat_session.py | 2 +- backend/app/models/document.py | 3 +- backend/app/models/escalation.py | 2 +- backend/app/models/user.py | 1 - backend/app/models/widget.py | 2 - backend/app/schemas/document.py | 2 +- backend/app/schemas/widget.py | 1 - backend/app/services/agent_service copy 2.py | 131 ------------------ backend/app/services/agent_service.py | 26 ++-- backend/app/services/agent_system/agents.py | 1 - .../app/services/agent_system/callbacks.py | 4 +- backend/app/services/agent_system/tools.py | 1 - backend/app/services/analysis_agent.py | 5 +- backend/app/services/rag_service.py | 2 - backend/app/utils/text_splitter.py | 1 - backend/reset_password.py | 2 - backend/test_agent_refactor.py | 1 - backend/test_analysis_integration.py | 5 +- backend/tests/conftest.py | 3 +- backend/tests/test_api.py | 1 - backend/tests/test_api_scoped.py | 3 +- backend/tests/test_auth.py | 5 +- backend/tests/test_auth_local.py | 3 - backend/tests/test_automated_analysis.py | 3 +- backend/tests/test_business.py | 2 - backend/tests/test_escalation_unit.py | 4 - backend/tests/test_rag_scoped.py | 1 - backend/tests/tools/test_analyze_sentiment.py | 1 - backend/tests/tools/test_say_goodbye.py | 1 - backend/tests/tools/test_say_hello.py | 1 - 42 files changed, 55 insertions(+), 372 deletions(-) delete mode 100644 backend/app/main copy.py delete mode 100644 backend/app/services/agent_service copy 2.py diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 1b67a80..278f397 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -5,14 +5,13 @@ from alembic import context from app.db.base import Base -# Import all your models here so they are registered with Base.metadata -from app.models.user import User -from app.models.document import Document -from app.models.business import Business -from app.models.widget import WidgetSettings, GuestUser, GuestMessage -from app.models.chat_session import ChatSession -from app.models.analytics import AnalyticsDailySummary -from app.models.escalation import Escalation +from app.models.user import User # noqa: F401 +from app.models.document import Document # noqa: F401 +from app.models.business import Business # noqa: F401 +from app.models.widget import WidgetSettings, GuestUser, GuestMessage # noqa: F401 +from app.models.chat_session import ChatSession # noqa: F401 +from app.models.analytics import AnalyticsDailySummary # noqa: F401 +from app.models.escalation import Escalation # noqa: F401 # Alembic Config object config = context.config diff --git a/backend/alembic/versions/2ca6c95dd95b_added_is_active_column.py b/backend/alembic/versions/2ca6c95dd95b_added_is_active_column.py index ef1a901..19e1dc1 100644 --- a/backend/alembic/versions/2ca6c95dd95b_added_is_active_column.py +++ b/backend/alembic/versions/2ca6c95dd95b_added_is_active_column.py @@ -9,7 +9,6 @@ from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. revision: str = '2ca6c95dd95b' diff --git a/backend/app/api/analytics.py b/backend/app/api/analytics.py index b2e3b1a..8cbc5cc 100644 --- a/backend/app/api/analytics.py +++ b/backend/app/api/analytics.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from sqlalchemy import func -from typing import List, Optional +from typing import Optional from datetime import datetime, timedelta from app.db.session import get_db @@ -13,6 +13,7 @@ from app.services.analysis_agent import generate_followup_content from app.models.widget import GuestMessage from app.core.response_wrapper import success_response +from app.core.security_utils import decrypt_string router = APIRouter() @@ -46,12 +47,6 @@ def get_analytics_overview( total_sessions = query.count() # Guests - guests_query = db.query(GuestUser).filter( - GuestUser.widget_id == widget.id, - GuestUser.created_at >= start_date # Logic for 'New Visitors' in period? Or Active? - # Standard: Total Unique Visitors in period (based on session activity or creation?) - # Let's use Active Visitors (had a session in period) - ) # Better: Count distinct Guest IDs in sessions query total_guests = query.with_entities(ChatSession.guest_id).distinct().count() @@ -59,7 +54,7 @@ def get_analytics_overview( leads_captured = db.query(GuestUser).join(ChatSession).filter( GuestUser.widget_id == widget.id, ChatSession.created_at >= start_date, - GuestUser.is_lead == True + GuestUser.is_lead ).distinct().count() # Avg Duration @@ -74,7 +69,7 @@ def get_analytics_overview( returning_guests_count = db.query(GuestUser).join(ChatSession).filter( GuestUser.widget_id == widget.id, ChatSession.created_at >= start_date, - GuestUser.is_returning == True + GuestUser.is_returning ).distinct().count() returning_percentage = 0 @@ -186,10 +181,6 @@ class FollowUpRequest(BaseModel): type: str # "email" or "transcript" extra_info: Optional[str] = "" -from app.core.security_utils import decrypt_string - -# ... - @router.post("/followup", response_model=None) async def generate_followup( request: FollowUpRequest, diff --git a/backend/app/api/escalation.py b/backend/app/api/escalation.py index 1c1f628..7e4a7b6 100644 --- a/backend/app/api/escalation.py +++ b/backend/app/api/escalation.py @@ -4,7 +4,6 @@ from app.core.response_wrapper import success_response from app.models.escalation import Escalation, EscalationStatus from app.models.widget import GuestMessage -from app.api.business import get_business router = APIRouter() diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py index 85eb139..250401d 100644 --- a/backend/app/api/routes.py +++ b/backend/app/api/routes.py @@ -6,10 +6,8 @@ from typing import List from app.services.rag_service import rag_service from app.services.agent_service import run_conversation -from app.schemas.document import IngestResponse from app.schemas.chat import ChatRequest, ChatResponse from app.core.security_utils import decrypt_string -import asyncio from app.core.response_wrapper import success_response diff --git a/backend/app/api/widget.py b/backend/app/api/widget.py index 27b807f..106fba6 100644 --- a/backend/app/api/widget.py +++ b/backend/app/api/widget.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, status, Request +from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.orm import Session from typing import List, Optional import uuid @@ -8,8 +8,7 @@ from app.db.session import get_db from app.models.widget import WidgetSettings, GuestUser, GuestMessage from app.models.user import User -from app.models.business import Business -from app.models.chat_session import ChatSession, SessionOrigin +from app.models.chat_session import ChatSession from app.schemas.widget import ( GuestStartRequest, GuestStartResponse, WidgetChatRequest, WidgetChatResponse, GuestMessageSchema, @@ -19,13 +18,12 @@ from app.services.agent_service import run_conversation from app.auth.router import get_current_user from app.core.response_wrapper import success_response +from app.services.analysis_agent import analyze_session, persist_analysis from app.core.security_utils import decrypt_string -from datetime import timedelta # Additional Schema for Updating Settings from pydantic import BaseModel -from urllib.parse import urlparse from app.core.config import settings router = APIRouter() @@ -475,7 +473,7 @@ async def init_guest_session( print(f"Skipping geolocation for localhost IP: {client_ip}") except httpx.TimeoutException: - print(f"GeoIP Lookup Timeout") + print("GeoIP Lookup Timeout") except httpx.HTTPError as e: print(f"GeoIP HTTP Error: {e}") except Exception as e: @@ -642,9 +640,12 @@ async def process_chat_message(db: Session, widget: WidgetSettings, guest: Guest # 5. Update Session Stats session = db.query(ChatSession).filter(ChatSession.id == session_id).first() if session: - if session.total_messages is None: session.total_messages = 0 - if session.user_messages is None: session.user_messages = 0 - if session.ai_messages is None: session.ai_messages = 0 + if session.total_messages is None: + session.total_messages = 0 + if session.user_messages is None: + session.user_messages = 0 + if session.ai_messages is None: + session.ai_messages = 0 session.total_messages += 2 # 1 user + 1 AI session.user_messages += 1 @@ -744,8 +745,6 @@ def get_session_details_widget( ] }) -from app.services.analysis_agent import analyze_session, persist_analysis - @router.post("/session/{session_id}/analyze", response_model=None) async def analyze_chat_session(session_id: str, db: Session = Depends(get_db)): # 1. Verify session exists diff --git a/backend/app/auth/router.py b/backend/app/auth/router.py index 81f03e6..eeb36b9 100644 --- a/backend/app/auth/router.py +++ b/backend/app/auth/router.py @@ -1,7 +1,7 @@ -from fastapi import APIRouter, Depends, HTTPException, status, Request +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.responses import RedirectResponse +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from sqlalchemy.orm import Session -from fastapi.responses import RedirectResponse, JSONResponse -from typing import Optional from app.db.session import get_db from app.models.user import User @@ -13,16 +13,6 @@ router = APIRouter(prefix="/auth", tags=["auth"]) -# --- Dependency: Get Current User --- -async def get_current_user(token: str = Depends(verify_token), db: Session = Depends(get_db)) -> User: - # Note: verify_token dependency above is a placeholder. - # In FastAPI, we usually use OAuth2PasswordBearer to extract token. - # But for simplicity here, we'll extract manually or assume middleware. - # Let's fix this to be a proper dependency. - pass - -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials - security = HTTPBearer() async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db)) -> User: diff --git a/backend/app/core/exception_handler.py b/backend/app/core/exception_handler.py index c8477a5..6733c13 100644 --- a/backend/app/core/exception_handler.py +++ b/backend/app/core/exception_handler.py @@ -2,7 +2,6 @@ from fastapi.exceptions import HTTPException, RequestValidationError from fastapi.responses import JSONResponse from app.core.response_wrapper import error_response -from typing import Union def get_error_code(status_code: int) -> str: diff --git a/backend/app/core/security_headers.py b/backend/app/core/security_headers.py index 32a4f48..18cbf5e 100644 --- a/backend/app/core/security_headers.py +++ b/backend/app/core/security_headers.py @@ -1,5 +1,5 @@ from starlette.middleware.base import BaseHTTPMiddleware -from starlette.types import ASGIApp, Receive, Scope, Send +from starlette.types import ASGIApp import os class SecurityHeadersMiddleware(BaseHTTPMiddleware): diff --git a/backend/app/core/security_utils.py b/backend/app/core/security_utils.py index 38f6151..8c3349a 100644 --- a/backend/app/core/security_utils.py +++ b/backend/app/core/security_utils.py @@ -1,5 +1,4 @@ import base64 -import os from typing import Optional from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes diff --git a/backend/app/main copy.py b/backend/app/main copy.py deleted file mode 100644 index bc7a5a7..0000000 --- a/backend/app/main copy.py +++ /dev/null @@ -1,111 +0,0 @@ -from fastapi import FastAPI, Request -from fastapi.exceptions import HTTPException, RequestValidationError -from fastapi.middleware.cors import CORSMiddleware -from fastapi.middleware.trustedhost import TrustedHostMiddleware -from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware -from slowapi import Limiter, _rate_limit_exceeded_handler -from slowapi.util import get_remote_address -from slowapi.errors import RateLimitExceeded -from slowapi.middleware import SlowAPIMiddleware -import os - -from app.api.routes import router as api_router -from app.auth.router import router as auth_router -from app.db.base import Base -from app.db.session import engine -from app.core.exception_handler import ( - http_exception_handler, - validation_exception_handler, - general_exception_handler -) -from app.core.security_headers import SecurityHeadersMiddleware - -# Create tables (if not using alembic, but we are. Keeping for dev convenience or removing if strictly alembic) -# Base.metadata.create_all(bind=engine) - -# Initialize Rate Limiter -limiter = Limiter(key_func=get_remote_address, default_limits=["100/minute"]) - -app = FastAPI( - title="Agentic RAG API", - description="API for Agentic RAG with Google OAuth2 and Multi-Agent Delegation.", - version="1.0.0", - docs_url="/docs", - redoc_url="/redoc" -) - -# Associate limiter with app -app.state.limiter = limiter -app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) - -# Register exception handlers -app.add_exception_handler(HTTPException, http_exception_handler) -app.add_exception_handler(RequestValidationError, validation_exception_handler) -app.add_exception_handler(Exception, general_exception_handler) - -# Environment -environment = os.getenv("ENVIRONMENT", "local") - -# Rate Limit Middleware -app.add_middleware(SlowAPIMiddleware) - -# Security Middlewares -# I am commenting out the HTTPS redirect middleware for now -# because this project is hosted behind a reverse proxy -# if environment in ["production", "staging"]: - # app.add_middleware(HTTPSRedirectMiddleware) - -app.add_middleware( - TrustedHostMiddleware, - allowed_hosts=["localhost", "127.0.0.1", "*.taimako.dubem.xyz", "taimako.dubem.xyz", "*.staging.taimako.ai", "api.taimako.dubem.xyz"] -) - -app.add_middleware(SecurityHeadersMiddleware) - -# Configure CORS -# origins = [ -# "http://localhost:3000", -# "http://127.0.0.1:5500", -# "http://localhost:8000", -# ] - -origins = [ - "http://localhost:3000", - "http://localhost:8000", - "https://taimako.dubem.xyz", - "https://taimako.dubem.xyz/", -] - -app.add_middleware( - CORSMiddleware, - allow_origin_regex=".*", - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -app.include_router(api_router) -app.include_router(auth_router) - -# Import and include business router -from app.api.business import router as business_router -app.include_router(business_router) - -from app.api.widget import router as widget_router -app.include_router(widget_router, prefix="/widgets", tags=["widgets"]) - -from app.api.analytics import router as analytics_router -app.include_router(analytics_router, prefix="/analytics", tags=["analytics"]) - -from app.api.escalation import router as escalation_router -app.include_router(escalation_router, prefix="/escalations", tags=["escalations"]) - -from app.core.response_wrapper import success_response - -@app.get("/") -async def root(): - return success_response(message="Agentic RAG API is running") - -@app.get("/health") -async def health_check(): - return {"status": "healthy"} diff --git a/backend/app/main.py b/backend/app/main.py index 8ed7475..daeb164 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,17 +1,19 @@ -import os from fastapi import FastAPI from fastapi.exceptions import HTTPException, RequestValidationError from app.api.routes import router as api_router from app.auth.router import router as auth_router -from app.db.base import Base -from app.db.session import engine +from app.api.business import router as business_router +from app.api.widget import router as widget_router +from app.api.analytics import router as analytics_router +from app.api.escalation import router as escalation_router from app.core.exception_handler import ( http_exception_handler, validation_exception_handler, general_exception_handler ) from app.core.middleware import register_middleware +from app.core.response_wrapper import success_response # Create tables (if not using alembic, but we are. Keeping for dev convenience or removing if strictly alembic) # Base.metadata.create_all(bind=engine) @@ -34,22 +36,11 @@ app.include_router(api_router) app.include_router(auth_router) - -# Import and include business router -from app.api.business import router as business_router app.include_router(business_router) - -from app.api.widget import router as widget_router app.include_router(widget_router, prefix="/widgets", tags=["widgets"]) - -from app.api.analytics import router as analytics_router app.include_router(analytics_router, prefix="/analytics", tags=["analytics"]) - -from app.api.escalation import router as escalation_router app.include_router(escalation_router, prefix="/escalations", tags=["escalations"]) -from app.core.response_wrapper import success_response - @app.get("/") async def root(): return success_response(message="Agentic RAG API is running") diff --git a/backend/app/models/chat_session.py b/backend/app/models/chat_session.py index ff5344f..cd3184f 100644 --- a/backend/app/models/chat_session.py +++ b/backend/app/models/chat_session.py @@ -1,5 +1,5 @@ import uuid -from sqlalchemy import Column, String, DateTime, ForeignKey, Text, Boolean, Enum, Integer, Float +from sqlalchemy import Column, String, DateTime, ForeignKey, Text, Boolean, Integer, Float from sqlalchemy.orm import relationship from datetime import datetime, timezone from app.db.base import Base diff --git a/backend/app/models/document.py b/backend/app/models/document.py index 3f3f52a..394d465 100644 --- a/backend/app/models/document.py +++ b/backend/app/models/document.py @@ -1,6 +1,5 @@ import uuid -from sqlalchemy import Column, String, DateTime, ForeignKey, Enum -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy import Column, String, DateTime, ForeignKey from datetime import datetime, timezone from app.db.base import Base diff --git a/backend/app/models/escalation.py b/backend/app/models/escalation.py index 6116367..59e7e5e 100644 --- a/backend/app/models/escalation.py +++ b/backend/app/models/escalation.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, String, Text, DateTime, ForeignKey, Boolean, Enum +from sqlalchemy import Column, String, Text, DateTime, ForeignKey from sqlalchemy.orm import relationship from datetime import datetime, timezone import uuid diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 60e4c98..6101524 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -1,6 +1,5 @@ import uuid from sqlalchemy import Column, String, Boolean, DateTime -from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship from datetime import datetime, timezone from app.db.base import Base diff --git a/backend/app/models/widget.py b/backend/app/models/widget.py index 09faad7..7ff2202 100644 --- a/backend/app/models/widget.py +++ b/backend/app/models/widget.py @@ -3,8 +3,6 @@ from sqlalchemy.orm import relationship from datetime import datetime, timezone from app.db.base import Base -from app.models.user import User -from app.models.business import Business # Note: ChatSession is imported via string reference in relationships to avoid circular imports def generate_uuid(): diff --git a/backend/app/schemas/document.py b/backend/app/schemas/document.py index 97640b9..fd750c1 100644 --- a/backend/app/schemas/document.py +++ b/backend/app/schemas/document.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, Field -from typing import List, Optional +from typing import Optional from datetime import datetime class DocumentMetadata(BaseModel): diff --git a/backend/app/schemas/widget.py b/backend/app/schemas/widget.py index 6750ac2..6eeb910 100644 --- a/backend/app/schemas/widget.py +++ b/backend/app/schemas/widget.py @@ -1,7 +1,6 @@ from pydantic import BaseModel, EmailStr from typing import Optional, List from datetime import datetime -from uuid import UUID # Guest Start class GuestStartRequest(BaseModel): diff --git a/backend/app/services/agent_service copy 2.py b/backend/app/services/agent_service copy 2.py deleted file mode 100644 index ebc0d96..0000000 --- a/backend/app/services/agent_service copy 2.py +++ /dev/null @@ -1,131 +0,0 @@ -from distro.distro import name -import google.generativeai as genai -from app.core.config import settings -from app.services.rag_service import rag_service -from app.models.chat import ChatResponse - -import os -import asyncio -from google.adk.agents import Agent -from google.adk.models.lite_llm import LiteLlm # For multi-model support -from google.adk.sessions import InMemorySessionService -from google.adk.runners import Runner -from google.genai import types # For creating message Content/Parts - - -import warnings -# Ignore all warnings -warnings.filterwarnings("ignore") - -import logging -logging.basicConfig(level=logging.ERROR) - -print("Libraries imported.") -print(f"Google API Key set: {'Yes' if os.environ.get('GOOGLE_API_KEY') and os.environ['GOOGLE_API_KEY'] != 'YOUR_GOOGLE_API_KEY' else 'No (REPLACE PLACEHOLDER!)'}") - -MODEL_GEMINI_2_0_FLASH = "gemini-2.0-flash" - -# --- Session Management --- -# Key Concept: SessionService stores conversation history & state. -# InMemorySessionService is simple, non-persistent storage for this tutorial. -session_service = InMemorySessionService() - -# Define constants for identifying the interaction context -APP_NAME = "customer_support_app" -USER_ID = "user_1" -SESSION_ID = "session_001" # Using a fixed ID for now - -async def init_session(app_name:str, user_id:str, session_id:str) -> InMemorySessionService: - session = await session_service.create_session( - app_name=app_name, - user_id=user_id, - session_id=session_id - ) - print(f"Session created: App='{app_name}', User='{user_id}', Session='{session_id}'") - return session - -# session = asyncio.run(init_session(APP_NAME,USER_ID,SESSION_ID)) - - -def get_context(user_input: str) -> str: - """Retrieves the context from the RAG service. - - Args: - user_input (str): The user's input message. - - Returns: - str: The retrieved context. - """ - print(f"--- Tool: get_context called for user_input: {user_input} ---") # Log tool execution - # Retrieve context - context_chunks = rag_service.query(user_input) - context_text = "\n\n".join(context_chunks) - return context_text - -rag_agent = Agent( - name="rag_agent", - model=MODEL_GEMINI_2_0_FLASH, - description="Provides context to the user based on the user's input.", - instruction="You are a helpful customer support assistant. " - "When the user asks a question, " - "use the 'get_context' tool to find context relevant to the user's question. " - "If the tool returns an error, inform the user politely. " - "If the tool is successful, present the context clearly and how it relates to the user's question.", - tools=[ - get_context - ] -) - -# --- Runner --- -# Key Concept: Runner orchestrates the agent execution loop. -runner = Runner( - agent=rag_agent, # The agent we want to run - app_name=APP_NAME, # Associates runs with our app - session_service=session_service # Uses our session manager -) -print(f"Runner created for agent '{runner.agent.name}'.") - -async def call_agent_async(query: str, runner, user_id, session_id): - """Sends a query to the agent and prints the final response.""" - print(f"\n>>> User Query: {query}") - - # Prepare the user's message in ADK format - content = types.Content(role='user', parts=[types.Part(text=query)]) - - final_response_text = "Agent did not produce a final response." # Default - - # Key Concept: run_async executes the agent logic and yields Events. - # We iterate through events to find the final answer. - async for event in runner.run_async(user_id=user_id, session_id=session_id, new_message=content): - # You can uncomment the line below to see *all* events during execution - print(f" [Event] Author: {event.author}, Type: {type(event).__name__}, Final: {event.is_final_response()}, Content: {event.content}") - - # Key Concept: is_final_response() marks the concluding message for the turn. - if event.is_final_response(): - if event.content and event.content.parts: - # Assuming text response in the first part - final_response_text = event.content.parts[0].text - elif event.actions and event.actions.escalate: # Handle potential errors/escalations - final_response_text = f"Agent escalated: {event.error_message or 'No specific message.'}" - # Add more checks here if needed (e.g., specific error codes) - break # Stop processing events once the final response is found - - print(f"<<< Agent Response: {final_response_text}") - return final_response_text - - -async def run_conversation(message): - return await call_agent_async( - message, - runner=runner, - user_id=USER_ID, - session_id=SESSION_ID - ) - -# import asyncio -# if __name__ == "__main__": -# try: -# asyncio.run(run_conversation("What are stephen skills?")) -# except Exception as e: -# print(f"An error occurred: {e}") - diff --git a/backend/app/services/agent_service.py b/backend/app/services/agent_service.py index efe192a..ac8198d 100644 --- a/backend/app/services/agent_service.py +++ b/backend/app/services/agent_service.py @@ -1,3 +1,15 @@ +import logging +import warnings +from typing import Optional + +from google.adk.runners import Runner +from google.genai import types + +from app.services.agent_system.service import session_service, init_session +from app.services.agent_system.agent_factory import AgentFactory + +warnings.filterwarnings("ignore") + try: from app.services.rag_service import rag_service except ImportError: @@ -12,20 +24,6 @@ def query(self, text, user_id): USER_ID = "test_user" SESSION_ID = "test_session" -import os -import asyncio -from google.adk.runners import Runner -from google.genai import types -import logging -from typing import Optional - -# Import from new modular structure -from app.services.agent_system.service import session_service, init_session -from app.services.agent_system.agent_factory import AgentFactory - -import warnings -warnings.filterwarnings("ignore") - logging.basicConfig(level=logging.ERROR) diff --git a/backend/app/services/agent_system/agents.py b/backend/app/services/agent_system/agents.py index f8439ad..57ef57f 100644 --- a/backend/app/services/agent_system/agents.py +++ b/backend/app/services/agent_system/agents.py @@ -1,5 +1,4 @@ from google.adk.agents import Agent -from google.adk.models.lite_llm import LiteLlm from app.services.agent_system.tools import ( get_context, say_hello, say_goodbye, analyze_sentiment, escalate_to_human diff --git a/backend/app/services/agent_system/callbacks.py b/backend/app/services/agent_system/callbacks.py index 5969530..290af60 100644 --- a/backend/app/services/agent_system/callbacks.py +++ b/backend/app/services/agent_system/callbacks.py @@ -46,7 +46,7 @@ def block_unsafe_content( # Check for legacy BLOCK keyword if "BLOCK" in last_user_message_text.upper(): - print(f"--- Callback: Found 'BLOCK'. Blocking LLM call! ---") + print("--- Callback: Found 'BLOCK'. Blocking LLM call! ---") return LlmResponse( content=types.Content( role="model", @@ -130,7 +130,7 @@ def sanitize_model_response( # Only return modified response if changes were made if sanitized_text != original_text: - print(f"--- Callback: sanitize_model_response modified response ---") + print("--- Callback: sanitize_model_response modified response ---") return LlmResponse( content=types.Content( role="model", diff --git a/backend/app/services/agent_system/tools.py b/backend/app/services/agent_system/tools.py index c4edf51..7a1999b 100644 --- a/backend/app/services/agent_system/tools.py +++ b/backend/app/services/agent_system/tools.py @@ -230,7 +230,6 @@ def escalate_to_human(reason: str, user_message: str, tool_context: ToolContext) print(f"Escalation: Widget found. User ID: {widget.user_id}") # Get business via user_id - from app.models.user import User business = db.query(Business).filter(Business.user_id == widget.user_id).first() if not business: diff --git a/backend/app/services/analysis_agent.py b/backend/app/services/analysis_agent.py index 7163b64..a7fe4be 100644 --- a/backend/app/services/analysis_agent.py +++ b/backend/app/services/analysis_agent.py @@ -1,9 +1,8 @@ -import os import json from datetime import datetime, timezone from typing import List, Optional, Tuple from sqlalchemy.orm import Session -from app.models.widget import GuestMessage, GuestUser +from app.models.widget import GuestMessage from app.models.chat_session import ChatSession # Use specific client for multi-tenant API key support @@ -72,7 +71,7 @@ async def analyze_session(db: Session, session_id: str, intents: Optional[List[s """ try: - print(f"Analysis Agent: Calling Gemini 2.0 Flash for analysis...") + print("Analysis Agent: Calling Gemini 2.0 Flash for analysis...") client = genai.Client(api_key=api_key) response = client.models.generate_content( model="gemini-2.0-flash", diff --git a/backend/app/services/rag_service.py b/backend/app/services/rag_service.py index 9e98bbc..6aba291 100644 --- a/backend/app/services/rag_service.py +++ b/backend/app/services/rag_service.py @@ -1,10 +1,8 @@ import uuid import os -import shutil from typing import List, Optional from fastapi import UploadFile from pypdf import PdfReader -import io import google.generativeai as genai from app.services.vector_db import vector_db from app.utils.text_splitter import recursive_character_text_splitter diff --git a/backend/app/utils/text_splitter.py b/backend/app/utils/text_splitter.py index 39a53c5..d14ce16 100644 --- a/backend/app/utils/text_splitter.py +++ b/backend/app/utils/text_splitter.py @@ -1,4 +1,3 @@ -import re def recursive_character_text_splitter(text: str, chunk_size: int = 1000, chunk_overlap: int = 200) -> list[str]: if not text: diff --git a/backend/reset_password.py b/backend/reset_password.py index f6a78c6..1ac5ec2 100644 --- a/backend/reset_password.py +++ b/backend/reset_password.py @@ -1,14 +1,12 @@ #!/usr/bin/env python3 """Quick script to reset a user's password.""" import sys -import os # Add the app directory to the path sys.path.insert(0, '/app') from app.db.session import SessionLocal from app.models.user import User -from app.models.business import Business # Import to resolve relationship from app.core.security import get_password_hash def reset_password(email: str, new_password: str): diff --git a/backend/test_agent_refactor.py b/backend/test_agent_refactor.py index 6aaf758..0dd6fb7 100644 --- a/backend/test_agent_refactor.py +++ b/backend/test_agent_refactor.py @@ -1,5 +1,4 @@ import asyncio -import os from app.services.agent_service import run_conversation, session_service, APP_NAME, USER_ID, SESSION_ID async def verify_refactor(): diff --git a/backend/test_analysis_integration.py b/backend/test_analysis_integration.py index 43c068a..9664dad 100644 --- a/backend/test_analysis_integration.py +++ b/backend/test_analysis_integration.py @@ -6,9 +6,6 @@ from app.services.agent_service import run_conversation from app.db.session import SessionLocal from app.models.chat_session import ChatSession -from app.models.widget import GuestUser, WidgetSettings -from app.models.user import User -from app.models.business import Business import uuid import os @@ -57,7 +54,7 @@ async def test_automated_analysis(): session = db.query(ChatSession).filter(ChatSession.id == session_id).first() if session: - print(f"✓ Session found in database") + print("✓ Session found in database") if session.summary: print(f"✓ Summary: {session.summary}") else: diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 1da4fc9..b7186fa 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -15,8 +15,7 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool -from app.models.user import User # Import to register models -from app.models.business import Business # Import to register models +from fastapi.testclient import TestClient from app.main import app from app.services.rag_service import rag_service diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index b40b3a8..84b1f18 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -1,4 +1,3 @@ -from unittest.mock import MagicMock def test_root(client): response = client.get("/") diff --git a/backend/tests/test_api_scoped.py b/backend/tests/test_api_scoped.py index 376c179..12f59c4 100644 --- a/backend/tests/test_api_scoped.py +++ b/backend/tests/test_api_scoped.py @@ -1,5 +1,4 @@ -import pytest -from unittest.mock import patch, MagicMock +from unittest.mock import patch from app.core.security import create_access_token from app.models.user import User diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index 9b7ed36..214fe40 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -1,6 +1,4 @@ -import pytest -from fastapi.testclient import TestClient -from unittest.mock import patch, MagicMock +from unittest.mock import patch from app.main import app from app.core.config import settings from app.core.security import create_access_token @@ -40,7 +38,6 @@ def test_google_callback(mock_get_user_info, mock_exchange_code, client): assert "access_token" in response.headers["location"] assert "refresh_token" in response.headers["location"] -from app.db.session import SessionLocal from app.models.user import User def test_get_me_unauthorized(client): diff --git a/backend/tests/test_auth_local.py b/backend/tests/test_auth_local.py index 104ab67..01f2000 100644 --- a/backend/tests/test_auth_local.py +++ b/backend/tests/test_auth_local.py @@ -1,6 +1,3 @@ -from fastapi.testclient import TestClient -from app.main import app -from app.api.routes import router def test_signup_success(client): response = client.post("/auth/signup", json={ diff --git a/backend/tests/test_automated_analysis.py b/backend/tests/test_automated_analysis.py index 878fc43..50b7a41 100644 --- a/backend/tests/test_automated_analysis.py +++ b/backend/tests/test_automated_analysis.py @@ -1,6 +1,6 @@ import pytest import asyncio -from unittest.mock import Mock, patch, AsyncMock +from unittest.mock import Mock, patch from app.services.agent_system.callbacks import trigger_session_analysis, _run_analysis_in_background from google.adk.agents.callback_context import CallbackContext from google.adk.models.llm_response import LlmResponse @@ -152,7 +152,6 @@ async def test_analysis_error_graceful(self): async def test_background_analysis_timeout(self): """Test that background analysis has timeout protection.""" - from app.db.session import SessionLocal # Mock analyze_session to take too long async def slow_analysis(*args, **kwargs): diff --git a/backend/tests/test_business.py b/backend/tests/test_business.py index c9f2d94..795b800 100644 --- a/backend/tests/test_business.py +++ b/backend/tests/test_business.py @@ -1,5 +1,3 @@ -import pytest -from fastapi.testclient import TestClient from app.models.business import Business from app.models.user import User from app.core.security import create_access_token diff --git a/backend/tests/test_escalation_unit.py b/backend/tests/test_escalation_unit.py index 613f408..5081e27 100644 --- a/backend/tests/test_escalation_unit.py +++ b/backend/tests/test_escalation_unit.py @@ -1,14 +1,10 @@ import pytest from unittest.mock import MagicMock, patch -from sqlalchemy.orm import Session from fastapi.testclient import TestClient from app.main import app -from app.db.base import Base -from app.db.session import get_db from app.models.business import Business from app.models.chat_session import ChatSession -from app.models.widget import GuestUser from app.models.escalation import Escalation, EscalationStatus from app.services.agent_system.tools import escalate_to_human from google.adk.tools.tool_context import ToolContext diff --git a/backend/tests/test_rag_scoped.py b/backend/tests/test_rag_scoped.py index 585bb34..a668062 100644 --- a/backend/tests/test_rag_scoped.py +++ b/backend/tests/test_rag_scoped.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock, patch from app.services.rag_service import rag_service from app.models.document import Document -from app.models.user import User @pytest.fixture def mock_file_storage(monkeypatch): diff --git a/backend/tests/tools/test_analyze_sentiment.py b/backend/tests/tools/test_analyze_sentiment.py index 030e9a4..0002687 100644 --- a/backend/tests/tools/test_analyze_sentiment.py +++ b/backend/tests/tools/test_analyze_sentiment.py @@ -46,7 +46,6 @@ def test_negative_sentiment_detected(self, mock_tool_context_with_key): mock_genai.Client.return_value = mock_client mock_client.models.generate_content.side_effect = Exception("API Error") - from importlib import reload import app.services.agent_system.tools as tools_module result = tools_module.analyze_sentiment( diff --git a/backend/tests/tools/test_say_goodbye.py b/backend/tests/tools/test_say_goodbye.py index dff50a4..279bc64 100644 --- a/backend/tests/tools/test_say_goodbye.py +++ b/backend/tests/tools/test_say_goodbye.py @@ -2,7 +2,6 @@ Tests for the say_goodbye tool. Validates farewell functionality and output format. """ -import pytest from app.services.agent_system.tools import say_goodbye diff --git a/backend/tests/tools/test_say_hello.py b/backend/tests/tools/test_say_hello.py index 4158392..29f5605 100644 --- a/backend/tests/tools/test_say_hello.py +++ b/backend/tests/tools/test_say_hello.py @@ -2,7 +2,6 @@ Tests for the say_hello tool. Validates greeting functionality with various input scenarios. """ -import pytest from app.services.agent_system.tools import say_hello From bbee8388d064f07dcf48fbb8357d73d42d142657 Mon Sep 17 00:00:00 2001 From: Stephen Nwankwo Date: Sat, 4 Jul 2026 20:33:10 +0100 Subject: [PATCH 8/9] clean dir --- backend/Pipfile | 11 - backend/__init__.py | 0 backend/add_column.py | 30 -- backend/api_docs.md | 253 ------------- backend/app/api/business.py | 4 +- backend/app/services/agent_system/tools.py | 5 +- backend/drop_chat_session.py | 57 --- backend/postman_collection.json | 350 ------------------ backend/reset_password.py | 34 -- backend/test.txt | 1 - backend/test_agent_refactor.py | 57 --- backend/test_analysis_integration.py | 82 ---- backend/test_output.txt | 52 --- backend/test_response_format.py | 65 ---- backend/test_upload.txt | 1 - backend/tests/conftest.py | 16 +- backend/tests/test_auth.py | 3 +- backend/tests/test_escalation_unit.py | 4 +- backend/tests/tools/test_escalate_to_human.py | 2 +- 19 files changed, 15 insertions(+), 1012 deletions(-) delete mode 100644 backend/Pipfile delete mode 100644 backend/__init__.py delete mode 100644 backend/add_column.py delete mode 100644 backend/api_docs.md delete mode 100644 backend/drop_chat_session.py delete mode 100644 backend/postman_collection.json delete mode 100644 backend/reset_password.py delete mode 100644 backend/test.txt delete mode 100644 backend/test_agent_refactor.py delete mode 100644 backend/test_analysis_integration.py delete mode 100644 backend/test_output.txt delete mode 100644 backend/test_response_format.py delete mode 100644 backend/test_upload.txt diff --git a/backend/Pipfile b/backend/Pipfile deleted file mode 100644 index d61ea53..0000000 --- a/backend/Pipfile +++ /dev/null @@ -1,11 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] - -[dev-packages] - -[requires] -python_version = "3.13" diff --git a/backend/__init__.py b/backend/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/add_column.py b/backend/add_column.py deleted file mode 100644 index 6de8b78..0000000 --- a/backend/add_column.py +++ /dev/null @@ -1,30 +0,0 @@ -import sqlite3 -import os - -db_path = 'sql_app.db' -print(f"Connecting to {db_path}...") - -if not os.path.exists(db_path): - print(f"Error: {db_path} not found!") - exit(1) - -try: - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - # Check if column exists first to avoid error if run multiple times - cursor.execute("PRAGMA table_info(chat_sessions)") - columns = [info[1] for info in cursor.fetchall()] - - if 'sentiment_score' in columns: - print("Column 'sentiment_score' already exists.") - else: - print("Adding column 'sentiment_score'...") - cursor.execute("ALTER TABLE chat_sessions ADD COLUMN sentiment_score FLOAT") - conn.commit() - print("Column added successfully.") - -except Exception as e: - print(f"Migration Error: {e}") -finally: - if conn: - conn.close() diff --git a/backend/api_docs.md b/backend/api_docs.md deleted file mode 100644 index f7ee152..0000000 --- a/backend/api_docs.md +++ /dev/null @@ -1,253 +0,0 @@ -# API Documentation - -Base URL: `http://localhost:8000` (or `http://localhost:8100` if running via Docker default in Makefile) - -## Authentication -All endpoints except authentication routes require a valid JWT token in the Authorization header: -``` -Authorization: Bearer -``` - -## Endpoints - -### Authentication - -#### 1. Sign Up -- **URL**: `/auth/signup` -- **Method**: `POST` -- **Content-Type**: `application/json` - -**Request Body**: -```json -{ - "email": "user@example.com", - "password": "securepassword", - "name": "John Doe" -} -``` - -#### 2. Login -- **URL**: `/auth/login` -- **Method**: `POST` -- **Content-Type**: `application/json` - -**Request Body**: -```json -{ - "email": "user@example.com", - "password": "securepassword" -} -``` - -**Response**: -```json -{ - "status": "success", - "data": { - "access_token": "eyJ...", - "refresh_token": "eyJ...", - "token_type": "bearer" - } -} -``` - -### Business Management - -#### 3. Create Business Profile -Create a business profile with custom agent configuration. - -- **URL**: `/business` -- **Method**: `POST` -- **Content-Type**: `application/json` -- **Auth**: Required - -**Request Body**: -```json -{ - "business_name": "Acme Support", - "description": "Customer support for Acme products", - "website": "https://acme.com", - "custom_agent_instruction": "Always be professional and mention our 24/7 support availability." -} -``` - -**Response**: -```json -{ - "status": "success", - "message": "Business profile created successfully", - "data": { - "id": "uuid", - "user_id": "uuid", - "business_name": "Acme Support", - "description": "Customer support for Acme products", - "website": "https://acme.com", - "custom_agent_instruction": "Always be professional and mention our 24/7 support availability.", - "created_at": "2025-12-08T00:00:00", - "updated_at": "2025-12-08T00:00:00" - } -} -``` - -#### 4. Get Business Profile -Retrieve the current user's business profile. - -- **URL**: `/business` -- **Method**: `GET` -- **Auth**: Required - -**Response**: -```json -{ - "status": "success", - "data": { - "id": "uuid", - "user_id": "uuid", - "business_name": "Acme Support", - "description": "Customer support for Acme products", - "website": "https://acme.com", - "custom_agent_instruction": "Always be professional and mention our 24/7 support availability.", - "created_at": "2025-12-08T00:00:00", - "updated_at": "2025-12-08T00:00:00" - } -} -``` - -#### 5. Update Business Profile -Update business profile fields. - -- **URL**: `/business` -- **Method**: `PUT` -- **Content-Type**: `application/json` -- **Auth**: Required - -**Request Body** (all fields optional): -```json -{ - "business_name": "Updated Name", - "description": "New description", - "website": "https://newsite.com", - "custom_agent_instruction": "New instructions for the agent" -} -``` - -### Document Management - -#### 6. Upload Documents -Uploads one or more files to the staging area. - -- **URL**: `/documents/upload` -- **Method**: `POST` -- **Content-Type**: `multipart/form-data` -- **Auth**: Required - -**Request Body**: -- `files`: List of files to upload (Binary) - -**Response**: -```json -{ - "status": "success", - "message": "Files uploaded successfully", - "data": { - "files": [ - "document1.pdf", - "notes.txt" - ] - } -} -``` - -#### 7. List Documents -Get all documents for the current user. - -- **URL**: `/documents` -- **Method**: `GET` -- **Auth**: Required - -**Response**: -```json -{ - "status": "success", - "data": [ - "document1.pdf", - "notes.txt" - ] -} -``` - -#### 8. Process Documents -Triggers the processing of uploaded documents (text splitting, embedding, and indexing). - -- **URL**: `/rag/process` -- **Method**: `POST` -- **Auth**: Required - -**Response**: -```json -{ - "status": "success", - "data": [ - { - "filename": "document1.pdf", - "chunks_created": 15, - "status": "success" - }, - { - "filename": "notes.txt", - "chunks_created": 8, - "status": "success" - } - ] -} -``` - -### Chat - -#### 9. Chat with Agent -Interact with the AI agent which uses your business configuration and processed documents as context. - -> **Note**: You must create a business profile before using this endpoint. - -- **URL**: `/chat` -- **Method**: `POST` -- **Content-Type**: `application/json` -- **Auth**: Required - -**Request Body**: -```json -{ - "message": "What are your support hours?" -} -``` - -**Response**: -```json -{ - "status": "success", - "data": { - "response": "We offer 24/7 support availability for all our customers. You can reach us anytime...", - "sources": [] - } -} -``` - -## Key Features - -- **User-Scoped Data**: Each user's documents and business configuration are isolated -- **Dynamic Agent**: Agent is created with your business name and custom instructions -- **Context-Aware**: Agent only accesses your uploaded documents -- **Session Management**: Conversation history is maintained per user - -## Running the API - -You can access the interactive Swagger UI at `/docs` (e.g., `http://localhost:8000/docs`) to test endpoints directly in the browser. - -## Workflow - -1. **Sign up** or **Login** to get authentication tokens -2. **Create a business profile** with your business details and custom agent instructions -3. **Upload documents** related to your business -4. **Process documents** to make them searchable -5. **Chat with the agent** - it will use your business context and documents to provide relevant responses - diff --git a/backend/app/api/business.py b/backend/app/api/business.py index 518784b..0ba145e 100644 --- a/backend/app/api/business.py +++ b/backend/app/api/business.py @@ -149,8 +149,8 @@ async def validate_api_key( # New SDK supports `client.models.list()`? # Or we can just try generating "test". - response = client.models.generate_content( - model='gemini-2.0-flash', + client.models.generate_content( + model='gemini-2.0-flash', contents='Test' ) # If no exception, we are good? diff --git a/backend/app/services/agent_system/tools.py b/backend/app/services/agent_system/tools.py index 7a1999b..3c2a2d2 100644 --- a/backend/app/services/agent_system/tools.py +++ b/backend/app/services/agent_system/tools.py @@ -3,7 +3,7 @@ from app.services.agent_system.tool_schemas import ( GetContextInput, ContextOutput, SayHelloInput, GreetingOutput, - SayGoodbyeInput, FarewellOutput + FarewellOutput ) from app.db.session import SessionLocal from app.models.business import Business @@ -108,9 +108,6 @@ def say_goodbye() -> str: Returns: str: A polite farewell message. """ - # Validate input (empty schema for this tool) - validated_input = SayGoodbyeInput() - # Create structured output output = FarewellOutput(message="Goodbye! Have a great day.") return output.message diff --git a/backend/drop_chat_session.py b/backend/drop_chat_session.py deleted file mode 100644 index 7d059b8..0000000 --- a/backend/drop_chat_session.py +++ /dev/null @@ -1,57 +0,0 @@ -import sqlite3 - -def clean_db(): - conn = sqlite3.connect('sql_app.db') - cursor = conn.cursor() - - try: - print("Dropping chat_sessions table...") - cursor.execute("DROP TABLE IF EXISTS chat_sessions") - print("Dropped chat_sessions.") - except Exception as e: - print(f"Error dropping chat_sessions: {e}") - - # Dropping column in sqlite is tricky, usually requires recreating table. - # But since Alembic batch operations handle this, and the column might not exist if previous run failed halfway. - # Actually, if previous run failed on FK creation, the column might be there. - # But we can't easily drop column in pure sqlite without new table. - # However, if we just drop chat_sessions, the FK constraint from guest_messages might linger if it wasn't named? - # No, FK is on the guest_messages table. - # If I don't drop the column `session_id` from `guest_messages`, Alembic might fail saying column exists or similar. - # Alembic's `add_column` will fail if column exists. - - # Let's inspect if column exists. - cursor.execute("PRAGMA table_info(guest_messages)") - columns = [info[1] for info in cursor.fetchall()] - if 'session_id' in columns: - print("Column session_id exists in guest_messages.") - # We must remove it. - # SQLite way: create new table without col, copy, drop old, rename. - # Too risky to do manually? - # Maybe I should just manually modify the migration file to skip adding column if exists? - # NO, that's hacking. - # Let's try to remove it using sqlite approach in this script. - - print("Recreating guest_messages without session_id...") - # Get schema - cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='guest_messages'") - create_sql = cursor.fetchone()[0] - # create_sql has the session_id definition? maybe. - # Easier: Rename table to _old, create new table (how to get schema?), copy, drop _old. - # Actually, let's just use the fact that I can use python to do this cleanly if I had the Model. - # But I don't want to load app stack. - - # Alternative: Just use alembic downgrade? - # I can try `alembic downgrade e2cd2430b4a5` (the base revision). - # But downgrading requires the migration to have been marked as done? It failed, so it's not marked? - # Or maybe it IS marked but failed? - # No, if it exits with 1, it probably didn't update alembic_version. - pass - else: - print("Column session_id does not exist.") - - conn.commit() - conn.close() - -if __name__ == "__main__": - clean_db() diff --git a/backend/postman_collection.json b/backend/postman_collection.json deleted file mode 100644 index 5279620..0000000 --- a/backend/postman_collection.json +++ /dev/null @@ -1,350 +0,0 @@ -{ - "info": { - "_postman_id": "agentic-rag-api-collection", - "name": "Agentic RAG API", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "Auth", - "item": [ - { - "name": "Signup", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"user@example.com\",\n \"password\": \"securepassword\",\n \"name\": \"Test User\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/auth/signup", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "signup" - ] - } - }, - "response": [] - }, - { - "name": "Login", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var jsonData = pm.response.json();", - "if (jsonData.status === 'success') {", - " pm.environment.set(\"access_token\", jsonData.data.access_token);", - " pm.environment.set(\"refresh_token\", jsonData.data.refresh_token);", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"user@example.com\",\n \"password\": \"securepassword\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/auth/login", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "login" - ] - } - }, - "response": [] - }, - { - "name": "Google Login", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/auth/google/login", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "google", - "login" - ] - } - }, - "response": [] - }, - { - "name": "Refresh Token", - "request": { - "method": "POST", - "header": [], - "url": { - "raw": "{{base_url}}/auth/refresh?refresh_token={{refresh_token}}", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "refresh" - ], - "query": [ - { - "key": "refresh_token", - "value": "{{refresh_token}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Get Me", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{access_token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/auth/me", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "me" - ] - } - }, - "response": [] - }, - { - "name": "Logout", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{access_token}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "url": { - "raw": "{{base_url}}/auth/logout", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "logout" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Documents", - "item": [ - { - "name": "Upload Documents", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{access_token}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "files", - "type": "file", - "src": [] - } - ] - }, - "url": { - "raw": "{{base_url}}/documents/upload", - "host": [ - "{{base_url}}" - ], - "path": [ - "documents", - "upload" - ] - } - }, - "response": [] - }, - { - "name": "List Documents", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{access_token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/documents", - "host": [ - "{{base_url}}" - ], - "path": [ - "documents" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "RAG", - "item": [ - { - "name": "Process Documents", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{access_token}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "url": { - "raw": "{{base_url}}/rag/process", - "host": [ - "{{base_url}}" - ], - "path": [ - "rag", - "process" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Chat", - "item": [ - { - "name": "Chat", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{access_token}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"message\": \"What is in the document?\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{base_url}}/chat", - "host": [ - "{{base_url}}" - ], - "path": [ - "chat" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Root", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/", - "host": [ - "{{base_url}}" - ], - "path": [ - "" - ] - } - }, - "response": [] - } - ], - "variable": [ - { - "key": "base_url", - "value": "http://localhost:8000", - "type": "string" - } - ] -} \ No newline at end of file diff --git a/backend/reset_password.py b/backend/reset_password.py deleted file mode 100644 index 1ac5ec2..0000000 --- a/backend/reset_password.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -"""Quick script to reset a user's password.""" -import sys - -# Add the app directory to the path -sys.path.insert(0, '/app') - -from app.db.session import SessionLocal -from app.models.user import User -from app.core.security import get_password_hash - -def reset_password(email: str, new_password: str): - db = SessionLocal() - try: - user = db.query(User).filter(User.email == email).first() - if not user: - print(f"User with email {email} not found!") - return False - - user.hashed_password = get_password_hash(new_password) - db.commit() - print(f"Password reset successfully for {email}") - return True - finally: - db.close() - -if __name__ == "__main__": - if len(sys.argv) != 3: - print("Usage: python reset_password.py ") - sys.exit(1) - - email = sys.argv[1] - new_password = sys.argv[2] - reset_password(email, new_password) diff --git a/backend/test.txt b/backend/test.txt deleted file mode 100644 index d670460..0000000 --- a/backend/test.txt +++ /dev/null @@ -1 +0,0 @@ -test content diff --git a/backend/test_agent_refactor.py b/backend/test_agent_refactor.py deleted file mode 100644 index 0dd6fb7..0000000 --- a/backend/test_agent_refactor.py +++ /dev/null @@ -1,57 +0,0 @@ -import asyncio -from app.services.agent_service import run_conversation, session_service, APP_NAME, USER_ID, SESSION_ID - -async def verify_refactor(): - print("--- Starting Verification ---") - - # 1. Test Guardrail (Block) - Should work without API billing! - print("\n[Test 1] Guardrail: Block Unsafe Content") - try: - response_block = await run_conversation("Please BLOCK this request") - print(f"Response: {response_block}") - assert "unsafe content" in response_block or "cannot process" in response_block - print("✅ Guardrail Test Passed") - except Exception as e: - print(f"❌ Guardrail Test Failed: {e}") - - # 2. Test Delegation (Greeting) - Needs API (Commented out to isolate Guardrail) - # print("\n[Test 2] Delegation: Greeting") - # try: - # response_hello = await run_conversation("Hello") - # print(f"Response: {response_hello}") - # assert "Hello" in response_hello or "assist" in response_hello - # print("✅ Delegation Test Passed") - # except Exception as e: - # print(f"⚠️ Delegation Test Failed (likely billing): {e}") - - # 3. Test RAG (Root Agent) - Needs API (Commented out to isolate Guardrail) - # print("\n[Test 3] RAG: Context Retrieval") - # try: - # response_rag = await run_conversation("What are stephen skills?") - # print(f"Response: {response_rag}") - # assert isinstance(response_rag, str) - # print("✅ RAG Test Passed") - # except Exception as e: - # print(f"⚠️ RAG Test Failed (likely billing): {e}") - - # 4. Test Session State - Checks if output_key worked (depends on previous tests) - print("\n[Test 4] Session State: Check output_key") - try: - session = await session_service.get_session(app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID) - last_response = session.state.get("last_agent_response") - print(f"Last Agent Response in State: {last_response}") - # If Guardrail passed, last_response should be the block message - if "unsafe content" in str(last_response): - print("✅ Session State Test Passed (Captured Guardrail Response)") - else: - print(f"⚠️ Session State Test Inconclusive: {last_response}") - except Exception as e: - print(f"❌ Session State Test Failed: {e}") - -if __name__ == "__main__": - try: - asyncio.run(verify_refactor()) - except Exception as e: - print(f"\n❌ Verification Failed: {e}") - import traceback - traceback.print_exc() diff --git a/backend/test_analysis_integration.py b/backend/test_analysis_integration.py deleted file mode 100644 index 9664dad..0000000 --- a/backend/test_analysis_integration.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -Quick integration test to verify automated session analysis works end-to-end. -Run with: uv run python test_analysis_integration.py -""" -import asyncio -from app.services.agent_service import run_conversation -from app.db.session import SessionLocal -from app.models.chat_session import ChatSession -import uuid -import os - -async def test_automated_analysis(): - """Test that analysis automatically triggers after agent response.""" - - print("\n=== Testing Automated Session Analysis ===\n") - - # Use test API key from environment or a placeholder - test_api_key = os.getenv("GOOGLE_API_KEY", "test-key") - - # Create a test session ID - session_id = f"test-session-{uuid.uuid4()}" - user_id = f"test-user-{uuid.uuid4()}" - - print(f"Session ID: {session_id}") - print(f"User ID: {user_id}") - - # Run a conversation - print("\n1. Sending test message to agent...") - try: - response = await run_conversation( - message="Hello, I need help with my account", - user_id=user_id, - business_name="TestBusiness", - custom_instruction="You are a helpful assistant.", - session_id=session_id, - intents=["Support", "Sales", "General"], - api_key=test_api_key - ) - print(f"✓ Agent Response: {response[:100]}...") - except Exception as e: - print(f"✗ Error getting agent response: {e}") - return - - # Wait a moment for background analysis to complete - print("\n2. Waiting for background analysis to complete...") - await asyncio.sleep(3) - - # Check if session was analyzed - print("\n3. Checking if session was analyzed in database...") - db = SessionLocal() - try: - # Note: The session might not exist in DB if this is just an ADK session - # This test mainly verifies the callback fires without errors - session = db.query(ChatSession).filter(ChatSession.id == session_id).first() - - if session: - print("✓ Session found in database") - if session.summary: - print(f"✓ Summary: {session.summary}") - else: - print("⚠ Summary not yet generated (analysis might still be running)") - - if session.top_intent: - print(f"✓ Intent: {session.top_intent}") - else: - print("⚠ Intent not yet generated") - else: - print("⚠ Session not found in database (this is normal for test sessions)") - print(" The important thing is that the callback fired without errors") - - finally: - db.close() - - print("\n=== Test Complete ===\n") - print("Key Success Indicators:") - print("✓ Agent responded successfully") - print("✓ No errors from analysis callback") - print("✓ Background task was created") - print("\nThe automated analysis workflow is working!") - -if __name__ == "__main__": - asyncio.run(test_automated_analysis()) diff --git a/backend/test_output.txt b/backend/test_output.txt deleted file mode 100644 index 56fc9f1..0000000 --- a/backend/test_output.txt +++ /dev/null @@ -1,52 +0,0 @@ -============================= test session starts ============================== -platform darwin -- Python 3.13.2, pytest-9.0.1, pluggy-1.6.0 -rootdir: /Users/vencomacbook/Desktop/sten/TaimakoAI/backend -configfile: pyproject.toml -plugins: anyio-4.11.0, Faker-38.2.0 -collected 2 items - -tests/test_escalation_unit.py .. [100%] - -=============================== warnings summary =============================== -app/db/base.py:3 - /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/db/base.py:3: MovedIn20Warning: The ``declarative_base()`` function is now available as sqlalchemy.orm.declarative_base(). (deprecated since: 2.0) (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9) - Base = declarative_base() - -app/schemas/user.py:23 - /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/schemas/user.py:23: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ - class UserResponse(UserBase): - -.venv/lib/python3.13/site-packages/google/cloud/aiplatform/models.py:52 - /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/.venv/lib/python3.13/site-packages/google/cloud/aiplatform/models.py:52: FutureWarning: Support for google-cloud-storage < 3.0.0 will be removed in a future version of google-cloud-aiplatform. Please upgrade to google-cloud-storage >= 3.0.0. - from google.cloud.aiplatform.utils import gcs_utils - -.venv/lib/python3.13/site-packages/litellm/types/llms/anthropic.py:531 - /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/.venv/lib/python3.13/site-packages/litellm/types/llms/anthropic.py:531: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ - class AnthropicResponseContentBlockToolUse(BaseModel): - -.venv/lib/python3.13/site-packages/litellm/types/rag.py:181 - /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/.venv/lib/python3.13/site-packages/litellm/types/rag.py:181: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ - class RAGIngestRequest(BaseModel): - -app/services/agent_system/tool_schemas.py:7 - /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/services/agent_system/tool_schemas.py:7: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ - class GetContextInput(BaseModel): - -app/services/agent_system/tool_schemas.py:24 - /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/services/agent_system/tool_schemas.py:24: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ - class SayHelloInput(BaseModel): - -app/services/agent_system/tool_schemas.py:47 - /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/services/agent_system/tool_schemas.py:47: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ - class ContextOutput(BaseModel): - -app/services/agent_system/tool_schemas.py:67 - /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/services/agent_system/tool_schemas.py:67: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ - class GreetingOutput(BaseModel): - -app/services/agent_system/tool_schemas.py:82 - /Users/vencomacbook/Desktop/sten/TaimakoAI/backend/app/services/agent_system/tool_schemas.py:82: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.12/migration/ - class FarewellOutput(BaseModel): - --- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html -======================== 2 passed, 10 warnings in 0.07s ======================== diff --git a/backend/test_response_format.py b/backend/test_response_format.py deleted file mode 100644 index ecfb1f8..0000000 --- a/backend/test_response_format.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -Test script to verify standardized error response format -""" -import requests -import json - -BASE_URL = "http://localhost:8000" - -def test_unauthenticated_access(): - """Test accessing protected endpoint without authentication""" - print("\n=== Test 1: Unauthenticated Access ===") - response = requests.get(f"{BASE_URL}/documents") - print(f"Status Code: {response.status_code}") - print(f"Response: {json.dumps(response.json(), indent=2)}") - - # Verify response format - data = response.json() - assert "status" in data, "Response missing 'status' field" - assert data["status"] == "error", f"Expected status='error', got '{data['status']}'" - assert "message" in data, "Response missing 'message' field" - assert "error_code" in data, "Response missing 'error_code' field" - print("✓ Response format is correct!") - -def test_invalid_token(): - """Test accessing protected endpoint with invalid token""" - print("\n=== Test 2: Invalid Token ===") - headers = {"Authorization": "Bearer invalid_token_here"} - response = requests.get(f"{BASE_URL}/documents", headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {json.dumps(response.json(), indent=2)}") - - # Verify response format - data = response.json() - assert "status" in data, "Response missing 'status' field" - assert data["status"] == "error", f"Expected status='error', got '{data['status']}'" - assert "message" in data, "Response missing 'message' field" - assert "error_code" in data, "Response missing 'error_code' field" - print("✓ Response format is correct!") - -def test_successful_endpoint(): - """Test a successful endpoint (root)""" - print("\n=== Test 3: Successful Endpoint ===") - response = requests.get(f"{BASE_URL}/") - print(f"Status Code: {response.status_code}") - print(f"Response: {json.dumps(response.json(), indent=2)}") - - # Verify response format - data = response.json() - assert "status" in data, "Response missing 'status' field" - assert data["status"] == "success", f"Expected status='success', got '{data['status']}'" - assert "message" in data, "Response missing 'message' field" - print("✓ Response format is correct!") - -if __name__ == "__main__": - try: - test_unauthenticated_access() - test_invalid_token() - test_successful_endpoint() - print("\n✅ All tests passed!") - except AssertionError as e: - print(f"\n❌ Test failed: {e}") - except requests.exceptions.ConnectionError: - print("\n❌ Could not connect to server. Make sure it's running on http://localhost:8000") - except Exception as e: - print(f"\n❌ Unexpected error: {e}") diff --git a/backend/test_upload.txt b/backend/test_upload.txt deleted file mode 100644 index d670460..0000000 --- a/backend/test_upload.txt +++ /dev/null @@ -1 +0,0 @@ -test content diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index b7186fa..72db986 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -10,14 +10,14 @@ sys.modules['chromadb.config'] = MagicMock() sys.modules['google.generativeai'] = MagicMock() -from app.db.session import get_db -from app.db.base import Base -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from sqlalchemy.pool import StaticPool -from fastapi.testclient import TestClient -from app.main import app -from app.services.rag_service import rag_service +from app.db.session import get_db # noqa: E402 +from app.db.base import Base # noqa: E402 +from sqlalchemy import create_engine # noqa: E402 +from sqlalchemy.orm import sessionmaker # noqa: E402 +from sqlalchemy.pool import StaticPool # noqa: E402 +from fastapi.testclient import TestClient # noqa: E402 +from app.main import app # noqa: E402 +from app.services.rag_service import rag_service # noqa: E402 @pytest.fixture(scope="function") def db_session(): diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index 214fe40..7effc00 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -2,6 +2,7 @@ from app.main import app from app.core.config import settings from app.core.security import create_access_token +from app.models.user import User # noqa: F401 def test_login_google_redirect(client): # Debug: Print all routes @@ -38,8 +39,6 @@ def test_google_callback(mock_get_user_info, mock_exchange_code, client): assert "access_token" in response.headers["location"] assert "refresh_token" in response.headers["location"] -from app.models.user import User - def test_get_me_unauthorized(client): response = client.get("/auth/me") assert response.status_code == 403 # HTTPBearer raises 403 when token is missing diff --git a/backend/tests/test_escalation_unit.py b/backend/tests/test_escalation_unit.py index 5081e27..e2acc41 100644 --- a/backend/tests/test_escalation_unit.py +++ b/backend/tests/test_escalation_unit.py @@ -94,8 +94,8 @@ def refresh_side_effect(instance): def test_escalation_api(): # Only if we can spin up a real DB or mock everything. # Validating the router exists and endpoint signature is correct. - client = TestClient(app) - # The endpoint needs a real DB session. + TestClient(app) + # The endpoint needs a real DB session. # Since I cannot easily setup a full integration test environment in this turn without checking concurrency, # I will rely on the unit test above for logic. pass diff --git a/backend/tests/tools/test_escalate_to_human.py b/backend/tests/tools/test_escalate_to_human.py index 2188482..3733389 100644 --- a/backend/tests/tools/test_escalate_to_human.py +++ b/backend/tests/tools/test_escalate_to_human.py @@ -102,7 +102,7 @@ def refresh_side_effect(instance): instance.id = "esc-789" mock_db_session.refresh.side_effect = refresh_side_effect - result = escalate_to_human( + escalate_to_human( reason="User frustrated", user_message="I need help!", tool_context=mock_tool_context From 237f4bd5ce9117bb5e720f534c842e185f8c6281 Mon Sep 17 00:00:00 2001 From: Stephen Nwankwo Date: Sat, 4 Jul 2026 21:04:34 +0100 Subject: [PATCH 9/9] fix: correct staging URL expectations in frontend config test Test had placeholder api.staging.taimako.ai URLs; actual config defaults to the Render deployment URLs for staging. Co-Authored-By: Claude Sonnet 4.6 --- frontend/tests/unit/config.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/tests/unit/config.test.ts b/frontend/tests/unit/config.test.ts index c4b666b..eb2e995 100644 --- a/frontend/tests/unit/config.test.ts +++ b/frontend/tests/unit/config.test.ts @@ -35,8 +35,8 @@ describe('config', () => { process.env.NEXT_PUBLIC_ENVIRONMENT = 'staging'; const config = await import('@/config'); - expect(config.BACKEND_URL).toBe('https://api.staging.taimako.ai'); - expect(config.FRONTEND_URL).toBe('https://app.staging.taimako.ai'); + expect(config.BACKEND_URL).toBe('https://taimako.onrender.com'); + expect(config.FRONTEND_URL).toBe('https://taimakoai.onrender.com'); }); it('returns production URLs for production environment', async () => {