WA template
This commit is contained in:
parent
7a2e21ba61
commit
d272ae0bd3
12 changed files with 1359 additions and 5 deletions
277
backend/app/agent_message_service.py
Normal file
277
backend/app/agent_message_service.py
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
import re
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .models import (
|
||||
AgentMessageDispatchLog,
|
||||
AgentMessageTemplate,
|
||||
AgentTemplateSubscription,
|
||||
AgentTemplateTestLog,
|
||||
UserAccount,
|
||||
)
|
||||
from .whatsapp_config import resolved_whatsapp_phone_number_id
|
||||
from .whatsapp_graph_client import WhatsAppGraphClient, WhatsAppGraphError
|
||||
from .whatsapp_messages import extract_wa_message_id, normalize_wa_number
|
||||
|
||||
class SendAgentMessagePayload(BaseModel):
|
||||
template: str = Field(min_length=2, max_length=64)
|
||||
to: str = Field(min_length=10, max_length=20)
|
||||
customer_name: str = Field(min_length=1, max_length=120)
|
||||
report_id: str | None = Field(default=None, max_length=120)
|
||||
|
||||
|
||||
def serialize_agent_template(template: AgentMessageTemplate, *, subscribed: bool = False) -> dict:
|
||||
return {
|
||||
"id": str(template.id),
|
||||
"slug": template.slug,
|
||||
"name": template.name,
|
||||
"description": template.description,
|
||||
"header_text": template.header_text,
|
||||
"body_preview": template.body_preview,
|
||||
"footer_text": template.footer_text,
|
||||
"meta_template_name": template.meta_template_name,
|
||||
"language": template.language,
|
||||
"category": template.category,
|
||||
"parameters": template.parameters,
|
||||
"button_label": template.button_label,
|
||||
"button_url_base": template.button_url_base,
|
||||
"is_active": template.is_active,
|
||||
"subscribed": subscribed,
|
||||
}
|
||||
|
||||
|
||||
def get_template_by_slug(db: Session, slug: str) -> AgentMessageTemplate:
|
||||
template = db.scalar(
|
||||
select(AgentMessageTemplate).where(
|
||||
AgentMessageTemplate.slug == slug,
|
||||
AgentMessageTemplate.is_active.is_(True),
|
||||
)
|
||||
)
|
||||
if not template:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Agent template not found")
|
||||
return template
|
||||
|
||||
|
||||
def require_subscription(db: Session, user: UserAccount, template: AgentMessageTemplate) -> AgentTemplateSubscription:
|
||||
subscription = db.scalar(
|
||||
select(AgentTemplateSubscription).where(
|
||||
AgentTemplateSubscription.user_id == user.id,
|
||||
AgentTemplateSubscription.template_id == template.id,
|
||||
)
|
||||
)
|
||||
if not subscription:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Subscribe to this agent template before sending messages",
|
||||
)
|
||||
return subscription
|
||||
|
||||
|
||||
def _validate_report_id(report_id: str) -> str:
|
||||
cleaned = report_id.strip()
|
||||
if not cleaned or len(cleaned) > 120:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid report_id")
|
||||
if not re.fullmatch(r"[A-Za-z0-9._-]+", cleaned):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="report_id may only contain letters, numbers, dots, underscores, and hyphens",
|
||||
)
|
||||
return cleaned
|
||||
|
||||
|
||||
def _build_template_components(template: AgentMessageTemplate, customer_name: str, report_id: str) -> list[dict]:
|
||||
components: list[dict] = [
|
||||
{
|
||||
"type": "body",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "text",
|
||||
"parameter_name": "customer_name",
|
||||
"text": customer_name.strip(),
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
if template.button_label and template.button_url_base:
|
||||
components.append(
|
||||
{
|
||||
"type": "button",
|
||||
"sub_type": "url",
|
||||
"index": "0",
|
||||
"parameters": [{"type": "text", "text": report_id}],
|
||||
}
|
||||
)
|
||||
return components
|
||||
|
||||
|
||||
def _whatsapp_client(db: Session) -> WhatsAppGraphClient:
|
||||
return WhatsAppGraphClient(phone_number_id=resolved_whatsapp_phone_number_id(db))
|
||||
|
||||
|
||||
async def send_agent_template_message(
|
||||
db: Session,
|
||||
user: UserAccount,
|
||||
payload: SendAgentMessagePayload,
|
||||
*,
|
||||
source: str = "api",
|
||||
) -> dict:
|
||||
template = get_template_by_slug(db, payload.template)
|
||||
require_subscription(db, user, template)
|
||||
|
||||
try:
|
||||
recipient = normalize_wa_number(payload.to)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
||||
|
||||
report_id = _validate_report_id(payload.report_id or f"run-{secrets.token_hex(3)}")
|
||||
components = _build_template_components(template, payload.customer_name, report_id)
|
||||
|
||||
client = _whatsapp_client(db)
|
||||
try:
|
||||
result = await client.send_template(
|
||||
recipient,
|
||||
template.meta_template_name,
|
||||
language_code=template.language,
|
||||
components=components,
|
||||
)
|
||||
except WhatsAppGraphError as exc:
|
||||
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
|
||||
|
||||
wa_message_id = extract_wa_message_id(result)
|
||||
log = AgentMessageDispatchLog(
|
||||
user_id=user.id,
|
||||
template_id=template.id,
|
||||
recipient=recipient,
|
||||
parameters={
|
||||
"customer_name": payload.customer_name.strip(),
|
||||
"report_id": report_id,
|
||||
},
|
||||
wa_message_id=wa_message_id,
|
||||
status="sent",
|
||||
source=source,
|
||||
)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
db.refresh(log)
|
||||
|
||||
report_url = f"{template.button_url_base.rstrip('/')}/{report_id}" if template.button_url_base else None
|
||||
return {
|
||||
"success": True,
|
||||
"template": template.slug,
|
||||
"to": recipient,
|
||||
"customer_name": payload.customer_name.strip(),
|
||||
"report_id": report_id,
|
||||
"report_url": report_url,
|
||||
"wa_message_id": wa_message_id,
|
||||
"dispatch_id": str(log.id),
|
||||
}
|
||||
|
||||
|
||||
def can_run_console_test(db: Session, user: UserAccount, template: AgentMessageTemplate) -> dict:
|
||||
since = datetime.now(timezone.utc) - timedelta(days=1)
|
||||
recent = db.scalar(
|
||||
select(AgentTemplateTestLog)
|
||||
.where(
|
||||
AgentTemplateTestLog.user_id == user.id,
|
||||
AgentTemplateTestLog.template_id == template.id,
|
||||
AgentTemplateTestLog.tested_at >= since,
|
||||
AgentTemplateTestLog.success.is_(True),
|
||||
)
|
||||
.order_by(AgentTemplateTestLog.tested_at.desc())
|
||||
)
|
||||
if recent:
|
||||
retry_after = recent.tested_at + timedelta(days=1)
|
||||
return {
|
||||
"allowed": False,
|
||||
"retry_after": retry_after,
|
||||
"last_tested_at": recent.tested_at,
|
||||
}
|
||||
return {"allowed": True, "retry_after": None, "last_tested_at": None}
|
||||
|
||||
|
||||
async def run_console_template_test(
|
||||
db: Session,
|
||||
user: UserAccount,
|
||||
template: AgentMessageTemplate,
|
||||
*,
|
||||
to: str,
|
||||
customer_name: str,
|
||||
report_id: str | None = None,
|
||||
) -> dict:
|
||||
require_subscription(db, user, template)
|
||||
test_status = can_run_console_test(db, user, template)
|
||||
if not test_status["allowed"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
||||
detail="Console test limit reached. You can test this template once per day.",
|
||||
)
|
||||
|
||||
payload = SendAgentMessagePayload(
|
||||
template=template.slug,
|
||||
to=to,
|
||||
customer_name=customer_name,
|
||||
report_id=report_id,
|
||||
)
|
||||
result = await send_agent_template_message(db, user, payload, source="console_test")
|
||||
db.add(
|
||||
AgentTemplateTestLog(
|
||||
user_id=user.id,
|
||||
template_id=template.id,
|
||||
recipient=result["to"],
|
||||
success=True,
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
return result
|
||||
|
||||
|
||||
def build_agent_message_docs(template: AgentMessageTemplate) -> dict:
|
||||
return {
|
||||
"template": serialize_agent_template(template),
|
||||
"api": {
|
||||
"method": "POST",
|
||||
"path": "/api/v1/agent/messages/send",
|
||||
"summary": (
|
||||
"Send an approved agent WhatsApp template via Urmi. Urmi authenticates your API key "
|
||||
"and relays the message through the Urmi AI WhatsApp Business account."
|
||||
),
|
||||
"authentication": {
|
||||
"type": "api_key",
|
||||
"header": "Authorization",
|
||||
"format": "Bearer urmi_live_<key>",
|
||||
"alternate_header": "X-Api-Key",
|
||||
},
|
||||
"parameters": {
|
||||
"template": f"Template slug. Use `{template.slug}`.",
|
||||
"to": "Recipient WhatsApp number with country code, e.g. 919149154337",
|
||||
"customer_name": "Recipient name inserted into the template body",
|
||||
"report_id": "Optional unique report id for the View Report button URL suffix",
|
||||
},
|
||||
"example": {
|
||||
"template": template.slug,
|
||||
"to": "919149154337",
|
||||
"customer_name": "Suyash",
|
||||
"report_id": "run-abc123",
|
||||
},
|
||||
"response_example": {
|
||||
"success": True,
|
||||
"template": template.slug,
|
||||
"to": "919149154337",
|
||||
"customer_name": "Suyash",
|
||||
"report_id": "run-abc123",
|
||||
"report_url": f"{template.button_url_base or ''}run-abc123",
|
||||
"wa_message_id": "wamid.HBg...",
|
||||
"dispatch_id": "00000000-0000-0000-0000-000000000001",
|
||||
},
|
||||
},
|
||||
"notes": [
|
||||
"Subscribe to the template in the Urmi console before calling this API.",
|
||||
"Messages are sent from the Urmi AI WhatsApp business number on your behalf.",
|
||||
"If report_id is omitted, Urmi generates a unique id automatically.",
|
||||
],
|
||||
}
|
||||
63
backend/app/agent_templates_seed.py
Normal file
63
backend/app/agent_templates_seed.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .models import AgentMessageTemplate
|
||||
|
||||
DEFAULT_AGENT_TEMPLATES = [
|
||||
{
|
||||
"slug": "agent_run_completion",
|
||||
"name": "Agent Run Success",
|
||||
"description": (
|
||||
"Notify end users when an Urmi AI agent run completes, with a dynamic "
|
||||
"View Report button linking to the completion report."
|
||||
),
|
||||
"header_text": "Agent Run Complete",
|
||||
"body_preview": (
|
||||
"Hello {{customer_name}}, your agent run is complete, you can access "
|
||||
"the completion report at your URMI AI Portal."
|
||||
),
|
||||
"footer_text": "Thanks and regards, team - URMI AI",
|
||||
"meta_template_name": "agent_run_completion",
|
||||
"language": "en",
|
||||
"category": "utility",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "customer_name",
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"description": "Recipient display name shown in the greeting",
|
||||
"example": "Suyash",
|
||||
},
|
||||
{
|
||||
"name": "report_id",
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"description": "Unique report identifier appended to the View Report button URL",
|
||||
"example": "run-abc123",
|
||||
},
|
||||
],
|
||||
"button_label": "View Report",
|
||||
"button_url_base": "https://urmiai.com/reports/",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def seed_agent_message_templates(db: Session) -> None:
|
||||
for item in DEFAULT_AGENT_TEMPLATES:
|
||||
existing = db.scalar(select(AgentMessageTemplate).where(AgentMessageTemplate.slug == item["slug"]))
|
||||
if existing:
|
||||
existing.name = item["name"]
|
||||
existing.description = item["description"]
|
||||
existing.header_text = item["header_text"]
|
||||
existing.body_preview = item["body_preview"]
|
||||
existing.footer_text = item["footer_text"]
|
||||
existing.meta_template_name = item["meta_template_name"]
|
||||
existing.language = item["language"]
|
||||
existing.category = item["category"]
|
||||
existing.parameters = item["parameters"]
|
||||
existing.button_label = item["button_label"]
|
||||
existing.button_url_base = item["button_url_base"]
|
||||
existing.is_active = True
|
||||
else:
|
||||
db.add(AgentMessageTemplate(**item))
|
||||
db.commit()
|
||||
|
|
@ -26,11 +26,16 @@ from .routes_numbers import router as numbers_router
|
|||
from .routes_urmiotp import router as urmiotp_router
|
||||
from .routes_voice import router as voice_router
|
||||
from .routes_whatsapp import router as whatsapp_router
|
||||
from .routes_agent_templates import router as agent_templates_router
|
||||
from .routes_agent_messages import router as agent_messages_router
|
||||
from .agent_templates_seed import seed_agent_message_templates
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
Base.metadata.create_all(bind=engine)
|
||||
with Session(engine) as db:
|
||||
seed_agent_message_templates(db)
|
||||
with engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
|
|
@ -88,6 +93,8 @@ app.include_router(numbers_router)
|
|||
app.include_router(integrations_router)
|
||||
app.include_router(voice_router)
|
||||
app.include_router(whatsapp_router)
|
||||
app.include_router(agent_templates_router)
|
||||
app.include_router(agent_messages_router)
|
||||
app.include_router(admin_router)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,12 @@ class UserAccount(Base):
|
|||
sms_delivery_webhooks: Mapped[list["SmsDeliveryWebhook"]] = relationship(
|
||||
back_populates="user", cascade="all, delete-orphan"
|
||||
)
|
||||
agent_template_subscriptions: Mapped[list["AgentTemplateSubscription"]] = relationship(
|
||||
back_populates="user", cascade="all, delete-orphan"
|
||||
)
|
||||
agent_message_dispatches: Mapped[list["AgentMessageDispatchLog"]] = relationship(
|
||||
back_populates="user", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class PasswordCredential(Base):
|
||||
|
|
@ -527,6 +533,82 @@ class WhatsAppMessage(Base):
|
|||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||
|
||||
|
||||
class AgentMessageTemplate(Base):
|
||||
"""Platform WhatsApp agent message templates available for user subscription."""
|
||||
|
||||
__tablename__ = "agent_message_templates"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
slug: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String(120))
|
||||
description: Mapped[str] = mapped_column(String(1000))
|
||||
header_text: Mapped[str | None] = mapped_column(String(120), nullable=True)
|
||||
body_preview: Mapped[str] = mapped_column(String(2000))
|
||||
footer_text: Mapped[str | None] = mapped_column(String(120), nullable=True)
|
||||
meta_template_name: Mapped[str] = mapped_column(String(80), index=True)
|
||||
language: Mapped[str] = mapped_column(String(12), default="en")
|
||||
category: Mapped[str] = mapped_column(String(32), default="utility")
|
||||
parameters: Mapped[list] = mapped_column(JSON, default=list)
|
||||
button_label: Mapped[str | None] = mapped_column(String(40), nullable=True)
|
||||
button_url_base: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
subscriptions: Mapped[list["AgentTemplateSubscription"]] = relationship(
|
||||
back_populates="template", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class AgentTemplateSubscription(Base):
|
||||
__tablename__ = "agent_template_subscriptions"
|
||||
__table_args__ = (UniqueConstraint("user_id", "template_id", name="uq_agent_template_subscription"),)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
template_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("agent_message_templates.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
subscribed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
user: Mapped[UserAccount] = relationship(back_populates="agent_template_subscriptions")
|
||||
template: Mapped[AgentMessageTemplate] = relationship(back_populates="subscriptions")
|
||||
|
||||
|
||||
class AgentTemplateTestLog(Base):
|
||||
__tablename__ = "agent_template_test_logs"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
template_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("agent_message_templates.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
recipient: Mapped[str] = mapped_column(String(20))
|
||||
success: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
tested_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||
|
||||
|
||||
class AgentMessageDispatchLog(Base):
|
||||
__tablename__ = "agent_message_dispatch_logs"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
template_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("agent_message_templates.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
recipient: Mapped[str] = mapped_column(String(20), index=True)
|
||||
parameters: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
wa_message_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(24), default="sent", index=True)
|
||||
source: Mapped[str] = mapped_column(String(16), default="api", index=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||
user: Mapped[UserAccount] = relationship(back_populates="agent_message_dispatches")
|
||||
template: Mapped[AgentMessageTemplate] = relationship()
|
||||
|
||||
|
||||
class PlatformSetting(Base):
|
||||
"""Runtime overrides for platform credentials (admin-editable)."""
|
||||
|
||||
|
|
|
|||
106
backend/app/routes_agent_messages.py
Normal file
106
backend/app/routes_agent_messages.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .agent_message_service import SendAgentMessagePayload, get_template_by_slug, send_agent_template_message
|
||||
from .api_key_auth import current_api_user
|
||||
from .database import get_db
|
||||
from .models import AgentMessageTemplate, UserAccount
|
||||
|
||||
router = APIRouter(prefix="/api/v1/agent", tags=["agent-messages"])
|
||||
|
||||
|
||||
def _explorer_doc_for_template(template: AgentMessageTemplate) -> dict:
|
||||
return {
|
||||
"id": "send-agent-message",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/agent/messages/send",
|
||||
"title": "Send agent WhatsApp message",
|
||||
"summary": (
|
||||
"Send an approved agent completion template through Urmi. Urmi authenticates your API key "
|
||||
"and delivers the WhatsApp message from the Urmi AI business number on your behalf."
|
||||
),
|
||||
"authentication": {
|
||||
"type": "api_key",
|
||||
"header": "Authorization",
|
||||
"format": "Bearer urmi_live_<key>",
|
||||
"alternate_header": "X-Api-Key",
|
||||
},
|
||||
"request_body": {
|
||||
"template": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"description": "Subscribed agent template slug",
|
||||
"example": template.slug,
|
||||
},
|
||||
"to": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"description": "Recipient WhatsApp number with country code",
|
||||
"example": "919149154337",
|
||||
},
|
||||
"customer_name": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"description": "Recipient name shown in the template body",
|
||||
"example": "Suyash",
|
||||
},
|
||||
"report_id": {
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"description": "Report id suffix for the dynamic View Report button URL",
|
||||
"example": "run-abc123",
|
||||
},
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "WhatsApp template accepted by Meta",
|
||||
"example": {
|
||||
"success": True,
|
||||
"template": template.slug,
|
||||
"to": "919149154337",
|
||||
"customer_name": "Suyash",
|
||||
"report_id": "run-abc123",
|
||||
"report_url": f"{template.button_url_base or ''}run-abc123",
|
||||
"wa_message_id": "wamid.HBg...",
|
||||
"dispatch_id": "00000000-0000-0000-0000-000000000001",
|
||||
},
|
||||
},
|
||||
"401": {"description": "Missing or invalid API key"},
|
||||
"403": {"description": "Template not subscribed"},
|
||||
"429": {"description": "Console test limit (console only)"},
|
||||
"502": {"description": "Meta WhatsApp API error"},
|
||||
},
|
||||
"notes": [
|
||||
"Subscribe to agent templates in Console → Agent → UrmiAI before calling this API.",
|
||||
"Urmi acts as the WhatsApp tech provider and sends from the Urmi AI WABA.",
|
||||
],
|
||||
"available_templates": [
|
||||
{
|
||||
"slug": template.slug,
|
||||
"name": template.name,
|
||||
"meta_template_name": template.meta_template_name,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/messages/docs/send")
|
||||
def send_agent_message_docs(db: Session = Depends(get_db)) -> dict:
|
||||
template = db.scalar(
|
||||
select(AgentMessageTemplate)
|
||||
.where(AgentMessageTemplate.is_active.is_(True))
|
||||
.order_by(AgentMessageTemplate.created_at.asc())
|
||||
)
|
||||
if not template:
|
||||
return {"id": "send-agent-message", "method": "POST", "path": "/api/v1/agent/messages/send"}
|
||||
return _explorer_doc_for_template(template)
|
||||
|
||||
|
||||
@router.post("/messages/send")
|
||||
async def send_agent_message(
|
||||
payload: SendAgentMessagePayload,
|
||||
db: Session = Depends(get_db),
|
||||
user: UserAccount = Depends(current_api_user),
|
||||
) -> dict:
|
||||
return await send_agent_template_message(db, user, payload, source="api")
|
||||
144
backend/app/routes_agent_templates.py
Normal file
144
backend/app/routes_agent_templates.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .agent_message_service import (
|
||||
build_agent_message_docs,
|
||||
can_run_console_test,
|
||||
get_template_by_slug,
|
||||
run_console_template_test,
|
||||
serialize_agent_template,
|
||||
)
|
||||
from .auth import current_user
|
||||
from .database import get_db
|
||||
from .models import AgentMessageTemplate, AgentTemplateSubscription, UserAccount
|
||||
|
||||
router = APIRouter(prefix="/api/v1/agent-templates", tags=["agent-templates"])
|
||||
|
||||
|
||||
class AgentTemplateTestRequest(BaseModel):
|
||||
to: str = Field(min_length=10, max_length=20)
|
||||
customer_name: str = Field(min_length=1, max_length=120)
|
||||
report_id: str | None = Field(default=None, max_length=120)
|
||||
|
||||
|
||||
def _subscription_map(db: Session, user: UserAccount) -> set[str]:
|
||||
rows = db.scalars(
|
||||
select(AgentTemplateSubscription.template_id).where(AgentTemplateSubscription.user_id == user.id)
|
||||
).all()
|
||||
return set(rows)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_agent_templates(
|
||||
db: Session = Depends(get_db),
|
||||
user: UserAccount = Depends(current_user),
|
||||
) -> list[dict]:
|
||||
subscribed_ids = _subscription_map(db, user)
|
||||
templates = db.scalars(
|
||||
select(AgentMessageTemplate)
|
||||
.where(AgentMessageTemplate.is_active.is_(True))
|
||||
.order_by(AgentMessageTemplate.created_at.asc())
|
||||
).all()
|
||||
return [
|
||||
serialize_agent_template(template, subscribed=template.id in subscribed_ids)
|
||||
for template in templates
|
||||
]
|
||||
|
||||
|
||||
@router.post("/{slug}/subscribe")
|
||||
def subscribe_agent_template(
|
||||
slug: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: UserAccount = Depends(current_user),
|
||||
) -> dict:
|
||||
template = get_template_by_slug(db, slug)
|
||||
existing = db.scalar(
|
||||
select(AgentTemplateSubscription).where(
|
||||
AgentTemplateSubscription.user_id == user.id,
|
||||
AgentTemplateSubscription.template_id == template.id,
|
||||
)
|
||||
)
|
||||
if existing:
|
||||
return serialize_agent_template(template, subscribed=True)
|
||||
db.add(AgentTemplateSubscription(user_id=user.id, template_id=template.id))
|
||||
db.commit()
|
||||
return serialize_agent_template(template, subscribed=True)
|
||||
|
||||
|
||||
@router.delete("/{slug}/subscribe")
|
||||
def unsubscribe_agent_template(
|
||||
slug: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: UserAccount = Depends(current_user),
|
||||
) -> dict:
|
||||
template = get_template_by_slug(db, slug)
|
||||
subscription = db.scalar(
|
||||
select(AgentTemplateSubscription).where(
|
||||
AgentTemplateSubscription.user_id == user.id,
|
||||
AgentTemplateSubscription.template_id == template.id,
|
||||
)
|
||||
)
|
||||
if subscription:
|
||||
db.delete(subscription)
|
||||
db.commit()
|
||||
return serialize_agent_template(template, subscribed=False)
|
||||
|
||||
|
||||
@router.get("/{slug}/docs")
|
||||
def agent_template_docs(
|
||||
slug: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: UserAccount = Depends(current_user),
|
||||
) -> dict:
|
||||
template = get_template_by_slug(db, slug)
|
||||
subscription = db.scalar(
|
||||
select(AgentTemplateSubscription).where(
|
||||
AgentTemplateSubscription.user_id == user.id,
|
||||
AgentTemplateSubscription.template_id == template.id,
|
||||
)
|
||||
)
|
||||
if not subscription:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Subscribe to this template to view API documentation",
|
||||
)
|
||||
return build_agent_message_docs(template)
|
||||
|
||||
|
||||
@router.get("/{slug}/test-status")
|
||||
def agent_template_test_status(
|
||||
slug: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: UserAccount = Depends(current_user),
|
||||
) -> dict:
|
||||
template = get_template_by_slug(db, slug)
|
||||
require = db.scalar(
|
||||
select(AgentTemplateSubscription).where(
|
||||
AgentTemplateSubscription.user_id == user.id,
|
||||
AgentTemplateSubscription.template_id == template.id,
|
||||
)
|
||||
)
|
||||
if not require:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Subscribe first")
|
||||
status_payload = can_run_console_test(db, user, template)
|
||||
return {"template": template.slug, **status_payload}
|
||||
|
||||
|
||||
@router.post("/{slug}/test")
|
||||
async def test_agent_template(
|
||||
slug: str,
|
||||
payload: AgentTemplateTestRequest,
|
||||
db: Session = Depends(get_db),
|
||||
user: UserAccount = Depends(current_user),
|
||||
) -> dict:
|
||||
template = get_template_by_slug(db, slug)
|
||||
return await run_console_template_test(
|
||||
db,
|
||||
user,
|
||||
template,
|
||||
to=payload.to,
|
||||
customer_name=payload.customer_name,
|
||||
report_id=payload.report_id,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue