@innvisor/conny-ai 9.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +68 -0
- package/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/brand-assets/A_dark_luxury_web_background_202605210700.jpeg +0 -0
- package/brand-assets/Conny.web.logo.png +0 -0
- package/brand-assets/Logo_Conny_Petalo_Claro.png +0 -0
- package/brand-assets/cl-nica-de-las-am-ricas/manifest.json +22 -0
- package/brand-assets/cl-nica-de-las-am-ricas/processed/business-identity.txt +11 -0
- package/brand-assets/cl-nica-de-las-am-ricas/raw/business-identity.txt +11 -0
- package/brand-assets/cl-nica-las-am-ricas/manifest.json +22 -0
- package/brand-assets/cl-nica-las-am-ricas/processed/business-identity.txt +11 -0
- package/brand-assets/cl-nica-las-am-ricas/raw/business-identity.txt +11 -0
- package/brand-assets/conny-demo/manifest.json +22 -0
- package/brand-assets/conny-demo/processed/business-identity.txt +7 -0
- package/brand-assets/conny-demo/raw/business-identity.txt +7 -0
- package/brand-assets/conny-logo.png +0 -0
- package/brand-assets/web.background.png +0 -0
- package/brand_assets.py +323 -0
- package/conny +28 -0
- package/conny-chat.py +579 -0
- package/conny-omni.py +3843 -0
- package/conny.py +113 -0
- package/conny_agents/__init__.py +1 -0
- package/conny_agents/agenda.py +1 -0
- package/conny_agents/captacion.py +1 -0
- package/conny_agents/conocimiento.py +1 -0
- package/conny_agents/escalacion.py +1 -0
- package/conny_agents/objeciones.py +1 -0
- package/conny_agents/seguimiento.py +1 -0
- package/conny_app.py +287 -0
- package/conny_audio.py +350 -0
- package/conny_audio_learn.py +84 -0
- package/conny_brain_v10.py +804 -0
- package/conny_bridge.py +656 -0
- package/conny_calendar.py +169 -0
- package/conny_cli.py +11784 -0
- package/conny_cli_bb.py +437 -0
- package/conny_commands.py +243 -0
- package/conny_config.py +215 -0
- package/conny_core/__init__.py +3 -0
- package/conny_core/conversation_engine.py +446 -0
- package/conny_core/first_turn_ops.py +287 -0
- package/conny_core/persona_registry.py +157 -0
- package/conny_core/prompt_ops.py +561 -0
- package/conny_cron.py +72 -0
- package/conny_demo_v2.py +209 -0
- package/conny_demo_voice.py +134 -0
- package/conny_design.py +43 -0
- package/conny_doctor.py +319 -0
- package/conny_domino.py +696 -0
- package/conny_generator.py +447 -0
- package/conny_google_auth.py +159 -0
- package/conny_i18n.py +619 -0
- package/conny_init.py +509 -0
- package/conny_integrations/__init__.py +4 -0
- package/conny_integrations/llm.py +1 -0
- package/conny_integrations/vault.py +77 -0
- package/conny_integrations/whatsapp.py +1 -0
- package/conny_intelligence.py +65 -0
- package/conny_learning.py +154 -0
- package/conny_memory.py +243 -0
- package/conny_memory_engine.py +292 -0
- package/conny_nova_proxy.py +170 -0
- package/conny_nuke_robot_phrases.py +493 -0
- package/conny_pairing.py +253 -0
- package/conny_patch.py +291 -0
- package/conny_persona_cli.py +150 -0
- package/conny_router.py +308 -0
- package/conny_runtime_ops.py +271 -0
- package/conny_session.py +516 -0
- package/conny_skills/__init__.py +1 -0
- package/conny_skills/demo_mode.py +35 -0
- package/conny_skills/text_processing.py +1 -0
- package/conny_skills/tone_detection.py +1 -0
- package/conny_smart_features.py +333 -0
- package/conny_studio.py +161 -0
- package/conny_sync_fix.py +306 -0
- package/conny_tui.py +512 -0
- package/conny_tui_select.py +202 -0
- package/conny_ultra_config.py +411 -0
- package/conny_uncertainty.py +174 -0
- package/conny_utils.py +87 -0
- package/conny_voice.py +156 -0
- package/conny_voice_engine.py +124 -0
- package/conny_web_search.py +66 -0
- package/conny_weekly_report.py +85 -0
- package/conny_worm.py +88 -0
- package/core/__init__.py +25 -0
- package/ecosystem.config.js +24 -0
- package/fix_init.py +27 -0
- package/install.sh +78 -0
- package/knowledge_base.py +330 -0
- package/nova/rules/default.yaml +37 -0
- package/nova_bridge.py +509 -0
- package/npm/conny.js +471 -0
- package/package.json +102 -0
- package/personas/conny/base/default.yaml +35 -0
- package/personas/conny/base/estetica_whatsapp.yaml +36 -0
- package/requirements.txt +14 -0
- package/run.sh +47 -0
- package/search.py +465 -0
- package/smart_handoff.py +1150 -0
- package/src/__init__.py +0 -0
- package/src/conny/__init__.py +0 -0
- package/src/conny/admin/__init__.py +0 -0
- package/src/conny/admin/api.py +234 -0
- package/src/conny/admin/dashboard.py +772 -0
- package/src/conny/api/__init__.py +0 -0
- package/src/conny/api/routes.py +8851 -0
- package/src/conny/brain/__init__.py +15 -0
- package/src/conny/brain/engine.py +804 -0
- package/src/conny/brain/learning.py +154 -0
- package/src/conny/brain/memory.py +324 -0
- package/src/conny/brain/smart_features.py +333 -0
- package/src/conny/brain/uncertainty.py +167 -0
- package/src/conny/channels/__init__.py +0 -0
- package/src/conny/channels/audio.py +316 -0
- package/src/conny/channels/cli.py +11795 -0
- package/src/conny/channels/logo_art.py +11 -0
- package/src/conny/channels/voice.py +156 -0
- package/src/conny/core/__init__.py +0 -0
- package/src/conny/core/config.py +215 -0
- package/src/conny/core/cron.py +72 -0
- package/src/conny/core/messenger.py +563 -0
- package/src/conny/core/router.py +297 -0
- package/src/conny/core/session.py +312 -0
- package/src/conny/demo/__init__.py +0 -0
- package/src/conny/demo/handler.py +3110 -0
- package/src/conny/integrations/__init__.py +19 -0
- package/src/conny/integrations/calendar.py +169 -0
- package/src/conny/integrations/knowledge.py +312 -0
- package/src/conny/integrations/search.py +66 -0
- package/src/conny/personas/__init__.py +0 -0
- package/src/conny/personas/generator.py +447 -0
- package/src/conny/production/__init__.py +0 -0
- package/src/conny/production/domino.py +696 -0
- package/src/conny/production/guard.py +550 -0
- package/src/conny/production/handoff.py +1150 -0
- package/src/conny/production/monitor.py +353 -0
- package/src/conny/utils/__init__.py +2 -0
- package/src/conny/utils/helpers.py +75 -0
- package/src/conny/utils/i18n.py +619 -0
- package/src/core/admin_engines.py +772 -0
- package/src/core/globals.py +11845 -0
- package/src/core/orchestrator.py +273 -0
- package/src/core/production_monitor.py +353 -0
- package/src/core/runtime.py +5487 -0
- package/src/domain/onboarding_flow.py +230 -0
- package/src/domain/prompts/__init__.py +1 -0
- package/src/domain/prompts/prospect_pitch.py +282 -0
- package/src/domain/send_guard.py +636 -0
- package/src/domain/swarm/queen.py +96 -0
- package/src/infrastructure/llm_providers/engine.py +487 -0
- package/src/interfaces/mcp_server.py +73 -0
- package/src/interfaces/nova_bridge.py +58 -0
- package/src/interfaces/web/admin_api.py +1379 -0
- package/src/interfaces/web/app.py +9408 -0
- package/src/interfaces/web/demo_handler.py +3450 -0
- package/src/interfaces/web/static/generate_avatars.py +46 -0
- package/v7/__init__.py +46 -0
- package/v7/agents/__init__.py +46 -0
- package/v7/agents/agenda.py +77 -0
- package/v7/agents/base.py +216 -0
- package/v7/agents/captacion.py +60 -0
- package/v7/agents/conocimiento.py +69 -0
- package/v7/agents/escalacion.py +83 -0
- package/v7/agents/objeciones.py +109 -0
- package/v7/agents/seguimiento.py +71 -0
- package/v7/memory/__init__.py +46 -0
- package/v7/memory/patient_profile.py +200 -0
- package/v7/orchestrator.py +275 -0
- package/v7/postprocess.py +127 -0
- package/v7/router.py +239 -0
- package/verify_conversation_impl.py +48 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""conny_persona_cli.py — Agency persona control CLI."""
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
PERSONAS_DIR = Path("/home/ubuntu/conny/personas")
|
|
13
|
+
INSTANCES_DIR = Path("/home/ubuntu/conny-instances")
|
|
14
|
+
API_BASE = "http://localhost:8001"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_persona_path(instance_id: str) -> Path:
|
|
18
|
+
candidates = [
|
|
19
|
+
INSTANCES_DIR / instance_id / "personas" / "persona.yaml",
|
|
20
|
+
PERSONAS_DIR / instance_id / "persona.yaml",
|
|
21
|
+
PERSONAS_DIR / instance_id / "runtime_override.json",
|
|
22
|
+
]
|
|
23
|
+
for c in candidates:
|
|
24
|
+
if c.exists():
|
|
25
|
+
return c
|
|
26
|
+
return PERSONAS_DIR / instance_id / "persona.yaml"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_key():
|
|
30
|
+
return os.getenv("ADMIN_API_KEY", os.getenv("MASTER_API_KEY", "conny_master_2026_santiago"))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def cmd_list(args):
|
|
34
|
+
print("Available instances:")
|
|
35
|
+
if INSTANCES_DIR.exists():
|
|
36
|
+
for d in sorted(INSTANCES_DIR.iterdir()):
|
|
37
|
+
if d.is_dir() and (d / ".env").exists():
|
|
38
|
+
print(f" {d.name} (instance)")
|
|
39
|
+
if PERSONAS_DIR.exists():
|
|
40
|
+
for d in sorted(PERSONAS_DIR.iterdir()):
|
|
41
|
+
if d.is_dir():
|
|
42
|
+
print(f" {d.name} (persona)")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def cmd_show(args):
|
|
46
|
+
path = get_persona_path(args.instance)
|
|
47
|
+
if not path.exists():
|
|
48
|
+
print(f"No persona found for '{args.instance}'")
|
|
49
|
+
return
|
|
50
|
+
print(f"Persona: {args.instance} ({path})")
|
|
51
|
+
print("─" * 50)
|
|
52
|
+
print(path.read_text())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def cmd_set(args):
|
|
56
|
+
payload = {args.field: args.value}
|
|
57
|
+
if args.field == "forbidden_topics":
|
|
58
|
+
payload[args.field] = [t.strip() for t in args.value.split(",")]
|
|
59
|
+
try:
|
|
60
|
+
r = httpx.post(f"{API_BASE}/admin/{args.instance}/persona",
|
|
61
|
+
json=payload, headers={"X-Admin-Key": _get_key()}, timeout=10)
|
|
62
|
+
if r.status_code == 200:
|
|
63
|
+
print(f"✓ Set {args.field}={args.value} for {args.instance}")
|
|
64
|
+
else:
|
|
65
|
+
print(f"✗ Error: {r.status_code} — {r.text}")
|
|
66
|
+
except Exception as e:
|
|
67
|
+
print(f"✗ Connection error: {e}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cmd_test(args):
|
|
71
|
+
key = os.getenv("MASTER_API_KEY", "conny_master_2026_santiago")
|
|
72
|
+
test_messages = [
|
|
73
|
+
"Hola, quiero una cita",
|
|
74
|
+
"Cuánto cuesta la consulta?",
|
|
75
|
+
"Tienen disponibilidad mañana?",
|
|
76
|
+
"Gracias, me agendas a las 3pm",
|
|
77
|
+
"Chao, bendiciones",
|
|
78
|
+
]
|
|
79
|
+
print(f"Testing persona for: {args.instance}")
|
|
80
|
+
print("─" * 50)
|
|
81
|
+
for msg in test_messages:
|
|
82
|
+
try:
|
|
83
|
+
r = httpx.post(f"{API_BASE}/test",
|
|
84
|
+
json={"message": msg, "chat_id": f"persona_test_{args.instance}"},
|
|
85
|
+
headers={"X-Master-Key": key}, timeout=30)
|
|
86
|
+
data = r.json()
|
|
87
|
+
print(f" [USER] {msg}")
|
|
88
|
+
print(f" [CONNY] {data.get('response', 'error')[:120]}")
|
|
89
|
+
print()
|
|
90
|
+
except Exception as e:
|
|
91
|
+
print(f" [ERROR] {e}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_status(args):
|
|
95
|
+
try:
|
|
96
|
+
r = httpx.get(f"{API_BASE}/admin/{args.instance}/status",
|
|
97
|
+
headers={"X-Admin-Key": _get_key()}, timeout=10)
|
|
98
|
+
if r.status_code == 200:
|
|
99
|
+
data = r.json()
|
|
100
|
+
print(f"Instance: {data.get('instance_id')}")
|
|
101
|
+
print(f"Persona: {json.dumps(data.get('persona', {}), indent=2, ensure_ascii=False)}")
|
|
102
|
+
print(f"Gaps today: {data.get('gaps_today', 0)}")
|
|
103
|
+
else:
|
|
104
|
+
print(f"Error: {r.status_code}")
|
|
105
|
+
except Exception as e:
|
|
106
|
+
print(f"Connection error: {e}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def cmd_export(args):
|
|
110
|
+
path = get_persona_path(args.instance)
|
|
111
|
+
if path.exists():
|
|
112
|
+
print(path.read_text())
|
|
113
|
+
else:
|
|
114
|
+
print(f"No persona found for {args.instance}", file=sys.stderr)
|
|
115
|
+
sys.exit(1)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cmd_history(args):
|
|
119
|
+
path = get_persona_path(args.instance)
|
|
120
|
+
if path.exists():
|
|
121
|
+
print(f"Persona file: {path}")
|
|
122
|
+
print(f"Last modified: {datetime.fromtimestamp(path.stat().st_mtime).isoformat()}")
|
|
123
|
+
else:
|
|
124
|
+
print(f"No persona file found for {args.instance}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main():
|
|
128
|
+
parser = argparse.ArgumentParser(prog="conny persona", description="Agency persona control")
|
|
129
|
+
sub = parser.add_subparsers(dest="command")
|
|
130
|
+
|
|
131
|
+
sub.add_parser("list", help="List available instances")
|
|
132
|
+
p = sub.add_parser("show", help="Show persona config"); p.add_argument("instance")
|
|
133
|
+
p = sub.add_parser("set", help="Set a field"); p.add_argument("instance"); p.add_argument("field"); p.add_argument("value")
|
|
134
|
+
p = sub.add_parser("test", help="Dry-run test"); p.add_argument("instance")
|
|
135
|
+
p = sub.add_parser("status", help="Runtime status"); p.add_argument("instance")
|
|
136
|
+
p = sub.add_parser("export", help="Export persona"); p.add_argument("instance")
|
|
137
|
+
p = sub.add_parser("history", help="Change history"); p.add_argument("instance")
|
|
138
|
+
|
|
139
|
+
args = parser.parse_args()
|
|
140
|
+
if not args.command:
|
|
141
|
+
parser.print_help()
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
cmds = {"list": cmd_list, "show": cmd_show, "set": cmd_set, "test": cmd_test,
|
|
145
|
+
"status": cmd_status, "export": cmd_export, "history": cmd_history}
|
|
146
|
+
cmds[args.command](args)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
main()
|
package/conny_router.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
conny_router.py — Webhook handlers and command routing.
|
|
4
|
+
Extracted from conny.py for Phase 3 split.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import random
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from conny_config import Config, DEMO_CMD_ALIASES, DEMO_COMMANDS, DEMO_HELP_FULL
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger("conny.router")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ── Platform Detection ─────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
def detect_incoming_platform(body: Dict[str, Any]) -> str:
|
|
27
|
+
"""Detect which platform sent the message."""
|
|
28
|
+
if body.get("entry"):
|
|
29
|
+
entry = body["entry"][0] if body["entry"] else {}
|
|
30
|
+
changes = entry.get("changes", [{}])[0] if entry.get("changes") else {}
|
|
31
|
+
value = changes.get("value", {})
|
|
32
|
+
if value.get("messages"):
|
|
33
|
+
return "whatsapp_cloud"
|
|
34
|
+
if value.get("statuses"):
|
|
35
|
+
return "whatsapp_cloud"
|
|
36
|
+
if body.get("message") or body.get("edited_message"):
|
|
37
|
+
return "telegram"
|
|
38
|
+
if body.get("data", {}).get("key", {}).get("fromMe"):
|
|
39
|
+
return "evolution"
|
|
40
|
+
if body.get("iswa"):
|
|
41
|
+
return "whatsapp"
|
|
42
|
+
return "unknown"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ── Command Detection ──────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
def detect_command(text: str) -> Optional[str]:
|
|
48
|
+
"""Detect if text is a command (slash or natural language alias)."""
|
|
49
|
+
if not text:
|
|
50
|
+
return None
|
|
51
|
+
text_norm = text.lower().strip()
|
|
52
|
+
|
|
53
|
+
# Slash commands
|
|
54
|
+
if text_norm.startswith("/"):
|
|
55
|
+
return text_norm
|
|
56
|
+
|
|
57
|
+
# Natural language aliases
|
|
58
|
+
for alias, cmd in DEMO_CMD_ALIASES.items():
|
|
59
|
+
if text_norm == alias or text_norm == "/" + alias:
|
|
60
|
+
return "/" + cmd
|
|
61
|
+
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ── Full Command Handler (moved from enqueue_message) ────────────────────────
|
|
66
|
+
|
|
67
|
+
def handle_command(
|
|
68
|
+
chat_id: str,
|
|
69
|
+
text: str,
|
|
70
|
+
demo_sessions: Dict[str, Any],
|
|
71
|
+
send_fn,
|
|
72
|
+
) -> Optional[List[str]]:
|
|
73
|
+
"""
|
|
74
|
+
Handle commands BEFORE session lookup.
|
|
75
|
+
Returns a list of response strings, or None if not a command.
|
|
76
|
+
"""
|
|
77
|
+
cmd = text.strip()
|
|
78
|
+
if not cmd.startswith("/"):
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
# /help — show all commands
|
|
82
|
+
if cmd in ("/help", "/ayuda", "/comandos"):
|
|
83
|
+
bn = demo_sessions.get(chat_id + "_name", "")
|
|
84
|
+
if bn:
|
|
85
|
+
return [DEMO_HELP_FULL]
|
|
86
|
+
return [
|
|
87
|
+
"Comandos: /help | /reset | /bot | /status | /memoria"
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
# /reset
|
|
91
|
+
if cmd in ("/reset", "/reiniciar"):
|
|
92
|
+
keys_del = [k for k in list(demo_sessions) if k.startswith(chat_id + "_") and not k.endswith("_ts")]
|
|
93
|
+
for k in keys_del:
|
|
94
|
+
try:
|
|
95
|
+
del demo_sessions[k]
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
return ["listo, sesión limpia ||| empezamos de nuevo"]
|
|
99
|
+
|
|
100
|
+
# /status
|
|
101
|
+
if cmd in ("/status", "/estado"):
|
|
102
|
+
bn = demo_sessions.get(chat_id + "_name", "")
|
|
103
|
+
return [f"Estado: {'demo activa' if bn else 'en onboarding'} ||| negocio: {bn or 'sin nombre'}"]
|
|
104
|
+
|
|
105
|
+
# /bot
|
|
106
|
+
if cmd in ("/bot", "/recepcionista"):
|
|
107
|
+
return ["modo recepcionista ||| háblame como cliente y te respondo en contexto"]
|
|
108
|
+
|
|
109
|
+
# /memoria
|
|
110
|
+
if cmd in ("/memoria",):
|
|
111
|
+
return ["no tengo memoria activa todavía ||| en la próxima versión lo tendre"]
|
|
112
|
+
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ── Webhook Parser: Telegram ────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
def parse_telegram_message(body: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
119
|
+
"""Parse Telegram webhook body into standard message format."""
|
|
120
|
+
msg = body.get("message") or body.get("edited_message")
|
|
121
|
+
if not msg:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
chat = msg.get("chat") or {}
|
|
125
|
+
chat_id = str(chat.get("id", ""))
|
|
126
|
+
if not chat_id:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
voice = msg.get("voice") or msg.get("audio")
|
|
130
|
+
audio_id = voice.get("file_id") if voice else None
|
|
131
|
+
|
|
132
|
+
document = msg.get("document")
|
|
133
|
+
photos = msg.get("photo") or []
|
|
134
|
+
caption = msg.get("caption", "").strip()
|
|
135
|
+
|
|
136
|
+
attachments = []
|
|
137
|
+
if document:
|
|
138
|
+
attachments.append({
|
|
139
|
+
"kind": "document",
|
|
140
|
+
"platform": "telegram",
|
|
141
|
+
"file_id": document.get("file_id", ""),
|
|
142
|
+
"filename": document.get("file_name", "document.bin"),
|
|
143
|
+
"mime_type": document.get("mime_type", "application/octet-stream"),
|
|
144
|
+
"caption": caption,
|
|
145
|
+
})
|
|
146
|
+
if photos:
|
|
147
|
+
photo = photos[-1]
|
|
148
|
+
attachments.append({
|
|
149
|
+
"kind": "image",
|
|
150
|
+
"platform": "telegram",
|
|
151
|
+
"file_id": photo.get("file_id", ""),
|
|
152
|
+
"filename": f"telegram_photo_{photo.get('file_unique_id', 'image')}.jpg",
|
|
153
|
+
"mime_type": "image/jpeg",
|
|
154
|
+
"caption": caption,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
text = msg.get("text", "").strip() or caption
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
"chat_id": chat_id,
|
|
161
|
+
"text": text,
|
|
162
|
+
"audio_id": audio_id,
|
|
163
|
+
"attachments": attachments,
|
|
164
|
+
"platform": "telegram",
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ── Webhook Parser: WhatsApp Cloud ─────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
def parse_whatsapp_cloud_message(body: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
171
|
+
"""Parse WhatsApp Cloud API webhook body."""
|
|
172
|
+
try:
|
|
173
|
+
entry = body.get("entry", [{}])[0]
|
|
174
|
+
changes = entry.get("changes", [{}])[0]
|
|
175
|
+
value = changes.get("value", {})
|
|
176
|
+
msgs = value.get("messages", [])
|
|
177
|
+
if not msgs:
|
|
178
|
+
return None
|
|
179
|
+
msg = msgs[0]
|
|
180
|
+
chat_id = msg.get("from", "")
|
|
181
|
+
msg_type = msg.get("type", "text")
|
|
182
|
+
|
|
183
|
+
attachments = []
|
|
184
|
+
text = None
|
|
185
|
+
audio_id = None
|
|
186
|
+
|
|
187
|
+
if msg_type == "text":
|
|
188
|
+
text = msg.get("text", {}).get("body", "").strip()
|
|
189
|
+
elif msg_type in ("audio", "voice"):
|
|
190
|
+
audio_id = msg.get("audio", msg.get("voice", {})).get("id", "")
|
|
191
|
+
elif msg_type == "image":
|
|
192
|
+
attachments.append({
|
|
193
|
+
"kind": "image",
|
|
194
|
+
"platform": "whatsapp_cloud",
|
|
195
|
+
"media_id": msg.get("image", {}).get("id", ""),
|
|
196
|
+
"filename": f"wa_cloud_image_{msg.get('id', 'image')}.jpg",
|
|
197
|
+
"mime_type": msg.get("image", {}).get("mime_type", "image/jpeg"),
|
|
198
|
+
"caption": msg.get("image", {}).get("caption", ""),
|
|
199
|
+
})
|
|
200
|
+
text = msg.get("image", {}).get("caption", "").strip()
|
|
201
|
+
elif msg_type == "document":
|
|
202
|
+
attachments.append({
|
|
203
|
+
"kind": "document",
|
|
204
|
+
"platform": "whatsapp_cloud",
|
|
205
|
+
"media_id": msg.get("document", {}).get("id", ""),
|
|
206
|
+
"filename": msg.get("document", {}).get("filename", f"wa_cloud_{msg.get('id', 'document')}"),
|
|
207
|
+
"mime_type": msg.get("document", {}).get("mime_type", "application/octet-stream"),
|
|
208
|
+
"caption": msg.get("document", {}).get("caption", ""),
|
|
209
|
+
})
|
|
210
|
+
text = msg.get("document", {}).get("caption", "").strip()
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
"chat_id": chat_id,
|
|
214
|
+
"text": text,
|
|
215
|
+
"audio_id": audio_id,
|
|
216
|
+
"attachments": attachments,
|
|
217
|
+
"platform": "whatsapp_cloud",
|
|
218
|
+
}
|
|
219
|
+
except (IndexError, KeyError, TypeError):
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ── Webhook Parser: WhatsApp Bridge ───────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
def parse_whatsapp_bridge_message(body: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
226
|
+
"""Parse WhatsApp Bridge (Baileys) webhook body."""
|
|
227
|
+
try:
|
|
228
|
+
chat_id = body.get("key", {}).get("remoteJid", "")
|
|
229
|
+
if not chat_id:
|
|
230
|
+
return None
|
|
231
|
+
if chat_id.endswith("@g.us"):
|
|
232
|
+
return None # Skip group chats
|
|
233
|
+
|
|
234
|
+
attachments = []
|
|
235
|
+
text = body.get("message", {}).get("conversationMessage", {}).get("conversation", "").strip()
|
|
236
|
+
|
|
237
|
+
if body.get("isImage") and body.get("imageBase64"):
|
|
238
|
+
attachments.append({
|
|
239
|
+
"kind": "image",
|
|
240
|
+
"platform": "whatsapp",
|
|
241
|
+
"filename": f"wa_bridge_{body.get('messageId', 'image')}.jpg",
|
|
242
|
+
"mime_type": body.get("imageMime", "image/jpeg"),
|
|
243
|
+
"caption": text or "",
|
|
244
|
+
"base64": body.get("imageBase64", ""),
|
|
245
|
+
})
|
|
246
|
+
text = ""
|
|
247
|
+
|
|
248
|
+
if body.get("isDocument") and body.get("docBase64"):
|
|
249
|
+
attachments.append({
|
|
250
|
+
"kind": "document",
|
|
251
|
+
"platform": "whatsapp",
|
|
252
|
+
"filename": body.get("docName", f"wa_doc_{body.get('messageId', 'doc')}"),
|
|
253
|
+
"mime_type": body.get("docMime", "application/octet-stream"),
|
|
254
|
+
"caption": text or "",
|
|
255
|
+
"base64": body.get("docBase64", ""),
|
|
256
|
+
})
|
|
257
|
+
text = ""
|
|
258
|
+
|
|
259
|
+
audio_id = None
|
|
260
|
+
if body.get("isAudio") and body.get("audioBase64"):
|
|
261
|
+
b64_mime = body.get("audioMime", "audio/ogg")
|
|
262
|
+
audio_id = f"wa_b64:{b64_mime}:{body['audioBase64']}"
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
"chat_id": chat_id,
|
|
266
|
+
"text": text,
|
|
267
|
+
"audio_id": audio_id,
|
|
268
|
+
"attachments": attachments,
|
|
269
|
+
"platform": "whatsapp",
|
|
270
|
+
}
|
|
271
|
+
except Exception:
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ── Full Webhook Parser ─────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
def parse_webhook(body: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
278
|
+
"""Parse any webhook body into standard format."""
|
|
279
|
+
platform = detect_incoming_platform(body)
|
|
280
|
+
|
|
281
|
+
if platform == "telegram":
|
|
282
|
+
return parse_telegram_message(body)
|
|
283
|
+
elif platform == "whatsapp_cloud":
|
|
284
|
+
return parse_whatsapp_cloud_message(body)
|
|
285
|
+
elif platform == "whatsapp":
|
|
286
|
+
return parse_whatsapp_bridge_message(body)
|
|
287
|
+
elif platform == "evolution":
|
|
288
|
+
try:
|
|
289
|
+
data = body.get("data", {})
|
|
290
|
+
key = data.get("key", {})
|
|
291
|
+
if key.get("fromMe", False):
|
|
292
|
+
return None
|
|
293
|
+
chat_id = key.get("remoteJid", "")
|
|
294
|
+
msg_data = data.get("message", {})
|
|
295
|
+
conv = msg_data.get("conversationMessage", {}).get("conversation", "").strip()
|
|
296
|
+
ext = msg_data.get("extendedTextMessage", {})
|
|
297
|
+
text = conv or ext.get("text", "").strip() or ""
|
|
298
|
+
return {
|
|
299
|
+
"chat_id": chat_id,
|
|
300
|
+
"text": text,
|
|
301
|
+
"audio_id": None,
|
|
302
|
+
"attachments": [],
|
|
303
|
+
"platform": "evolution",
|
|
304
|
+
}
|
|
305
|
+
except Exception:
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
return None
|