@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.
Files changed (175) hide show
  1. package/.env.example +68 -0
  2. package/CHANGELOG.md +54 -0
  3. package/LICENSE +21 -0
  4. package/README.md +369 -0
  5. package/brand-assets/A_dark_luxury_web_background_202605210700.jpeg +0 -0
  6. package/brand-assets/Conny.web.logo.png +0 -0
  7. package/brand-assets/Logo_Conny_Petalo_Claro.png +0 -0
  8. package/brand-assets/cl-nica-de-las-am-ricas/manifest.json +22 -0
  9. package/brand-assets/cl-nica-de-las-am-ricas/processed/business-identity.txt +11 -0
  10. package/brand-assets/cl-nica-de-las-am-ricas/raw/business-identity.txt +11 -0
  11. package/brand-assets/cl-nica-las-am-ricas/manifest.json +22 -0
  12. package/brand-assets/cl-nica-las-am-ricas/processed/business-identity.txt +11 -0
  13. package/brand-assets/cl-nica-las-am-ricas/raw/business-identity.txt +11 -0
  14. package/brand-assets/conny-demo/manifest.json +22 -0
  15. package/brand-assets/conny-demo/processed/business-identity.txt +7 -0
  16. package/brand-assets/conny-demo/raw/business-identity.txt +7 -0
  17. package/brand-assets/conny-logo.png +0 -0
  18. package/brand-assets/web.background.png +0 -0
  19. package/brand_assets.py +323 -0
  20. package/conny +28 -0
  21. package/conny-chat.py +579 -0
  22. package/conny-omni.py +3843 -0
  23. package/conny.py +113 -0
  24. package/conny_agents/__init__.py +1 -0
  25. package/conny_agents/agenda.py +1 -0
  26. package/conny_agents/captacion.py +1 -0
  27. package/conny_agents/conocimiento.py +1 -0
  28. package/conny_agents/escalacion.py +1 -0
  29. package/conny_agents/objeciones.py +1 -0
  30. package/conny_agents/seguimiento.py +1 -0
  31. package/conny_app.py +287 -0
  32. package/conny_audio.py +350 -0
  33. package/conny_audio_learn.py +84 -0
  34. package/conny_brain_v10.py +804 -0
  35. package/conny_bridge.py +656 -0
  36. package/conny_calendar.py +169 -0
  37. package/conny_cli.py +11784 -0
  38. package/conny_cli_bb.py +437 -0
  39. package/conny_commands.py +243 -0
  40. package/conny_config.py +215 -0
  41. package/conny_core/__init__.py +3 -0
  42. package/conny_core/conversation_engine.py +446 -0
  43. package/conny_core/first_turn_ops.py +287 -0
  44. package/conny_core/persona_registry.py +157 -0
  45. package/conny_core/prompt_ops.py +561 -0
  46. package/conny_cron.py +72 -0
  47. package/conny_demo_v2.py +209 -0
  48. package/conny_demo_voice.py +134 -0
  49. package/conny_design.py +43 -0
  50. package/conny_doctor.py +319 -0
  51. package/conny_domino.py +696 -0
  52. package/conny_generator.py +447 -0
  53. package/conny_google_auth.py +159 -0
  54. package/conny_i18n.py +619 -0
  55. package/conny_init.py +509 -0
  56. package/conny_integrations/__init__.py +4 -0
  57. package/conny_integrations/llm.py +1 -0
  58. package/conny_integrations/vault.py +77 -0
  59. package/conny_integrations/whatsapp.py +1 -0
  60. package/conny_intelligence.py +65 -0
  61. package/conny_learning.py +154 -0
  62. package/conny_memory.py +243 -0
  63. package/conny_memory_engine.py +292 -0
  64. package/conny_nova_proxy.py +170 -0
  65. package/conny_nuke_robot_phrases.py +493 -0
  66. package/conny_pairing.py +253 -0
  67. package/conny_patch.py +291 -0
  68. package/conny_persona_cli.py +150 -0
  69. package/conny_router.py +308 -0
  70. package/conny_runtime_ops.py +271 -0
  71. package/conny_session.py +516 -0
  72. package/conny_skills/__init__.py +1 -0
  73. package/conny_skills/demo_mode.py +35 -0
  74. package/conny_skills/text_processing.py +1 -0
  75. package/conny_skills/tone_detection.py +1 -0
  76. package/conny_smart_features.py +333 -0
  77. package/conny_studio.py +161 -0
  78. package/conny_sync_fix.py +306 -0
  79. package/conny_tui.py +512 -0
  80. package/conny_tui_select.py +202 -0
  81. package/conny_ultra_config.py +411 -0
  82. package/conny_uncertainty.py +174 -0
  83. package/conny_utils.py +87 -0
  84. package/conny_voice.py +156 -0
  85. package/conny_voice_engine.py +124 -0
  86. package/conny_web_search.py +66 -0
  87. package/conny_weekly_report.py +85 -0
  88. package/conny_worm.py +88 -0
  89. package/core/__init__.py +25 -0
  90. package/ecosystem.config.js +24 -0
  91. package/fix_init.py +27 -0
  92. package/install.sh +78 -0
  93. package/knowledge_base.py +330 -0
  94. package/nova/rules/default.yaml +37 -0
  95. package/nova_bridge.py +509 -0
  96. package/npm/conny.js +471 -0
  97. package/package.json +102 -0
  98. package/personas/conny/base/default.yaml +35 -0
  99. package/personas/conny/base/estetica_whatsapp.yaml +36 -0
  100. package/requirements.txt +14 -0
  101. package/run.sh +47 -0
  102. package/search.py +465 -0
  103. package/smart_handoff.py +1150 -0
  104. package/src/__init__.py +0 -0
  105. package/src/conny/__init__.py +0 -0
  106. package/src/conny/admin/__init__.py +0 -0
  107. package/src/conny/admin/api.py +234 -0
  108. package/src/conny/admin/dashboard.py +772 -0
  109. package/src/conny/api/__init__.py +0 -0
  110. package/src/conny/api/routes.py +8851 -0
  111. package/src/conny/brain/__init__.py +15 -0
  112. package/src/conny/brain/engine.py +804 -0
  113. package/src/conny/brain/learning.py +154 -0
  114. package/src/conny/brain/memory.py +324 -0
  115. package/src/conny/brain/smart_features.py +333 -0
  116. package/src/conny/brain/uncertainty.py +167 -0
  117. package/src/conny/channels/__init__.py +0 -0
  118. package/src/conny/channels/audio.py +316 -0
  119. package/src/conny/channels/cli.py +11795 -0
  120. package/src/conny/channels/logo_art.py +11 -0
  121. package/src/conny/channels/voice.py +156 -0
  122. package/src/conny/core/__init__.py +0 -0
  123. package/src/conny/core/config.py +215 -0
  124. package/src/conny/core/cron.py +72 -0
  125. package/src/conny/core/messenger.py +563 -0
  126. package/src/conny/core/router.py +297 -0
  127. package/src/conny/core/session.py +312 -0
  128. package/src/conny/demo/__init__.py +0 -0
  129. package/src/conny/demo/handler.py +3110 -0
  130. package/src/conny/integrations/__init__.py +19 -0
  131. package/src/conny/integrations/calendar.py +169 -0
  132. package/src/conny/integrations/knowledge.py +312 -0
  133. package/src/conny/integrations/search.py +66 -0
  134. package/src/conny/personas/__init__.py +0 -0
  135. package/src/conny/personas/generator.py +447 -0
  136. package/src/conny/production/__init__.py +0 -0
  137. package/src/conny/production/domino.py +696 -0
  138. package/src/conny/production/guard.py +550 -0
  139. package/src/conny/production/handoff.py +1150 -0
  140. package/src/conny/production/monitor.py +353 -0
  141. package/src/conny/utils/__init__.py +2 -0
  142. package/src/conny/utils/helpers.py +75 -0
  143. package/src/conny/utils/i18n.py +619 -0
  144. package/src/core/admin_engines.py +772 -0
  145. package/src/core/globals.py +11845 -0
  146. package/src/core/orchestrator.py +273 -0
  147. package/src/core/production_monitor.py +353 -0
  148. package/src/core/runtime.py +5487 -0
  149. package/src/domain/onboarding_flow.py +230 -0
  150. package/src/domain/prompts/__init__.py +1 -0
  151. package/src/domain/prompts/prospect_pitch.py +282 -0
  152. package/src/domain/send_guard.py +636 -0
  153. package/src/domain/swarm/queen.py +96 -0
  154. package/src/infrastructure/llm_providers/engine.py +487 -0
  155. package/src/interfaces/mcp_server.py +73 -0
  156. package/src/interfaces/nova_bridge.py +58 -0
  157. package/src/interfaces/web/admin_api.py +1379 -0
  158. package/src/interfaces/web/app.py +9408 -0
  159. package/src/interfaces/web/demo_handler.py +3450 -0
  160. package/src/interfaces/web/static/generate_avatars.py +46 -0
  161. package/v7/__init__.py +46 -0
  162. package/v7/agents/__init__.py +46 -0
  163. package/v7/agents/agenda.py +77 -0
  164. package/v7/agents/base.py +216 -0
  165. package/v7/agents/captacion.py +60 -0
  166. package/v7/agents/conocimiento.py +69 -0
  167. package/v7/agents/escalacion.py +83 -0
  168. package/v7/agents/objeciones.py +109 -0
  169. package/v7/agents/seguimiento.py +71 -0
  170. package/v7/memory/__init__.py +46 -0
  171. package/v7/memory/patient_profile.py +200 -0
  172. package/v7/orchestrator.py +275 -0
  173. package/v7/postprocess.py +127 -0
  174. package/v7/router.py +239 -0
  175. package/verify_conversation_impl.py +48 -0
@@ -0,0 +1,253 @@
1
+ import os
2
+ import json
3
+ import time
4
+ import secrets
5
+ import re
6
+ from typing import List, Dict, Optional, Tuple, Any
7
+
8
+ # Configuration constants
9
+ PAIRING_CODE_LENGTH = 8
10
+ PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
11
+ PAIRING_PENDING_TTL = 3600 # 1 hour in seconds
12
+ PAIRING_PENDING_MAX = 3
13
+
14
+ # Base directory for storing pairing data
15
+ # Ensures it respects user's home directory and creates the structure if it doesn't exist
16
+ CONNY_PAIRING_BASE_DIR = os.path.join(os.path.expanduser("~"), ".conny", "pairing")
17
+
18
+ # Ensure the base directory exists
19
+ os.makedirs(CONNY_PAIRING_BASE_DIR, exist_ok=True)
20
+
21
+ def _now_iso() -> str:
22
+ """Returns the current time in ISO format."""
23
+ return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
24
+
25
+ def _load_json(path: str, fallback: Any = None) -> Any:
26
+ """Loads JSON data from a file, returns fallback if file not found or invalid."""
27
+ try:
28
+ if os.path.exists(path):
29
+ with open(path, 'r', encoding='utf-8') as f:
30
+ data = json.load(f)
31
+ # Basic validation for structure if fallback is provided
32
+ if fallback is not None and isinstance(data, dict) and "version" not in data:
33
+ return fallback
34
+ return data
35
+ else:
36
+ return fallback
37
+ except (json.JSONDecodeError, OSError):
38
+ return fallback
39
+
40
+ def _save_json(path: str, data: Any) -> None:
41
+ """Saves data to a JSON file."""
42
+ try:
43
+ with open(path, 'w', encoding='utf-8') as f:
44
+ json.dump(data, f, indent=2)
45
+ except OSError as e:
46
+ print(f"Error saving JSON to {path}: {e}") # Basic logging
47
+
48
+ def _generate_code() -> str:
49
+ """Generates a random pairing code."""
50
+ return ''.join(secrets.choice(PAIRING_CODE_ALPHABET) for _ in range(PAIRING_CODE_LENGTH))
51
+
52
+ def _get_channel_paths(channel: str) -> Tuple[str, str]:
53
+ """Returns the file paths for pairing requests and allow list for a given channel."""
54
+ channel_safe = re.sub(r'[\\/:*?"<>|]', '_', channel.lower()) # Sanitize channel name for filename
55
+ pairing_path = os.path.join(CONNY_PAIRING_BASE_DIR, f"{channel_safe}-pairing.json")
56
+ allow_path = os.path.join(CONNY_PAIRING_BASE_DIR, f"{channel_safe}-allowFrom.json")
57
+ return pairing_path, allow_path
58
+
59
+ def load_pending(channel: str) -> List[Dict]:
60
+ """Loads pending pairing requests for a channel."""
61
+ path, _ = _get_channel_paths(channel)
62
+ fallback = {"version": 1, "requests": []}
63
+ data = _load_json(path, fallback)
64
+ # Ensure requests are loaded and pruned for expiry
65
+ now = time.time()
66
+ loaded_requests = data.get("requests", [])
67
+ active_requests = [req for req in loaded_requests if now - int(req.get("timestamp", 0)) < PAIRING_PENDING_TTL]
68
+ if len(active_requests) != len(loaded_requests):
69
+ _save_json(path, {"version": 1, "requests": active_requests}) # Save pruned list
70
+ return active_requests
71
+
72
+ def save_pending(channel: str, requests: List[Dict]) -> None:
73
+ """Saves pending pairing requests for a channel."""
74
+ path, _ = _get_channel_paths(channel)
75
+ _save_json(path, {"version": 1, "requests": requests})
76
+
77
+ def load_allow(channel: str) -> List[str]:
78
+ """Loads the allow list for a channel."""
79
+ _, path = _get_channel_paths(channel)
80
+ fallback = {"version": 1, "allowed_chat_ids": []}
81
+ data = _load_json(path, fallback)
82
+ return data.get("allowed_chat_ids", [])
83
+
84
+ def save_allow(channel: str, allowed_chat_ids: List[str]) -> None:
85
+ """Saves the allow list for a channel."""
86
+ _, path = _get_channel_paths(channel)
87
+ _save_json(path, {"version": 1, "allowed_chat_ids": allowed_chat_ids})
88
+
89
+ def generate_code_for_chat(chat_id: str, channel: str) -> str:
90
+ """
91
+ Generates a pairing code for a chat_id if not already pending or allowed.
92
+ Returns the code and updates the pending requests.
93
+ """
94
+ pending_requests = load_pending(channel)
95
+ allowed_ids = load_allow(channel)
96
+
97
+ # Check if already allowed
98
+ if chat_id in allowed_ids:
99
+ return "already_paired" # Special indicator
100
+
101
+ # Check if already has a pending request
102
+ for req in pending_requests:
103
+ if req.get("chat_id") == chat_id:
104
+ # Return existing code if not expired
105
+ if time.time() - req.get("timestamp", 0) < PAIRING_PENDING_TTL:
106
+ return req.get("code")
107
+ else:
108
+ break # Expired, will be removed
109
+
110
+ # Prune expired requests before adding a new one
111
+ now = time.time()
112
+ active_pending = [req for req in pending_requests if now - req.get("timestamp", 0) < PAIRING_PENDING_TTL]
113
+
114
+ # Check if limit is reached
115
+ if len(active_pending) >= PAIRING_PENDING_MAX:
116
+ # Remove oldest to make space, if allowed by logic (or return error/indicator)
117
+ # Simple approach: return error/indicator that limit reached
118
+ return "limit_reached"
119
+
120
+ code = _generate_code()
121
+ new_request = {
122
+ "chat_id": chat_id,
123
+ "code": code,
124
+ "timestamp": int(now),
125
+ "channel": channel,
126
+ }
127
+ active_pending.append(new_request)
128
+ save_pending(channel, active_pending)
129
+ return code
130
+
131
+ def list_pending(channel: str) -> List[Dict]:
132
+ """Returns all active pending pairing requests for a channel."""
133
+ return load_pending(channel)
134
+
135
+ def approve_code(code: str, channel: str, chat_id: Optional[str] = None) -> bool:
136
+ """
137
+ Approves a pairing code. Adds chat_id to allow list and removes the request.
138
+ Returns True if successful, False otherwise.
139
+ """
140
+ pending_requests = load_pending(channel)
141
+ allowed_ids = load_allow(channel)
142
+
143
+ original_request = None
144
+ new_pending_requests = []
145
+ for req in pending_requests:
146
+ if req.get("code") == code:
147
+ original_request = req
148
+ # Don't add this request to the new list (effectively removing it)
149
+ else:
150
+ new_pending_requests.append(req)
151
+
152
+ if original_request:
153
+ # Add the chat_id to the allow list if provided and not already there
154
+ request_chat_id = original_request.get("chat_id")
155
+ target_chat_id = chat_id or request_chat_id # Use provided chat_id if available, else from request
156
+
157
+ if target_chat_id and target_chat_id not in allowed_ids:
158
+ allowed_ids.append(target_chat_id)
159
+ save_allow(channel, allowed_ids)
160
+
161
+ save_pending(channel, new_pending_requests)
162
+ return True
163
+ return False
164
+
165
+ def is_paired(chat_id: str, channel: str) -> bool:
166
+ """Checks if a chat_id is in the allow list for a channel."""
167
+ allowed_ids = load_allow(channel)
168
+ return chat_id in allowed_ids
169
+
170
+ def ensure_pairing_for_chat(chat_id: str, channel: str) -> str:
171
+ """
172
+ Ensures a chat_id is either paired or has an active pending request.
173
+ Returns the pairing code if a new one was generated, 'already_paired' if already allowed,
174
+ or 'limit_reached' if max pending requests are active.
175
+ """
176
+ if is_paired(chat_id, channel):
177
+ return "already_paired"
178
+ else:
179
+ return generate_code_for_chat(chat_id, channel)
180
+
181
+ # Example usage (for testing, not part of the main module export)
182
+ if __name__ == "__main__":
183
+ # Example for Telegram channel
184
+ TEST_CHANNEL = "telegram"
185
+ TEST_CHAT_ID_1 = "123456789"
186
+ TEST_CHAT_ID_2 = "987654321"
187
+ TEST_CHAT_ID_3 = "111111111"
188
+ TEST_CHAT_ID_4 = "222222222"
189
+
190
+ print(f"--- Testing Pairing Module for Channel: {TEST_CHANNEL} ---")
191
+
192
+ # Generate code for chat 1
193
+ code1 = ensure_pairing_for_chat(TEST_CHAT_ID_1, TEST_CHANNEL)
194
+ print(f"Ensure pairing for {TEST_CHAT_ID_1}: {code1}")
195
+
196
+ # Generate code for chat 2
197
+ code2 = ensure_pairing_for_chat(TEST_CHAT_ID_2, TEST_CHANNEL)
198
+ print(f"Ensure pairing for {TEST_CHAT_ID_2}: {code2}")
199
+
200
+ # Generate code for chat 3
201
+ code3 = ensure_pairing_for_chat(TEST_CHAT_ID_3, TEST_CHANNEL)
202
+ print(f"Ensure pairing for {TEST_CHAT_ID_3}: {code3}")
203
+
204
+ # Try to generate code for chat 4 (should hit limit if MAX is 3)
205
+ code4_limit = ensure_pairing_for_chat(TEST_CHAT_ID_4, TEST_CHANNEL)
206
+ print(f"Ensure pairing for {TEST_CHAT_ID_4} (expect limit): {code4_limit}")
207
+
208
+ # List pending
209
+ pending = list_pending(TEST_CHANNEL)
210
+ print(f"Pending requests: {pending}")
211
+
212
+ # Approve code for chat 1
213
+ print(f"\nApproving code {code1} for {TEST_CHAT_ID_1}...")
214
+ success_approve1 = approve_code(code1, TEST_CHANNEL, TEST_CHAT_ID_1)
215
+ print(f"Approval successful: {success_approve1}")
216
+
217
+ # Check if chat 1 is now paired
218
+ print(f"Is {TEST_CHAT_ID_1} paired? {is_paired(TEST_CHAT_ID_1, TEST_CHANNEL)}")
219
+ print(f"Is {TEST_CHAT_ID_2} paired? {is_paired(TEST_CHAT_ID_2, TEST_CHANNEL)}")
220
+
221
+ # List pending again (should be less)
222
+ pending_after_approve = list_pending(TEST_CHANNEL)
223
+ print(f"Pending requests after approve: {pending_after_approve}")
224
+
225
+ # Try to approve a non-existent or already approved code
226
+ print("\nApproving non-existent code 'ABCDEFGH'...")
227
+ fail_approve = approve_code("ABCDEFGH", TEST_CHANNEL)
228
+ print(f"Approval successful: {fail_approve}")
229
+
230
+ print(f"\nApproving {code2} again for {TEST_CHAT_ID_2}...")
231
+ success_approve2 = approve_code(code2, TEST_CHANNEL, TEST_CHAT_ID_2)
232
+ print(f"Approval successful: {success_approve2}")
233
+
234
+ # Test ensure_pairing for already paired chat
235
+ print(f"\nEnsure pairing for {TEST_CHAT_ID_1} (already paired):")
236
+ code_already = ensure_pairing_for_chat(TEST_CHAT_ID_1, TEST_CHANNEL)
237
+ print(f"Result: {code_already}")
238
+
239
+ print("\n--- Test Cleanup ---")
240
+ # Clean up created files
241
+ try:
242
+ os.remove(os.path.join(CONNY_PAIRING_BASE_DIR, f"{TEST_CHANNEL}-pairing.json"))
243
+ os.remove(os.path.join(CONNY_PAIRING_BASE_DIR, f"{TEST_CHANNEL}-allowFrom.json"))
244
+ print("Cleanup successful.")
245
+ except OSError as e:
246
+ print(f"Cleanup failed: {e}")
247
+ finally:
248
+ # Remove base dir if empty
249
+ try:
250
+ if not os.listdir(CONNY_PAIRING_BASE_DIR):
251
+ os.rmdir(CONNY_PAIRING_BASE_DIR)
252
+ except OSError:
253
+ pass
package/conny_patch.py ADDED
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ conny_patch.py — Aplica todos los fixes de producción a conny.py
4
+
5
+ Fixes:
6
+ 1. Presencia online al recibir mensaje (soluciona checkmark gris)
7
+ 2. Typing más humano con presencia antes de cada burbuja
8
+ 3. Comando /token para generar códigos de activación desde el chat
9
+ 4. Comando /activar para activar instancia sin API externa
10
+ 5. Help actualizado con nuevos comandos
11
+
12
+ Uso: python3 conny_patch.py /home/ubuntu/conny/conny.py
13
+ """
14
+
15
+ import sys
16
+ import shutil
17
+ from pathlib import Path
18
+ from datetime import datetime
19
+
20
+ if len(sys.argv) < 2:
21
+ print("Uso: python3 conny_patch.py /ruta/a/conny.py")
22
+ sys.exit(1)
23
+
24
+ target = Path(sys.argv[1])
25
+ if not target.exists():
26
+ print(f"Error: {target} no existe")
27
+ sys.exit(1)
28
+
29
+ # Backup
30
+ backup = target.with_suffix(f".py.bak_{datetime.now().strftime('%Y%m%d_%H%M%S')}")
31
+ shutil.copy2(target, backup)
32
+ print(f"Backup: {backup}")
33
+
34
+ src = target.read_text(encoding="utf-8")
35
+ original_len = len(src)
36
+
37
+ # ══════════════════════════════════════════════════════════════════════════════
38
+ # PATCH 1 — _typing_action: agregar presencia available antes del typing
39
+ # Soluciona el checkmark gris — WA necesita saber que estamos online
40
+ # ══════════════════════════════════════════════════════════════════════════════
41
+
42
+ OLD_TYPING = ''' elif platform == "whatsapp":
43
+ if Config.WHATSAPP_BRIDGE_URL:
44
+ async with httpx.AsyncClient(timeout=8.0) as client:
45
+ # Solo typing — el /read se dispara por separado con delay natural
46
+ await client.post(
47
+ f"{Config.WHATSAPP_BRIDGE_URL}/typing",
48
+ json={"to": chat_id, "duration": duration}
49
+ )'''
50
+
51
+ NEW_TYPING = ''' elif platform == "whatsapp":
52
+ if Config.WHATSAPP_BRIDGE_URL:
53
+ async with httpx.AsyncClient(timeout=8.0) as client:
54
+ # 1. Aparecer online ANTES de typing — soluciona checkmark gris
55
+ # WhatsApp solo entrega mensajes cuando el remitente está online
56
+ try:
57
+ await client.post(
58
+ f"{Config.WHATSAPP_BRIDGE_URL}/presence",
59
+ json={"status": "available", "timeout": 120000},
60
+ timeout=3.0
61
+ )
62
+ except Exception:
63
+ pass
64
+ # 2. Typing proporcional al mensaje
65
+ await client.post(
66
+ f"{Config.WHATSAPP_BRIDGE_URL}/typing",
67
+ json={"to": chat_id, "duration": duration}
68
+ )'''
69
+
70
+ if OLD_TYPING in src:
71
+ src = src.replace(OLD_TYPING, NEW_TYPING)
72
+ print("✓ PATCH 1 aplicado: presencia online antes de typing")
73
+ else:
74
+ print("⚠ PATCH 1 no encontrado — puede que ya esté aplicado")
75
+
76
+ # ══════════════════════════════════════════════════════════════════════════════
77
+ # PATCH 2 — webhook WhatsApp: marcar presencia available al recibir mensaje
78
+ # Esto hace que Conny aparezca online inmediatamente cuando llega un mensaje
79
+ # ══════════════════════════════════════════════════════════════════════════════
80
+
81
+ OLD_WEBHOOK_WA = ''' if not chat_id:
82
+ return {"ok": True}
83
+
84
+ if audio_id:
85
+ await conny._typing_action(chat_id)'''
86
+
87
+ NEW_WEBHOOK_WA = ''' if not chat_id:
88
+ return {"ok": True}
89
+
90
+ # Aparecer online al recibir — soluciona checkmark gris y "último vez visto"
91
+ if platform == "whatsapp" and Config.WHATSAPP_BRIDGE_URL:
92
+ async def _go_online():
93
+ try:
94
+ async with httpx.AsyncClient(timeout=3.0) as _hx:
95
+ await _hx.post(
96
+ f"{Config.WHATSAPP_BRIDGE_URL}/presence",
97
+ json={"status": "available", "timeout": 180000}
98
+ )
99
+ except Exception:
100
+ pass
101
+ asyncio.create_task(_go_online())
102
+
103
+ if audio_id:
104
+ await conny._typing_action(chat_id)'''
105
+
106
+ if OLD_WEBHOOK_WA in src:
107
+ src = src.replace(OLD_WEBHOOK_WA, NEW_WEBHOOK_WA)
108
+ print("✓ PATCH 2 aplicado: presencia online al recibir mensaje")
109
+ else:
110
+ print("⚠ PATCH 2 no encontrado")
111
+
112
+ # ══════════════════════════════════════════════════════════════════════════════
113
+ # PATCH 3 — Comandos /token y /activar en el handler admin
114
+ # /token [nombre] → genera un código de activación
115
+ # /activar → activa esta instancia si ya tiene datos
116
+ # ══════════════════════════════════════════════════════════════════════════════
117
+
118
+ OLD_REGLAS_CMD = ''' elif cmd == "/reglas":
119
+ return await self._admin_show_trust_rules()'''
120
+
121
+ NEW_REGLAS_CMD = ''' elif cmd == "/reglas":
122
+ return await self._admin_show_trust_rules()
123
+
124
+ # ── Tokens de activacion ───────────────────────────────────────
125
+ elif cmd == "/token" or cmd.startswith("/token "):
126
+ # /token → genera token para esta instancia
127
+ # /token Clinica Demo → genera token con ese nombre
128
+ label = cmd.split("/token", 1)[1].strip() if " " in cmd else ""
129
+ if not label:
130
+ clinic_name = clinic.get("name") or "Mi Negocio"
131
+ label = clinic_name
132
+ new_token = generate_activation_token(label)
133
+ expires_at = (datetime.now() + timedelta(hours=Config.TOKEN_EXPIRY_HOURS)).isoformat()
134
+ saved = db.create_activation_token(new_token, label, expires_at)
135
+ if saved:
136
+ return [
137
+ f"Token de activacion generado para: {label}",
138
+ f"{new_token}",
139
+ f"Expira en {Config.TOKEN_EXPIRY_HOURS}h. Enviaselo al administrador del negocio."
140
+ ]
141
+ return ["No pude generar el token. Intenta de nuevo."]
142
+
143
+ elif cmd == "/activar":
144
+ # Activa esta instancia directamente (para instancias ya configuradas)
145
+ clinic_name = clinic.get("name") or ""
146
+ if not clinic_name:
147
+ db.update_clinic(setup_step="idle")
148
+ return [
149
+ "Para activar, primero dime el nombre de tu negocio.",
150
+ "Escribe el nombre y te configuro de una."
151
+ ]
152
+ db.update_clinic(setup_done=1)
153
+ admin_ids = clinic.get("admin_chat_ids", [])
154
+ if isinstance(admin_ids, str):
155
+ try: admin_ids = json.loads(admin_ids) if admin_ids else []
156
+ except: admin_ids = []
157
+ if chat_id not in admin_ids:
158
+ admin_ids.append(chat_id)
159
+ db.update_clinic(admin_chat_ids=json.dumps(admin_ids))
160
+ return [
161
+ f"Instancia activada.",
162
+ f"Negocio: {clinic_name}",
163
+ f"Ya puedo atender pacientes. Escribe /config para ver todo."
164
+ ]'''
165
+
166
+ if OLD_REGLAS_CMD in src:
167
+ src = src.replace(OLD_REGLAS_CMD, NEW_REGLAS_CMD)
168
+ print("✓ PATCH 3 aplicado: comandos /token y /activar")
169
+ else:
170
+ print("⚠ PATCH 3 no encontrado")
171
+
172
+ # ══════════════════════════════════════════════════════════════════════════════
173
+ # PATCH 4 — Help actualizado con nuevos comandos
174
+ # ══════════════════════════════════════════════════════════════════════════════
175
+
176
+ OLD_HELP = ''' elif cmd.startswith("/"):
177
+ return [
178
+ "Comandos disponibles:\\n\\n"
179
+ "/citas — citas pendientes\\n"
180
+ "/chats — ver conversaciones de pacientes\\n"
181
+ "/chat [id] — conversación completa\\n"
182
+ "/reglas — lo aprendido del feedback\\n"
183
+ "/sector — ver/cambiar sector del negocio\\n"
184
+ "/nova — motor de gobernanza\\n"
185
+ "/whatsapp — conectar WhatsApp Business\\n"
186
+ "/agenda — estado del calendario\\n"
187
+ "/personalidad — ajustar personalidad\\n"
188
+ "/config — ver configuración\\n"
189
+ "/metricas — métricas\\n\\n"
190
+ "V6.0 — Inteligencia:\\n"
191
+ "/pipeline — leads por temperatura (frío/tibio/caliente)\\n"
192
+ "/perdidos — por qué se van los clientes\\n"
193
+ "/coach — feedback de ventas de la semana\\n"
194
+ "/estilo — clonar tu forma de escribir\\n\\n"
195
+ "V6.0 — Automatización:\\n"
196
+ "/reactivar — reactivar clientes 60+ días inactivos\\n"
197
+ "/seguimiento — estado de follow-ups automáticos\\n"
198
+ "/reporte — reporte ahora mismo\\n"
199
+ "/broadcast [msg] — mensaje masivo a pacientes\\n\\n"
200
+ "V6.0 — Canales y pagos:\\n"
201
+ "/instagram — conectar DMs de Instagram\\n"
202
+ "/pagos — pagos desde el chat (Wompi/MercadoPago)\\n"
203
+ "/preconsulta — formulario pre-cita automático\\n\\n"
204
+ "O escríbeme en lenguaje natural."
205
+ ]'''
206
+
207
+ NEW_HELP = ''' elif cmd.startswith("/"):
208
+ return [
209
+ "Comandos disponibles:\\n\\n"
210
+ "Atencion:\\n"
211
+ "/citas — citas pendientes\\n"
212
+ "/chats — ver conversaciones de pacientes\\n"
213
+ "/chat [id] — conversacion completa\\n"
214
+ "/reglas — reglas aprendidas\\n"
215
+ "/broadcast [msg] — mensaje masivo\\n\\n"
216
+ "Configuracion:\\n"
217
+ "/config — ver configuracion\\n"
218
+ "/sector — ver/cambiar sector\\n"
219
+ "/personalidad — ajustar personalidad\\n"
220
+ "/whatsapp — conectar WhatsApp\\n"
221
+ "/agenda — estado del calendario\\n"
222
+ "/nova — motor de gobernanza\\n"
223
+ "/metricas — metricas\\n\\n"
224
+ "Activacion:\\n"
225
+ "/token [nombre] — generar codigo de activacion\\n"
226
+ "/activar — activar esta instancia\\n\\n"
227
+ "Inteligencia (V7):\\n"
228
+ "/pipeline — leads por temperatura\\n"
229
+ "/perdidos — analisis de perdidos\\n"
230
+ "/coach — feedback de ventas\\n"
231
+ "/reactivar — reactivar clientes inactivos\\n"
232
+ "/reporte — reporte inmediato\\n\\n"
233
+ "O escribe en lenguaje natural."
234
+ ]'''
235
+
236
+ if OLD_HELP in src:
237
+ src = src.replace(OLD_HELP, NEW_HELP)
238
+ print("✓ PATCH 4 aplicado: help actualizado")
239
+ else:
240
+ print("⚠ PATCH 4 no encontrado — ayuda sin cambios")
241
+
242
+ # ══════════════════════════════════════════════════════════════════════════════
243
+ # PATCH 5 — _send_bubbles: marcar offline después de enviar todas las burbujas
244
+ # Comportamiento humano: aparece online → escribe → desaparece
245
+ # ══════════════════════════════════════════════════════════════════════════════
246
+
247
+ OLD_SEND_BUBBLES_END = ''' if i < len(bubbles) - 1:
248
+ # Pausa inter-burbuja más humana — evita que WA trate las ráfagas como spam
249
+ inter_pause = random.uniform(1.4, 2.8) if is_wa else random.uniform(0.8, 1.8)
250
+ await asyncio.sleep(inter_pause)'''
251
+
252
+ NEW_SEND_BUBBLES_END = ''' if i < len(bubbles) - 1:
253
+ # Pausa inter-burbuja más humana — evita que WA trate las ráfagas como spam
254
+ inter_pause = random.uniform(1.4, 2.8) if is_wa else random.uniform(0.8, 1.8)
255
+ await asyncio.sleep(inter_pause)
256
+
257
+ # Después de enviar todo: volver a unavailable con delay natural
258
+ # Simula comportamiento humano: escribe, manda, se desconecta
259
+ if is_wa and Config.WHATSAPP_BRIDGE_URL:
260
+ async def _go_offline_delayed():
261
+ await asyncio.sleep(random.uniform(8.0, 18.0))
262
+ try:
263
+ async with httpx.AsyncClient(timeout=3.0) as _hx:
264
+ await _hx.post(
265
+ f"{Config.WHATSAPP_BRIDGE_URL}/presence",
266
+ json={"status": "unavailable"}
267
+ )
268
+ except Exception:
269
+ pass
270
+ asyncio.create_task(_go_offline_delayed())'''
271
+
272
+ if OLD_SEND_BUBBLES_END in src:
273
+ src = src.replace(OLD_SEND_BUBBLES_END, NEW_SEND_BUBBLES_END)
274
+ print("✓ PATCH 5 aplicado: offline después de enviar (comportamiento humano)")
275
+ else:
276
+ print("⚠ PATCH 5 no encontrado")
277
+
278
+ # ══════════════════════════════════════════════════════════════════════════════
279
+ # Escribir resultado
280
+ # ══════════════════════════════════════════════════════════════════════════════
281
+
282
+ target.write_text(src, encoding="utf-8")
283
+ new_len = len(src)
284
+
285
+ print(f"\n{'='*60}")
286
+ print(f"Archivo actualizado: {target}")
287
+ print(f"Tamaño: {original_len:,} → {new_len:,} chars (+{new_len - original_len:,})")
288
+ print(f"Backup guardado en: {backup}")
289
+ print(f"\nSiguiente paso:")
290
+ print(f" pm2 restart conny --update-env")
291
+ print(f" pm2 logs conny --lines 20 --nostream")