@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/brand_assets.py
ADDED
|
@@ -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()
|