@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,323 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import io
5
+ import json
6
+ import mimetypes
7
+ import re
8
+ import zipfile
9
+ from dataclasses import dataclass
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional
13
+ from xml.etree import ElementTree as ET
14
+
15
+ try:
16
+ from pypdf import PdfReader # type: ignore
17
+ except Exception:
18
+ PdfReader = None
19
+
20
+
21
+ TEXT_EXTENSIONS = {
22
+ ".txt",
23
+ ".md",
24
+ ".markdown",
25
+ ".json",
26
+ ".csv",
27
+ ".tsv",
28
+ ".yaml",
29
+ ".yml",
30
+ ".html",
31
+ ".htm",
32
+ ".xml",
33
+ ".css",
34
+ ".js",
35
+ ".ts",
36
+ ".jsx",
37
+ ".tsx",
38
+ ".svg",
39
+ }
40
+
41
+ RICH_TEXT_EXTENSIONS = {
42
+ ".pdf",
43
+ ".docx",
44
+ }
45
+
46
+ TEXT_MIME_PREFIXES = (
47
+ "text/",
48
+ "application/json",
49
+ "application/xml",
50
+ "image/svg+xml",
51
+ )
52
+
53
+
54
+ def _slugify(value: str) -> str:
55
+ value = (value or "").strip().lower()
56
+ value = re.sub(r"[^a-z0-9]+", "-", value)
57
+ value = re.sub(r"-{2,}", "-", value).strip("-")
58
+ return value or "default"
59
+
60
+
61
+ def _now_iso() -> str:
62
+ return datetime.now(timezone.utc).isoformat()
63
+
64
+
65
+ def _safe_name(filename: str) -> str:
66
+ filename = (filename or "").strip().replace("\\", "/").split("/")[-1]
67
+ filename = re.sub(r"[^A-Za-z0-9._-]+", "_", filename)
68
+ return filename or "asset.bin"
69
+
70
+
71
+ def _looks_textual(filename: str, mime_type: str) -> bool:
72
+ filename = filename or ""
73
+ mime_type = (mime_type or "").lower().strip()
74
+ if Path(filename).suffix.lower() in TEXT_EXTENSIONS | RICH_TEXT_EXTENSIONS:
75
+ return True
76
+ return any(mime_type.startswith(prefix) for prefix in TEXT_MIME_PREFIXES)
77
+
78
+
79
+ def _decode_text(data: bytes) -> str:
80
+ for encoding in ("utf-8", "utf-8-sig", "latin-1"):
81
+ try:
82
+ return data.decode(encoding)
83
+ except Exception:
84
+ continue
85
+ return ""
86
+
87
+
88
+ def _extract_pdf_text(data: bytes) -> str:
89
+ if not data:
90
+ return ""
91
+ if PdfReader is None:
92
+ return ""
93
+ try:
94
+ reader = PdfReader(io.BytesIO(data))
95
+ chunks: List[str] = []
96
+ for page in reader.pages:
97
+ try:
98
+ text = page.extract_text() or ""
99
+ except Exception:
100
+ text = ""
101
+ if text.strip():
102
+ chunks.append(text.strip())
103
+ return "\n\n".join(chunks).strip()
104
+ except Exception:
105
+ return ""
106
+
107
+
108
+ def _extract_docx_text(data: bytes) -> str:
109
+ if not data:
110
+ return ""
111
+ try:
112
+ with zipfile.ZipFile(io.BytesIO(data)) as zf:
113
+ names = [name for name in zf.namelist() if name.startswith("word/") and name.endswith(".xml")]
114
+ text_blocks: List[str] = []
115
+ namespace = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"}
116
+ for name in names:
117
+ if not any(key in name for key in ("document.xml", "header", "footer")):
118
+ continue
119
+ raw = zf.read(name)
120
+ root = ET.fromstring(raw)
121
+ for paragraph in root.findall(".//w:p", namespace):
122
+ parts = [node.text or "" for node in paragraph.findall(".//w:t", namespace)]
123
+ paragraph_text = "".join(parts).strip()
124
+ if paragraph_text:
125
+ text_blocks.append(paragraph_text)
126
+ return "\n".join(text_blocks).strip()
127
+ except Exception:
128
+ return ""
129
+
130
+
131
+ def _extract_rich_text(filename: str, mime_type: str, data: bytes) -> str:
132
+ suffix = Path(filename or "").suffix.lower()
133
+ mime = (mime_type or "").lower().strip()
134
+ if suffix == ".pdf" or mime == "application/pdf":
135
+ return _extract_pdf_text(data)
136
+ if suffix == ".docx" or mime == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
137
+ return _extract_docx_text(data)
138
+ if suffix in TEXT_EXTENSIONS or any(mime.startswith(prefix) for prefix in TEXT_MIME_PREFIXES):
139
+ return _decode_text(data)
140
+ return ""
141
+
142
+
143
+ @dataclass
144
+ class SavedBrandAsset:
145
+ manifest_entry: Dict[str, Any]
146
+ extracted_text: str = ""
147
+ saved_path: str = ""
148
+
149
+
150
+ class BrandAssetStore:
151
+ """
152
+ Vault persistente por instancia.
153
+ Conserva archivos originales, texto extraído y un manifest versionado.
154
+ """
155
+
156
+ def __init__(self, base_dir: str, instance_name: str):
157
+ self.instance_slug = _slugify(instance_name)
158
+ self.root = Path(base_dir).expanduser() / self.instance_slug
159
+ self.raw_dir = self.root / "raw"
160
+ self.extracted_dir = self.root / "processed"
161
+ self.manifest_path = self.root / "manifest.json"
162
+ self.raw_dir.mkdir(parents=True, exist_ok=True)
163
+ self.extracted_dir.mkdir(parents=True, exist_ok=True)
164
+ self._ensure_manifest()
165
+
166
+ def _ensure_manifest(self) -> None:
167
+ if self.manifest_path.exists():
168
+ return
169
+ self._write_manifest(
170
+ {
171
+ "version": 1,
172
+ "instance_slug": self.instance_slug,
173
+ "created_at": _now_iso(),
174
+ "updated_at": _now_iso(),
175
+ "assets": [],
176
+ }
177
+ )
178
+
179
+ def _read_manifest(self) -> Dict[str, Any]:
180
+ try:
181
+ return json.loads(self.manifest_path.read_text(encoding="utf-8"))
182
+ except Exception:
183
+ return {
184
+ "version": 1,
185
+ "instance_slug": self.instance_slug,
186
+ "created_at": _now_iso(),
187
+ "updated_at": _now_iso(),
188
+ "assets": [],
189
+ }
190
+
191
+ def _write_manifest(self, data: Dict[str, Any]) -> None:
192
+ data["updated_at"] = _now_iso()
193
+ self.manifest_path.write_text(
194
+ json.dumps(data, indent=2, ensure_ascii=False),
195
+ encoding="utf-8",
196
+ )
197
+
198
+ def save_text_note(
199
+ self,
200
+ label: str,
201
+ text: str,
202
+ *,
203
+ source: str = "admin_text",
204
+ category: str = "knowledge",
205
+ mime_type: str = "text/plain",
206
+ ) -> SavedBrandAsset:
207
+ content = (text or "").strip()
208
+ if not content:
209
+ return SavedBrandAsset(manifest_entry={}, extracted_text="", saved_path="")
210
+
211
+ filename = _safe_name(f"{_slugify(label)}.txt")
212
+ raw_path = self.raw_dir / filename
213
+ raw_path.write_text(content, encoding="utf-8")
214
+
215
+ extracted_path = self.extracted_dir / f"{raw_path.stem}.txt"
216
+ extracted_path.write_text(content, encoding="utf-8")
217
+
218
+ sha = hashlib.sha256(content.encode("utf-8")).hexdigest()
219
+ entry = {
220
+ "id": sha[:12],
221
+ "filename": filename,
222
+ "category": category,
223
+ "source": source,
224
+ "mime_type": mime_type,
225
+ "raw_path": str(raw_path),
226
+ "extracted_path": str(extracted_path),
227
+ "bytes": len(content.encode("utf-8")),
228
+ "words": len(content.split()),
229
+ "sha256": sha,
230
+ "created_at": _now_iso(),
231
+ "textual": True,
232
+ }
233
+ manifest = self._read_manifest()
234
+ manifest.setdefault("assets", []).append(entry)
235
+ self._write_manifest(manifest)
236
+ return SavedBrandAsset(entry, extracted_text=content, saved_path=str(raw_path))
237
+
238
+ def save_binary_asset(
239
+ self,
240
+ *,
241
+ filename: str,
242
+ data: bytes,
243
+ mime_type: str = "",
244
+ source: str = "admin_attachment",
245
+ category: str = "asset",
246
+ caption: str = "",
247
+ ) -> SavedBrandAsset:
248
+ safe_name = _safe_name(filename)
249
+ raw_path = self.raw_dir / safe_name
250
+ raw_path.write_bytes(data)
251
+
252
+ guessed_mime = mime_type or mimetypes.guess_type(safe_name)[0] or "application/octet-stream"
253
+ extracted_text = _extract_rich_text(safe_name, guessed_mime, data)
254
+ textual = bool(extracted_text.strip()) or _looks_textual(safe_name, guessed_mime)
255
+ extracted_path = ""
256
+ if extracted_text:
257
+ extracted_path = str(self.extracted_dir / f"{raw_path.stem}.txt")
258
+ Path(extracted_path).write_text(extracted_text, encoding="utf-8")
259
+
260
+ sha = hashlib.sha256(data).hexdigest()
261
+ entry = {
262
+ "id": sha[:12],
263
+ "filename": safe_name,
264
+ "category": category,
265
+ "source": source,
266
+ "mime_type": guessed_mime,
267
+ "raw_path": str(raw_path),
268
+ "extracted_path": extracted_path,
269
+ "bytes": len(data),
270
+ "words": len(extracted_text.split()) if extracted_text else 0,
271
+ "sha256": sha,
272
+ "caption": (caption or "").strip(),
273
+ "created_at": _now_iso(),
274
+ "textual": textual,
275
+ }
276
+ manifest = self._read_manifest()
277
+ manifest.setdefault("assets", []).append(entry)
278
+ self._write_manifest(manifest)
279
+ return SavedBrandAsset(entry, extracted_text=extracted_text, saved_path=str(raw_path))
280
+
281
+ def manifest(self) -> Dict[str, Any]:
282
+ return self._read_manifest()
283
+
284
+ def clear(self) -> None:
285
+ if self.root.exists():
286
+ for child in sorted(self.root.rglob("*"), reverse=True):
287
+ if child.is_file():
288
+ child.unlink(missing_ok=True)
289
+ elif child.is_dir():
290
+ try:
291
+ child.rmdir()
292
+ except OSError:
293
+ pass
294
+ self.raw_dir.mkdir(parents=True, exist_ok=True)
295
+ self.extracted_dir.mkdir(parents=True, exist_ok=True)
296
+ self._ensure_manifest()
297
+
298
+ def summary_lines(self, limit: int = 6) -> List[str]:
299
+ manifest = self._read_manifest()
300
+ assets = manifest.get("assets", [])
301
+ lines = [
302
+ f"Vault: {self.root}",
303
+ f"Assets: {len(assets)}",
304
+ ]
305
+ for item in assets[-limit:]:
306
+ line = f"- {item.get('filename', 'asset')} · {item.get('category', 'asset')}"
307
+ if item.get("caption"):
308
+ line += f" · {item['caption'][:60]}"
309
+ lines.append(line)
310
+ return lines
311
+
312
+ def latest_identity_summary(self, limit: int = 5) -> str:
313
+ manifest = self._read_manifest()
314
+ assets = manifest.get("assets", [])
315
+ if not assets:
316
+ return ""
317
+ snippets = []
318
+ for item in assets[-limit:]:
319
+ filename = item.get("filename", "asset")
320
+ category = item.get("category", "asset")
321
+ caption = (item.get("caption") or "").strip()
322
+ snippets.append(f"{filename} [{category}]{': ' + caption if caption else ''}")
323
+ return "Activos de marca cargados: " + " | ".join(snippets)
package/conny ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Wrapper para delegar la ejecución a la nueva CLI de Conny (Node.js).
4
+ Mantiene compatibilidad con scripts antiguos que esperan que `conny` sea un ejecutable Python.
5
+ """
6
+ import os
7
+ import sys
8
+
9
+ def main():
10
+ # Encontrar la ruta base dinámica donde reside este ejecutable
11
+ base_dir = os.path.dirname(os.path.abspath(__file__))
12
+ target_script = os.path.join(base_dir, "npm", "conny.js")
13
+
14
+ # Construir los argumentos para node
15
+ args = ["node", target_script] + sys.argv[1:]
16
+
17
+ try:
18
+ # Reemplazar el proceso actual con node
19
+ os.execvp("node", args)
20
+ except FileNotFoundError:
21
+ print("Error: No se pudo encontrar 'node' en el PATH. Asegúrate de tener Node.js instalado.")
22
+ sys.exit(1)
23
+ except Exception as e:
24
+ print(f"Error al ejecutar la CLI de Conny: {e}")
25
+ sys.exit(1)
26
+
27
+ if __name__ == "__main__":
28
+ main()