Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.12.8
3.12
2 changes: 1 addition & 1 deletion LOGFIRE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ from src.logging_config import setup_logfire

@asynccontextmanager
async def lifespan(app: FastAPI):
setup_logfire() # Initializes Logfire with FastAPI and Pydantic instrumentation
setup_logfire(app) # Initializes Logfire with FastAPI and Pydantic instrumentation
# ... rest of startup
```

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ This will guide you through:
4. Facebook Page configuration
5. Bot configuration persistence

When you choose **Test the bot** (or run `uv run python -m src.cli.setup_cli test`), conversations are persisted to Supabase (`test_sessions`, `test_messages`) so you can inspect them in the Supabase Dashboard while testing.

## Environment Variables

See `.env.example` for all required environment variables:
Expand Down
9 changes: 9 additions & 0 deletions RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,15 @@ supabase db pull
# Via Supabase dashboard: SELECT id, bot_config_id, LENGTH(content) as content_size FROM reference_documents;
```

### Test REPL conversation persistence

Test conversations from **Test the bot** (in-flow) or **`uv run python -m src.cli.setup_cli test`** are stored in Supabase.

- **Tables:** `test_sessions` (one per REPL run: `reference_doc_id`, `source_url`, `tone`) and `test_messages` (each user/bot exchange).
- **Session ID:** When a test REPL starts, the CLI prints `Session ID: <uuid> — view in Supabase: test_sessions / test_messages`. Use that UUID to filter.
- **View in Supabase:** Open Table Editor (or SQL) → `test_sessions` for config, `test_messages` for history. Filter `test_messages` by `test_session_id` = the echoed session ID to see the current run.
- If Supabase is unavailable during a test run, the CLI warns and the REPL continues without persisting.

### Agent Service Debugging

```bash
Expand Down
26 changes: 26 additions & 0 deletions migrations/004_test_conversations.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- Test conversation persistence for setup test REPL.
-- test_sessions: one per REPL invocation (config); test_messages: one per user/bot exchange.

CREATE TABLE test_sessions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
reference_doc_id uuid NOT NULL REFERENCES reference_documents(id) ON DELETE CASCADE,
source_url text NOT NULL,
tone text NOT NULL,
created_at timestamptz DEFAULT now()
);

CREATE TABLE test_messages (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
test_session_id uuid NOT NULL REFERENCES test_sessions(id) ON DELETE CASCADE,
user_message text NOT NULL,
response_text text NOT NULL,
confidence float,
requires_escalation boolean DEFAULT false,
escalation_reason text,
created_at timestamptz DEFAULT now()
);

CREATE INDEX idx_test_sessions_reference_doc_id ON test_sessions(reference_doc_id);
CREATE INDEX idx_test_sessions_created_at ON test_sessions(created_at);
CREATE INDEX idx_test_messages_test_session_id ON test_messages(test_session_id);
CREATE INDEX idx_test_messages_created_at ON test_messages(created_at);
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "facebook-messenger-scrape-bot"
version = "0.1.0"
description = "Production-ready FastAPI application for AI-powered Facebook Messenger bots using PydanticAI Gateway"
readme = "README.md"
requires-python = ">=3.12.8"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.104.0",
"uvicorn[standard]>=0.24.0",
Expand Down
2 changes: 1 addition & 1 deletion railway.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build]
builder = "NIXPACKS"
builder = "RAILPACK"

[deploy]
startCommand = "uvicorn src.main:app --host 0.0.0.0 --port $PORT"
Expand Down
34 changes: 17 additions & 17 deletions src/api/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@
async def verify_webhook(request: Request):
"""Facebook webhook verification endpoint."""
settings = get_settings()

mode = request.query_params.get("hub.mode")
token = request.query_params.get("hub.verify_token")
challenge = request.query_params.get("hub.challenge")

if mode == "subscribe" and token == settings.facebook_verify_token:
logger.info("Webhook verified successfully")
return PlainTextResponse(challenge)

logger.warning("Webhook verification failed")
return Response(status_code=403)

Expand All @@ -39,29 +39,29 @@ async def verify_webhook(request: Request):
async def handle_webhook(request: Request, background_tasks: BackgroundTasks):
"""Handle incoming Facebook Messenger webhook events."""
payload = await request.json()

if payload.get("object") != "page":
return {"status": "ignored"}

for entry in payload.get("entry", []):
page_id = entry.get("id")

for messaging_event in entry.get("messaging", []):
sender_id = messaging_event.get("sender", {}).get("id")
message = messaging_event.get("message", {})
message_text = message.get("text")

if not message_text:
continue

# Process message in background
background_tasks.add_task(
process_message,
page_id=page_id,
sender_id=sender_id,
message_text=message_text,
)

return {"status": "ok"}


Expand All @@ -73,33 +73,33 @@ async def process_message(page_id: str, sender_id: str, message_text: str):
if not bot_config:
logger.error(f"No bot configuration found for page_id: {page_id}")
return

# Get reference document
ref_doc = get_reference_document(bot_config.reference_doc_id)
if not ref_doc:
logger.error(f"No reference document found: {bot_config.reference_doc_id}")
return

# Build agent context
context = AgentContext(
bot_config_id=bot_config.id,
reference_doc=ref_doc["content"],
tone=bot_config.tone,
recent_messages=[], # TODO: Load from message_history
tenant_id=getattr(bot_config, 'tenant_id', None),
tenant_id=getattr(bot_config, "tenant_id", None),
)

# Get response from agent (NEW: Using PydanticAI Gateway)
agent_service = MessengerAgentService()
response = await agent_service.respond(context, message_text)

# Send response via Facebook
await send_message(
page_access_token=bot_config.facebook_page_access_token,
recipient_id=sender_id,
text=response.message,
)

# Save to history
save_message_history(
bot_id=bot_config.id,
Expand All @@ -109,11 +109,11 @@ async def process_message(page_id: str, sender_id: str, message_text: str):
confidence=response.confidence,
requires_escalation=response.requires_escalation,
)

logger.info(
f"Processed message for page {page_id}: "
f"confidence={response.confidence}, escalation={response.requires_escalation}"
)

except Exception as e:
logger.error(f"Error processing message: {e}")
65 changes: 57 additions & 8 deletions src/cli/setup_cli.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""Typer-based interactive setup CLI."""

import os

os.environ.setdefault("LOGFIRE_IGNORE_NO_CONFIG", "1")

from pathlib import Path

_project_root = Path(__file__).resolve().parent.parent.parent
from dotenv import load_dotenv

load_dotenv(_project_root / ".env")
load_dotenv(_project_root / ".env.local")

Expand All @@ -18,7 +21,9 @@
from src.db.repository import (
create_bot_configuration,
create_reference_document,
create_test_session,
get_reference_document_by_source_url,
save_test_message,
)
from src.models.agent_models import AgentContext
from src.services.agent_service import MessengerAgentService
Expand Down Expand Up @@ -72,7 +77,9 @@ def _run_async_with_cleanup(coro):
# Cancel all pending tasks that aren't done
try:
if not loop.is_closed():
pending = [task for task in asyncio.all_tasks(loop) if not task.done()]
pending = [
task for task in asyncio.all_tasks(loop) if not task.done()
]
for task in pending:
task.cancel()
# Wait for cancellation to complete, ignoring exceptions
Expand Down Expand Up @@ -120,10 +127,16 @@ def _select_tone(message: str = "Select a tone") -> str:
return choice


def _run_test_repl(ref_doc_content: str, tone: str) -> None:
def _run_test_repl(
ref_doc_content: str,
tone: str,
reference_doc_id: str,
source_url: str,
) -> None:
"""
Run the test REPL: user types messages, agent responds. No Facebook required.
Uses placeholder bot_config_id; agent only needs reference_doc and tone.
Persists exchanges to Supabase (test_sessions / test_messages) when available.
"""
context = AgentContext(
bot_config_id="cli-test",
Expand All @@ -132,7 +145,23 @@ def _run_test_repl(ref_doc_content: str, tone: str) -> None:
recent_messages=[],
)
agent = MessengerAgentService()
typer.echo("\nTest the bot (type 'quit' or press Enter with empty message to exit).\n")
typer.echo(
"\nTest the bot (type 'quit' or press Enter with empty message to exit).\n"
)

session_id: str | None = None
try:
session_id = create_test_session(reference_doc_id, source_url, tone)
typer.echo(
f"Session ID: {session_id} — view in Supabase: test_sessions / test_messages\n"
)
except Exception:
session_id = None
typer.echo(
"Could not create test session (Supabase unavailable). Conversation will not be persisted.\n",
err=True,
)

recent_messages: list[str] = []

while True:
Expand All @@ -146,9 +175,23 @@ def _run_test_repl(ref_doc_content: str, tone: str) -> None:
response = _run_async_with_cleanup(agent.respond(context, user_message))
typer.echo(f"Bot: {response.message}")
if response.requires_escalation and response.escalation_reason:
typer.echo(typer.style(f" [escalation: {response.escalation_reason}]", fg=typer.colors.YELLOW))
typer.echo(
typer.style(
f" [escalation: {response.escalation_reason}]",
fg=typer.colors.YELLOW,
)
)
recent_messages.append(f"User: {user_message}")
recent_messages.append(f"Bot: {response.message}")
if session_id is not None:
save_test_message(
test_session_id=session_id,
user_message=user_message,
response_text=response.message,
confidence=response.confidence,
requires_escalation=response.requires_escalation,
escalation_reason=response.escalation_reason,
)
except Exception as e:
typer.echo(f"Error: {e}", err=True)
typer.echo("Exiting test.\n")
Expand Down Expand Up @@ -239,7 +282,7 @@ def setup():
return
if action == ACTION_TEST_BOT:
tone = _select_tone("Select a tone for testing")
_run_test_repl(ref_doc_content, tone)
_run_test_repl(ref_doc_content, tone, reference_doc_id, normalized_url)
continue
# ACTION_CONTINUE
break
Expand Down Expand Up @@ -286,15 +329,21 @@ def test():
Test the bot using an existing reference document (no Facebook credentials).
Paste the website URL; if a reference doc exists for that URL, you can chat with the bot.
"""
website_url = typer.prompt("Website URL (for which you already have a reference document)")
website_url = typer.prompt(
"Website URL (for which you already have a reference document)"
)
normalized_url = _normalize_website_url(website_url)
existing_doc = get_reference_document_by_source_url(normalized_url)
if not existing_doc:
typer.echo("No reference document found for this URL. Run setup first to scrape and generate one.", err=True)
typer.echo(
"No reference document found for this URL. Run setup first to scrape and generate one.",
err=True,
)
raise typer.Exit(1)
ref_doc_content = existing_doc["content"]
reference_doc_id = existing_doc["id"]
tone = _select_tone("Select a tone for testing")
_run_test_repl(ref_doc_content, tone)
_run_test_repl(ref_doc_content, tone, reference_doc_id, normalized_url)


if __name__ == "__main__":
Expand Down
Loading
Loading