refactor: extract shared modules chatwoot_client.py + inboxes_io.py (v1.8)
- chatwoot_client.py (364 lines): unified Chatwoot session auth + API calls - inboxes_io.py (122 lines): unified inboxes.json read/write/validate - provision_server.py: deleted 9 duplicate functions (-188 lines) - ws_agent: deleted 5 duplicate auth functions, removed import requests - All imports use sys.path.insert for cross-directory access - Zero behavior changes, pure DRY refactor
This commit is contained in:
+122
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unified Inboxes Config IO — shared by WS Agent & Provision Server
|
||||
|
||||
Provides stateless read/write/validate/construct primitives for
|
||||
Chatwoot inbox routing configuration (inboxes.json).
|
||||
|
||||
Usage:
|
||||
import inboxes_io
|
||||
cfg = inboxes_io.read_inboxes_raw(Path("inboxes.json"))
|
||||
entry = inboxes_io.build_inbox_entry(...)
|
||||
inboxes_io.write_inboxes(Path("inboxes.json"), cfg)
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
log = logging.getLogger("inboxes_io")
|
||||
|
||||
# ── Default _meta block ──────────────────────────────────────────
|
||||
INBOXES_META = {
|
||||
"_meta": {
|
||||
"version": "1.2",
|
||||
"updated_at": None, # filled at write time
|
||||
"description": "Chatwoot WS Agent inbox routing config — hot-reloadable",
|
||||
},
|
||||
}
|
||||
|
||||
# ── Required field sets ──────────────────────────────────────────
|
||||
REQUIRED_ENTRY_KEYS = ["name", "target_agent", "system_prompt", "prompt_template"]
|
||||
TEMPLATE_PLACEHOLDERS = ["{sender_name}", "{customer_msg}"]
|
||||
|
||||
|
||||
def validate_entry(config: dict) -> bool:
|
||||
"""Validate a single inbox config entry.
|
||||
|
||||
Checks required keys exist and prompt_template contains
|
||||
the mandatory placeholders.
|
||||
"""
|
||||
if not isinstance(config, dict):
|
||||
log.warning("Config is not a dict: %s", type(config).__name__)
|
||||
return False
|
||||
for key in REQUIRED_ENTRY_KEYS:
|
||||
if key not in config:
|
||||
log.warning("Config missing required key '%s'", key)
|
||||
return False
|
||||
prompt = config.get("prompt_template", "")
|
||||
for ph in TEMPLATE_PLACEHOLDERS:
|
||||
if ph not in prompt:
|
||||
log.warning("prompt_template missing placeholder %s", ph)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def read_inboxes_raw(path: Path) -> dict:
|
||||
"""Read inboxes.json from *path*.
|
||||
|
||||
Returns the parsed dict, or a dict with just ``_meta`` if the
|
||||
file is missing. Never returns ``None``.
|
||||
"""
|
||||
if path.exists():
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception as e:
|
||||
log.warning("Failed to read %s: %s", path, e)
|
||||
meta = dict(INBOXES_META)
|
||||
meta["_meta"] = dict(meta["_meta"])
|
||||
meta["_meta"]["updated_at"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
return meta
|
||||
|
||||
|
||||
def write_inboxes(path: Path, config: dict) -> None:
|
||||
"""Write *config* (dict) to *path* as pretty-printed JSON."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Always refresh _meta timestamp
|
||||
if "_meta" not in config or not isinstance(config["_meta"], dict):
|
||||
config["_meta"] = dict(INBOXES_META["_meta"])
|
||||
config["_meta"]["updated_at"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
path.write_text(json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
log.info("Inboxes config written to %s", path)
|
||||
|
||||
|
||||
def ensure_agent_workspace(base_dir: Path, agent_id: str, name: str = "") -> Path:
|
||||
"""Create workspace directory for a QwenPaw agent.
|
||||
|
||||
Returns the created Path.
|
||||
"""
|
||||
agent_dir = base_dir / agent_id
|
||||
agent_dir.mkdir(parents=True, exist_ok=True)
|
||||
return agent_dir
|
||||
|
||||
|
||||
def build_inbox_entry(name: str, domain: str, channel: str,
|
||||
inbox_id: int, inbox_token: str, agent_id: str) -> dict:
|
||||
"""Build an inbox config entry dict for *inboxes.json*.
|
||||
|
||||
The entry includes a generic system_prompt and prompt_template
|
||||
that callers can override after receiving the dict.
|
||||
"""
|
||||
return {
|
||||
"name": name,
|
||||
"type": channel,
|
||||
"target_agent": agent_id,
|
||||
"system_prompt": (
|
||||
f"You are a customer service agent for {name} ({domain}). "
|
||||
f"Answer questions professionally in the customer's language. "
|
||||
f"If you cannot fully resolve the issue, end with [HANDOFF]."
|
||||
),
|
||||
"prompt_template": (
|
||||
"Customer '{sender_name}' sent this message:\n\n"
|
||||
"{customer_msg}\n\n"
|
||||
"Write a direct reply (no preamble, no markdown). "
|
||||
"Keep it concise (2-4 sentences). "
|
||||
"Use the same language as the customer."
|
||||
),
|
||||
"note_prefix": f"\U0001f916 AI \u81ea\u52a8\u56de\u590d ({name})",
|
||||
"signature": "",
|
||||
"status": "active",
|
||||
}
|
||||
Reference in New Issue
Block a user