Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
@@ -92,3 +92,136 @@ async def add_private_note(
|
||||
"response": response.json(),
|
||||
"action_result": action_result.model_dump(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
def _chatwoot_message_url(conversation_id: str) -> str:
|
||||
return (
|
||||
settings.chatwoot_base_url.rstrip("/")
|
||||
+ f"/api/v1/accounts/{settings.chatwoot_account_id}"
|
||||
+ f"/conversations/{conversation_id}/messages"
|
||||
)
|
||||
|
||||
|
||||
def _chatwoot_ready() -> Dict:
|
||||
if not settings.chatwoot_write_enabled:
|
||||
return {"enabled": False, "status": "skipped", "reason": "CHATWOOT_WRITE_ENABLED=false"}
|
||||
if not settings.chatwoot_base_url or not settings.chatwoot_account_id or not settings.chatwoot_api_token:
|
||||
return {"enabled": True, "status": "skipped", "reason": "missing Chatwoot config"}
|
||||
return {"enabled": True, "status": "ready"}
|
||||
|
||||
|
||||
|
||||
def format_chatwoot_public_message(content: str) -> str:
|
||||
"""Prepare operator-written text for Chatwoot public messages.
|
||||
|
||||
Chatwoot renders public message content as Markdown-like text. In that
|
||||
renderer, blank-line paragraph breaks are preserved but single newlines
|
||||
inside a paragraph can be collapsed into spaces. That breaks operator
|
||||
replies with price lists written as separate lines using "–" bullets.
|
||||
|
||||
The textarea remains plain text in ClientFlow. Before sending, we add
|
||||
Markdown hard-break markers (two trailing spaces before line breaks) to
|
||||
non-empty lines, so the text keeps the same visual line breaks in
|
||||
Chatwoot without requiring operators to know Markdown.
|
||||
"""
|
||||
text = str(content or "").replace("\r\n", "\n").replace("\r", "\n").strip()
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
formatted_lines: list[str] = []
|
||||
for line in text.split("\n"):
|
||||
clean_line = line.rstrip()
|
||||
if not clean_line:
|
||||
formatted_lines.append("")
|
||||
continue
|
||||
formatted_lines.append(clean_line + " ")
|
||||
|
||||
return "\n".join(formatted_lines).strip()
|
||||
|
||||
|
||||
async def send_public_message(conversation_id: str, content: str) -> Dict:
|
||||
"""Send a public outgoing Chatwoot message.
|
||||
|
||||
This is used by the ClientFlow reply assistant. It is deliberately gated by
|
||||
CHATWOOT_WRITE_ENABLED and returns a structured result instead of raising for
|
||||
normal configuration skips.
|
||||
"""
|
||||
conversation_id = str(conversation_id or "").strip()
|
||||
content = format_chatwoot_public_message(content)
|
||||
if not conversation_id:
|
||||
return {"enabled": settings.chatwoot_write_enabled, "status": "failed", "reason": "missing conversation_id"}
|
||||
if not content:
|
||||
return {"enabled": settings.chatwoot_write_enabled, "status": "failed", "reason": "empty content"}
|
||||
|
||||
ready = _chatwoot_ready()
|
||||
if ready.get("status") != "ready":
|
||||
return ready
|
||||
|
||||
payload = {
|
||||
"content": content,
|
||||
"message_type": "outgoing",
|
||||
"private": False,
|
||||
"content_type": "text",
|
||||
"content_attributes": {},
|
||||
}
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"api_access_token": settings.chatwoot_api_token,
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=30) as client:
|
||||
response = await client.post(_chatwoot_message_url(conversation_id), headers=headers, json=payload)
|
||||
|
||||
if response.status_code >= 400:
|
||||
return {"enabled": True, "status": "failed", "status_code": response.status_code, "body": response.text}
|
||||
return {"enabled": True, "status": "sent", "status_code": response.status_code, "response": response.json()}
|
||||
|
||||
|
||||
async def send_public_message_with_attachments(
|
||||
conversation_id: str,
|
||||
content: str,
|
||||
*,
|
||||
attachments: list[dict] | None = None,
|
||||
) -> Dict:
|
||||
"""Send a public message and optional PDF attachments through Chatwoot.
|
||||
|
||||
``attachments`` items must contain filename, content bytes and content_type.
|
||||
Chatwoot expects multipart/form-data with ``attachments[]`` fields when
|
||||
files are present.
|
||||
"""
|
||||
attachments = attachments or []
|
||||
if not attachments:
|
||||
return await send_public_message(conversation_id, content)
|
||||
|
||||
conversation_id = str(conversation_id or "").strip()
|
||||
content = format_chatwoot_public_message(content)
|
||||
if not conversation_id:
|
||||
return {"enabled": settings.chatwoot_write_enabled, "status": "failed", "reason": "missing conversation_id"}
|
||||
if not content:
|
||||
return {"enabled": settings.chatwoot_write_enabled, "status": "failed", "reason": "empty content"}
|
||||
|
||||
ready = _chatwoot_ready()
|
||||
if ready.get("status") != "ready":
|
||||
return ready
|
||||
|
||||
data = {
|
||||
"content": content,
|
||||
"message_type": "outgoing",
|
||||
"private": "false",
|
||||
"content_type": "text",
|
||||
}
|
||||
files = []
|
||||
for attachment in attachments:
|
||||
filename = str(attachment.get("filename") or "documento.pdf")
|
||||
file_bytes = attachment.get("content") or b""
|
||||
content_type = str(attachment.get("content_type") or "application/pdf")
|
||||
files.append(("attachments[]", (filename, file_bytes, content_type)))
|
||||
|
||||
headers = {"api_access_token": settings.chatwoot_api_token}
|
||||
async with httpx.AsyncClient(timeout=60) as client:
|
||||
response = await client.post(_chatwoot_message_url(conversation_id), headers=headers, data=data, files=files)
|
||||
|
||||
if response.status_code >= 400:
|
||||
return {"enabled": True, "status": "failed", "status_code": response.status_code, "body": response.text}
|
||||
return {"enabled": True, "status": "sent", "status_code": response.status_code, "response": response.json()}
|
||||
|
||||
Reference in New Issue
Block a user