@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
package/conny_pairing.py
ADDED
|
@@ -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")
|