ragfly-cli 1.16.0__py3-none-any.whl
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.
- ragfly_cli/__init__.py +1 -0
- ragfly_cli/__main__.py +5 -0
- ragfly_cli/_http.py +67 -0
- ragfly_cli/cli.py +918 -0
- ragfly_cli/cloud_commands.py +201 -0
- ragfly_cli/config.py +102 -0
- ragfly_cli/grupo_activo.py +142 -0
- ragfly_cli/keyring_store.py +70 -0
- ragfly_cli/oop/__init__.py +12 -0
- ragfly_cli/oop/cli_command.py +86 -0
- ragfly_cli/oop/http_client.py +106 -0
- ragfly_cli/version_check.py +96 -0
- ragfly_cli-1.16.0.dist-info/METADATA +73 -0
- ragfly_cli-1.16.0.dist-info/RECORD +17 -0
- ragfly_cli-1.16.0.dist-info/WHEEL +5 -0
- ragfly_cli-1.16.0.dist-info/entry_points.txt +2 -0
- ragfly_cli-1.16.0.dist-info/top_level.txt +1 -0
ragfly_cli/cli.py
ADDED
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI principal de RAGfly — Cliente local y Cloud.
|
|
3
|
+
|
|
4
|
+
Comandos locales:
|
|
5
|
+
ragfly version — Versión del cliente
|
|
6
|
+
ragfly estado — Estado de la BD local
|
|
7
|
+
ragfly setup — Configuración inicial
|
|
8
|
+
ragfly escanear — Escanear directorio de documentos
|
|
9
|
+
ragfly procesar — Procesar documentos (CHUNKEAR; el cloud vectoriza)
|
|
10
|
+
ragfly sync — Sincronizar con el cloud
|
|
11
|
+
ragfly api — API local para integración
|
|
12
|
+
ragfly gui — Interfaz gráfica nativa (PySide6)
|
|
13
|
+
|
|
14
|
+
Comandos cloud:
|
|
15
|
+
ragfly login — Autenticar contra el cloud
|
|
16
|
+
ragfly logout — Cerrar sesión
|
|
17
|
+
ragfly cloud me — Ver contexto activo
|
|
18
|
+
ragfly cloud documento listar/ver
|
|
19
|
+
ragfly cloud espacio listar/ver
|
|
20
|
+
ragfly cloud cola ver/ejecuciones
|
|
21
|
+
ragfly cloud habilidad listar/ver/ejecutar
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
import click
|
|
28
|
+
import httpx
|
|
29
|
+
from rich.console import Console
|
|
30
|
+
from rich.table import Table
|
|
31
|
+
from rich.panel import Panel
|
|
32
|
+
|
|
33
|
+
from . import __version__
|
|
34
|
+
from ._http import default_headers
|
|
35
|
+
|
|
36
|
+
console = Console()
|
|
37
|
+
err_console = Console(stderr=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
41
|
+
# Grupo raíz
|
|
42
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
43
|
+
|
|
44
|
+
@click.group()
|
|
45
|
+
def app():
|
|
46
|
+
"""RAGfly — Cliente local y Cloud."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
51
|
+
# Comandos globales: login / logout
|
|
52
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
53
|
+
|
|
54
|
+
@app.command()
|
|
55
|
+
@click.option("--email", "-e", default=None, help="Email (para uso no interactivo)")
|
|
56
|
+
@click.option("--password-stdin", is_flag=True, help="Lee la contraseña desde stdin")
|
|
57
|
+
def login(email: str | None, password_stdin: bool):
|
|
58
|
+
"""Autenticarse contra el cloud y guardar sesión."""
|
|
59
|
+
from .cloud_commands import login as _login, CloudError
|
|
60
|
+
|
|
61
|
+
console.print()
|
|
62
|
+
if not email:
|
|
63
|
+
email = click.prompt(" Email")
|
|
64
|
+
|
|
65
|
+
if password_stdin:
|
|
66
|
+
password = sys.stdin.readline().rstrip("\n")
|
|
67
|
+
else:
|
|
68
|
+
password = click.prompt(" Contraseña", hide_input=True)
|
|
69
|
+
|
|
70
|
+
console.print()
|
|
71
|
+
console.print("[dim] Conectando...[/dim]", end="")
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
data = _login(email, password)
|
|
75
|
+
except CloudError as e:
|
|
76
|
+
console.print()
|
|
77
|
+
err_console.print(f"[red]✗ {e}[/red]")
|
|
78
|
+
raise SystemExit(e.exit_code)
|
|
79
|
+
|
|
80
|
+
console.print()
|
|
81
|
+
console.print(f"[green]✓ Sesión iniciada como[/green] {email}")
|
|
82
|
+
|
|
83
|
+
grupo = (
|
|
84
|
+
data.get("grupo_activo")
|
|
85
|
+
or data.get("usuario", {}).get("grupo_por_defecto", "")
|
|
86
|
+
or "—"
|
|
87
|
+
)
|
|
88
|
+
entidad = (
|
|
89
|
+
data.get("entidad_activa")
|
|
90
|
+
or data.get("usuario", {}).get("entidad_por_defecto", "")
|
|
91
|
+
or "—"
|
|
92
|
+
)
|
|
93
|
+
console.print(f" Grupo activo: {grupo}")
|
|
94
|
+
console.print(f" Entidad activa: {entidad}")
|
|
95
|
+
console.print(f" Sesión guardada en [dim]keyring del SO[/dim]")
|
|
96
|
+
console.print()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@app.command()
|
|
100
|
+
def logout():
|
|
101
|
+
"""Cerrar sesión (elimina el JWT del keyring)."""
|
|
102
|
+
from .cloud_commands import borrar_credenciales, ya_esta_logueado
|
|
103
|
+
|
|
104
|
+
if ya_esta_logueado():
|
|
105
|
+
borrar_credenciales()
|
|
106
|
+
console.print("[green]✓ Sesión cerrada.[/green]")
|
|
107
|
+
else:
|
|
108
|
+
console.print("[yellow]No había sesión activa.[/yellow]")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
112
|
+
# Sub-grupo: cloud
|
|
113
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
114
|
+
|
|
115
|
+
@app.group()
|
|
116
|
+
def cloud():
|
|
117
|
+
"""Operaciones contra el cloud de RAGfly (requiere ragfly login)."""
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@cloud.command("me")
|
|
122
|
+
@click.option("-o", "--output", type=click.Choice(["tabla", "json"]), default="tabla")
|
|
123
|
+
def cloud_me(output: str):
|
|
124
|
+
"""Ver el contexto activo del usuario autenticado."""
|
|
125
|
+
from .cloud_commands import cloud_get
|
|
126
|
+
from .oop import CliCommand
|
|
127
|
+
|
|
128
|
+
cmd = CliCommand()
|
|
129
|
+
data = cmd.protegido(cloud_get, "/auth/me")
|
|
130
|
+
|
|
131
|
+
if output == "json":
|
|
132
|
+
console.print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
console.print()
|
|
136
|
+
# /auth/me retorna campos en el nivel raíz: codigo_usuario, nombre, grupo_activo, etc.
|
|
137
|
+
tabla = Table(show_header=False, border_style="dim")
|
|
138
|
+
tabla.add_column("Campo", style="bold")
|
|
139
|
+
tabla.add_column("Valor")
|
|
140
|
+
tabla.add_row("Usuario", data.get("codigo_usuario") or data.get("email", "—"))
|
|
141
|
+
tabla.add_row("Nombre", data.get("nombre") or data.get("nombre_completo", "—"))
|
|
142
|
+
tabla.add_row("Rol principal", data.get("rol_principal", "—"))
|
|
143
|
+
tabla.add_row("Grupo activo", str(data.get("grupo_activo") or data.get("nombre_grupo", "—")))
|
|
144
|
+
tabla.add_row("Entidad activa", str(data.get("entidad_activa") or data.get("nombre_entidad", "—")))
|
|
145
|
+
console.print(tabla)
|
|
146
|
+
console.print()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── cloud grupo ──────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
@cloud.group("grupo")
|
|
152
|
+
def cloud_grupo():
|
|
153
|
+
"""Gestionar el grupo activo del cliente (paridad con dropdown del header web)."""
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@cloud_grupo.command("listar")
|
|
158
|
+
@click.option("-o", "--output", type=click.Choice(["tabla", "json"]), default="tabla")
|
|
159
|
+
def cloud_grupo_listar(output: str):
|
|
160
|
+
"""Listar grupos disponibles para el usuario autenticado."""
|
|
161
|
+
from .cloud_commands import obtener_token
|
|
162
|
+
from .config import get_config
|
|
163
|
+
from .grupo_activo import listar_grupos_disponibles, get_grupo_activo_local
|
|
164
|
+
from .oop import CliCommand
|
|
165
|
+
|
|
166
|
+
cmd = CliCommand()
|
|
167
|
+
|
|
168
|
+
def _accion():
|
|
169
|
+
token = obtener_token()
|
|
170
|
+
return listar_grupos_disponibles(token, get_config().cloud_url)
|
|
171
|
+
|
|
172
|
+
grupos = cmd.protegido(_accion)
|
|
173
|
+
activo = get_grupo_activo_local()
|
|
174
|
+
|
|
175
|
+
if output == "json":
|
|
176
|
+
console.print(json.dumps({"activo": activo, "grupos": grupos}, indent=2, ensure_ascii=False))
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
console.print()
|
|
180
|
+
if not grupos:
|
|
181
|
+
console.print("[yellow]Sin grupos asignados.[/yellow]")
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
t = Table(title="Grupos disponibles", border_style="dim")
|
|
185
|
+
t.add_column("", width=2)
|
|
186
|
+
t.add_column("Código", style="bold")
|
|
187
|
+
t.add_column("Nombre")
|
|
188
|
+
t.add_column("Alias", style="dim")
|
|
189
|
+
for g in grupos:
|
|
190
|
+
marca = "[green]●[/green]" if g["codigo_grupo"] == activo else " "
|
|
191
|
+
t.add_row(marca, g["codigo_grupo"], g.get("nombre_grupo") or "—", g.get("alias_grupo") or "—")
|
|
192
|
+
console.print(t)
|
|
193
|
+
if activo:
|
|
194
|
+
console.print(f" [dim]Grupo activo local: {activo}[/dim]")
|
|
195
|
+
else:
|
|
196
|
+
console.print(" [dim]Sin grupo activo local — usando defecto del usuario.[/dim]")
|
|
197
|
+
console.print()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@cloud_grupo.command("cambiar")
|
|
201
|
+
@click.argument("codigo_grupo")
|
|
202
|
+
def cloud_grupo_cambiar(codigo_grupo: str):
|
|
203
|
+
"""Cambiar el grupo activo del cliente. Valida con el backend antes de persistir."""
|
|
204
|
+
from .cloud_commands import obtener_token
|
|
205
|
+
from .config import get_config
|
|
206
|
+
from .grupo_activo import cambiar_grupo_remoto, set_grupo_activo
|
|
207
|
+
from .oop import CliCommand
|
|
208
|
+
|
|
209
|
+
cmd = CliCommand()
|
|
210
|
+
|
|
211
|
+
def _accion():
|
|
212
|
+
token = obtener_token()
|
|
213
|
+
return cambiar_grupo_remoto(codigo_grupo, token, get_config().cloud_url)
|
|
214
|
+
|
|
215
|
+
contexto = cmd.protegido(_accion)
|
|
216
|
+
set_grupo_activo(codigo_grupo)
|
|
217
|
+
|
|
218
|
+
nombre = contexto.get("nombre_grupo") or codigo_grupo
|
|
219
|
+
console.print(f"[green]✓ Grupo activo cambiado a:[/green] [bold]{codigo_grupo}[/bold] ({nombre})")
|
|
220
|
+
entidad = contexto.get("entidad_activa")
|
|
221
|
+
if entidad:
|
|
222
|
+
console.print(f" Entidad activa: {entidad}")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@cloud_grupo.command("limpiar")
|
|
226
|
+
def cloud_grupo_limpiar():
|
|
227
|
+
"""Quitar el grupo activo local — vuelve al grupo por defecto del usuario."""
|
|
228
|
+
from .grupo_activo import clear_grupo_activo, get_grupo_activo_local
|
|
229
|
+
|
|
230
|
+
actual = get_grupo_activo_local()
|
|
231
|
+
if not actual:
|
|
232
|
+
console.print("[yellow]No hay grupo activo local configurado.[/yellow]")
|
|
233
|
+
return
|
|
234
|
+
clear_grupo_activo()
|
|
235
|
+
console.print(f"[green]✓ Grupo activo local '{actual}' eliminado.[/green]")
|
|
236
|
+
console.print(" El cliente usará el grupo por defecto del usuario en la próxima request.")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ── cloud documento ──────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
@cloud.group("documento")
|
|
242
|
+
def cloud_documento():
|
|
243
|
+
"""Gestionar documentos en el cloud."""
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@cloud_documento.command("listar")
|
|
248
|
+
@click.option("--estado", default=None, help="Filtrar por estado (ej. VECTORIZADO)")
|
|
249
|
+
@click.option("--limite", default=20, show_default=True)
|
|
250
|
+
@click.option("--pagina", default=1, show_default=True)
|
|
251
|
+
@click.option("-o", "--output", type=click.Choice(["tabla", "json", "csv"]), default="tabla")
|
|
252
|
+
def cloud_documento_listar(estado: str | None, limite: int, pagina: int, output: str):
|
|
253
|
+
"""Listar documentos del grupo activo."""
|
|
254
|
+
from .cloud_commands import cloud_get
|
|
255
|
+
from .oop import CliCommand
|
|
256
|
+
|
|
257
|
+
params: dict = {"limite": limite, "pagina": pagina}
|
|
258
|
+
if estado:
|
|
259
|
+
params["estado"] = estado
|
|
260
|
+
|
|
261
|
+
cmd = CliCommand()
|
|
262
|
+
data = cmd.protegido(cloud_get, "/documentos/paginado", params=params)
|
|
263
|
+
|
|
264
|
+
items = data.get("items", data) if isinstance(data, dict) else data
|
|
265
|
+
|
|
266
|
+
if output == "json":
|
|
267
|
+
console.print(json.dumps(items, indent=2, ensure_ascii=False, default=str))
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
if output == "csv":
|
|
271
|
+
console.print("codigo,nombre,estado,ubicacion")
|
|
272
|
+
for d in items:
|
|
273
|
+
console.print(
|
|
274
|
+
f"{d.get('codigo_documento','')},{d.get('nombre_documento','')}"
|
|
275
|
+
f",{d.get('codigo_estado_doc','')},{d.get('nombre_ubicacion','')}"
|
|
276
|
+
)
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
console.print()
|
|
280
|
+
t = Table(title=f"Documentos (pág. {pagina})", border_style="dim")
|
|
281
|
+
t.add_column("Código", style="dim", no_wrap=True)
|
|
282
|
+
t.add_column("Nombre")
|
|
283
|
+
t.add_column("Estado")
|
|
284
|
+
t.add_column("Ubicación")
|
|
285
|
+
t.add_column("Tamaño", justify="right")
|
|
286
|
+
|
|
287
|
+
for d in items:
|
|
288
|
+
estado_val = d.get("codigo_estado_doc", "—")
|
|
289
|
+
color = {"VECTORIZADO": "green", "ESCANEADO": "cyan", "CARGADO": "yellow",
|
|
290
|
+
"REVISAR": "red", "CHUNKEADO": "blue"}.get(estado_val, "white")
|
|
291
|
+
t.add_row(
|
|
292
|
+
str(d.get("codigo_documento", "—")),
|
|
293
|
+
d.get("nombre_documento", "—")[:50],
|
|
294
|
+
f"[{color}]{estado_val}[/{color}]",
|
|
295
|
+
d.get("nombre_ubicacion") or d.get("codigo_ubicacion", "—"),
|
|
296
|
+
_fmt_bytes(d.get("tamano_bytes")),
|
|
297
|
+
)
|
|
298
|
+
console.print(t)
|
|
299
|
+
|
|
300
|
+
total = data.get("total") if isinstance(data, dict) else None
|
|
301
|
+
if total:
|
|
302
|
+
console.print(f" [dim]Total: {total} | Página {pagina}[/dim]")
|
|
303
|
+
console.print()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@cloud_documento.command("ver")
|
|
307
|
+
@click.argument("codigo")
|
|
308
|
+
@click.option("-o", "--output", type=click.Choice(["tabla", "json"]), default="tabla")
|
|
309
|
+
def cloud_documento_ver(codigo: str, output: str):
|
|
310
|
+
"""Ver detalle de un documento."""
|
|
311
|
+
from .cloud_commands import cloud_get
|
|
312
|
+
from .oop import CliCommand
|
|
313
|
+
|
|
314
|
+
cmd = CliCommand()
|
|
315
|
+
data = cmd.protegido(cloud_get, f"/documentos/{codigo}")
|
|
316
|
+
|
|
317
|
+
if output == "json":
|
|
318
|
+
console.print(json.dumps(data, indent=2, ensure_ascii=False, default=str))
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
console.print()
|
|
322
|
+
t = Table(show_header=False, border_style="dim", title=f"Documento {codigo}")
|
|
323
|
+
t.add_column("Campo", style="bold")
|
|
324
|
+
t.add_column("Valor")
|
|
325
|
+
t.add_row("Código", str(data.get("codigo_documento", "—")))
|
|
326
|
+
t.add_row("Nombre", data.get("nombre_documento", "—"))
|
|
327
|
+
t.add_row("Estado", data.get("codigo_estado_doc", "—"))
|
|
328
|
+
t.add_row("Ubicación", data.get("nombre_ubicacion") or data.get("codigo_ubicacion", "—"))
|
|
329
|
+
t.add_row("Tamaño", _fmt_bytes(data.get("tamano_bytes")))
|
|
330
|
+
t.add_row("Páginas", str(data.get("total_paginas", "—")))
|
|
331
|
+
t.add_row("Chunks", str(data.get("total_chunks", "—")))
|
|
332
|
+
t.add_row("Creado", str(data.get("fecha_creacion", "—"))[:19])
|
|
333
|
+
t.add_row("Procesado", str(data.get("fecha_actualizacion", "—"))[:19])
|
|
334
|
+
if data.get("resumen_documento"):
|
|
335
|
+
t.add_row("Resumen", data["resumen_documento"][:120])
|
|
336
|
+
console.print(t)
|
|
337
|
+
console.print()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# ── cloud espacio ────────────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
@cloud.group("espacio")
|
|
343
|
+
def cloud_espacio():
|
|
344
|
+
"""Gestionar Espacios de Trabajo en el cloud."""
|
|
345
|
+
pass
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@cloud_espacio.command("listar")
|
|
349
|
+
@click.option("--limite", default=20, show_default=True)
|
|
350
|
+
@click.option("-o", "--output", type=click.Choice(["tabla", "json"]), default="tabla")
|
|
351
|
+
def cloud_espacio_listar(limite: int, output: str):
|
|
352
|
+
"""Listar Espacios de Trabajo del grupo activo."""
|
|
353
|
+
from .cloud_commands import cloud_get
|
|
354
|
+
from .oop import CliCommand
|
|
355
|
+
|
|
356
|
+
cmd = CliCommand()
|
|
357
|
+
data = cmd.protegido(cloud_get, "/espacios-trabajo/paginado", params={"limite": limite})
|
|
358
|
+
|
|
359
|
+
items = data.get("items", data) if isinstance(data, dict) else data
|
|
360
|
+
|
|
361
|
+
if output == "json":
|
|
362
|
+
console.print(json.dumps(items, indent=2, ensure_ascii=False, default=str))
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
console.print()
|
|
366
|
+
t = Table(title="Espacios de Trabajo", border_style="dim")
|
|
367
|
+
t.add_column("ID", justify="right", style="dim")
|
|
368
|
+
t.add_column("Nombre")
|
|
369
|
+
t.add_column("Descripción")
|
|
370
|
+
t.add_column("Docs", justify="right")
|
|
371
|
+
t.add_column("Creado")
|
|
372
|
+
|
|
373
|
+
for e in items:
|
|
374
|
+
t.add_row(
|
|
375
|
+
str(e.get("id_espacio", "—")),
|
|
376
|
+
e.get("nombre_espacio", "—")[:40],
|
|
377
|
+
(e.get("descripcion") or "")[:40],
|
|
378
|
+
str(e.get("n_documentos", "—")),
|
|
379
|
+
str(e.get("fecha_creacion", "—"))[:10],
|
|
380
|
+
)
|
|
381
|
+
console.print(t)
|
|
382
|
+
console.print()
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@cloud_espacio.command("ver")
|
|
386
|
+
@click.argument("id_espacio", type=int)
|
|
387
|
+
@click.option("--limite", default=20, show_default=True)
|
|
388
|
+
@click.option("-o", "--output", type=click.Choice(["tabla", "json"]), default="tabla")
|
|
389
|
+
def cloud_espacio_ver(id_espacio: int, limite: int, output: str):
|
|
390
|
+
"""Ver detalle de un Espacio de Trabajo con sus documentos."""
|
|
391
|
+
from .cloud_commands import cloud_get
|
|
392
|
+
from .oop import CliCommand
|
|
393
|
+
|
|
394
|
+
cmd = CliCommand()
|
|
395
|
+
# No hay GET /{id} suelto — buscamos en paginado y filtramos
|
|
396
|
+
espacios_data = cmd.protegido(cloud_get, "/espacios-trabajo/paginado", params={"limite": 200})
|
|
397
|
+
items_esp = espacios_data.get("items", espacios_data) if isinstance(espacios_data, dict) else espacios_data
|
|
398
|
+
espacio = next((e for e in items_esp if e.get("id_espacio") == id_espacio), None)
|
|
399
|
+
if not espacio:
|
|
400
|
+
cmd.salir(f"Espacio #{id_espacio} no encontrado.", exit_code=1)
|
|
401
|
+
docs_data = cmd.protegido(
|
|
402
|
+
cloud_get,
|
|
403
|
+
f"/espacios-trabajo/{id_espacio}/documentos/paginado",
|
|
404
|
+
params={"limite": limite},
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
if output == "json":
|
|
408
|
+
console.print(json.dumps({"espacio": espacio, "documentos": docs_data},
|
|
409
|
+
indent=2, ensure_ascii=False, default=str))
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
console.print()
|
|
413
|
+
info = Table(show_header=False, border_style="dim",
|
|
414
|
+
title=f"Espacio #{id_espacio}")
|
|
415
|
+
info.add_column("Campo", style="bold")
|
|
416
|
+
info.add_column("Valor")
|
|
417
|
+
info.add_row("Nombre", espacio.get("nombre_espacio", "—"))
|
|
418
|
+
info.add_row("Descripción", espacio.get("descripcion") or "—")
|
|
419
|
+
info.add_row("Creado", str(espacio.get("fecha_creacion", "—"))[:19])
|
|
420
|
+
console.print(info)
|
|
421
|
+
console.print()
|
|
422
|
+
|
|
423
|
+
docs = docs_data.get("items", docs_data) if isinstance(docs_data, dict) else docs_data
|
|
424
|
+
if docs:
|
|
425
|
+
t = Table(title=f"Documentos ({len(docs)})", border_style="dim")
|
|
426
|
+
t.add_column("Código", style="dim")
|
|
427
|
+
t.add_column("Nombre")
|
|
428
|
+
t.add_column("Estado")
|
|
429
|
+
t.add_column("Cola", justify="right")
|
|
430
|
+
for d in docs:
|
|
431
|
+
t.add_row(
|
|
432
|
+
str(d.get("codigo_documento", "—")),
|
|
433
|
+
d.get("nombre_documento", "—")[:45],
|
|
434
|
+
d.get("codigo_estado_doc", "—"),
|
|
435
|
+
d.get("estado_cola", "—"),
|
|
436
|
+
)
|
|
437
|
+
console.print(t)
|
|
438
|
+
console.print()
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# ── cloud cola ───────────────────────────────────────────────────────────────
|
|
442
|
+
|
|
443
|
+
@cloud.group("cola")
|
|
444
|
+
def cloud_cola():
|
|
445
|
+
"""Ver el estado de la cola de procesamiento."""
|
|
446
|
+
pass
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@cloud_cola.command("ver")
|
|
450
|
+
@click.option("--proceso", default=None, help="Filtrar por proceso (ej. VECTORIZAR)")
|
|
451
|
+
@click.option("--estado", default=None, help="Filtrar por estado (PENDIENTE, EJECUTANDO, etc.)")
|
|
452
|
+
@click.option("--limite", default=20, show_default=True)
|
|
453
|
+
@click.option("-o", "--output", type=click.Choice(["tabla", "json"]), default="tabla")
|
|
454
|
+
def cloud_cola_ver(proceso: str | None, estado: str | None, limite: int, output: str):
|
|
455
|
+
"""Ver el estado actual de la cola del pipeline."""
|
|
456
|
+
from .cloud_commands import cloud_get
|
|
457
|
+
from .oop import CliCommand
|
|
458
|
+
|
|
459
|
+
params: dict = {"limite": limite}
|
|
460
|
+
if proceso:
|
|
461
|
+
params["proceso"] = proceso
|
|
462
|
+
if estado:
|
|
463
|
+
params["estado"] = estado
|
|
464
|
+
|
|
465
|
+
cmd = CliCommand()
|
|
466
|
+
data = cmd.protegido(cloud_get, "/cola-estados-docs/paginado", params=params)
|
|
467
|
+
|
|
468
|
+
items = data.get("items", data) if isinstance(data, dict) else data
|
|
469
|
+
|
|
470
|
+
if output == "json":
|
|
471
|
+
console.print(json.dumps(items, indent=2, ensure_ascii=False, default=str))
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
console.print()
|
|
475
|
+
t = Table(title="Cola de procesamiento", border_style="dim")
|
|
476
|
+
t.add_column("ID", justify="right", style="dim")
|
|
477
|
+
t.add_column("Proceso")
|
|
478
|
+
t.add_column("Estado")
|
|
479
|
+
t.add_column("Documento")
|
|
480
|
+
t.add_column("Encolado")
|
|
481
|
+
t.add_column("Error")
|
|
482
|
+
|
|
483
|
+
_est_color = {
|
|
484
|
+
"PENDIENTE": "yellow", "EJECUTANDO": "cyan",
|
|
485
|
+
"TERMINADO": "green", "ERROR": "red",
|
|
486
|
+
}
|
|
487
|
+
for item in items:
|
|
488
|
+
est = item.get("estado_cola", "—")
|
|
489
|
+
t.add_row(
|
|
490
|
+
str(item.get("id_cola", "—")),
|
|
491
|
+
item.get("proceso") or item.get("codigo_habilidad", "—"),
|
|
492
|
+
f"[{_est_color.get(est, 'white')}]{est}[/{_est_color.get(est, 'white')}]",
|
|
493
|
+
str(item.get("codigo_documento", "—")),
|
|
494
|
+
str(item.get("fecha_inicio", item.get("fecha_cola", "—")))[:16],
|
|
495
|
+
(item.get("mensaje_error") or "")[:30],
|
|
496
|
+
)
|
|
497
|
+
console.print(t)
|
|
498
|
+
|
|
499
|
+
total = data.get("total") if isinstance(data, dict) else None
|
|
500
|
+
if total:
|
|
501
|
+
console.print(f" [dim]Total en cola: {total}[/dim]")
|
|
502
|
+
console.print()
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
@cloud_cola.command("ejecuciones")
|
|
506
|
+
@click.option("--limite", default=10, show_default=True)
|
|
507
|
+
@click.option("-o", "--output", type=click.Choice(["tabla", "json"]), default="tabla")
|
|
508
|
+
def cloud_cola_ejecuciones(limite: int, output: str):
|
|
509
|
+
"""Ver historial de ejecuciones de habilidades."""
|
|
510
|
+
from .cloud_commands import cloud_get
|
|
511
|
+
from .oop import CliCommand
|
|
512
|
+
|
|
513
|
+
cmd = CliCommand()
|
|
514
|
+
data = cmd.protegido(cloud_get, "/cola-estados-docs/ejecuciones", params={"limite": limite})
|
|
515
|
+
|
|
516
|
+
items = data.get("items", data) if isinstance(data, dict) else data
|
|
517
|
+
|
|
518
|
+
if output == "json":
|
|
519
|
+
console.print(json.dumps(items, indent=2, ensure_ascii=False, default=str))
|
|
520
|
+
return
|
|
521
|
+
|
|
522
|
+
console.print()
|
|
523
|
+
t = Table(title="Historial de ejecuciones", border_style="dim")
|
|
524
|
+
t.add_column("ID", justify="right", style="dim")
|
|
525
|
+
t.add_column("Habilidad")
|
|
526
|
+
t.add_column("Inicio")
|
|
527
|
+
t.add_column("Fin")
|
|
528
|
+
t.add_column("Docs", justify="right")
|
|
529
|
+
t.add_column("OK", justify="right", style="green")
|
|
530
|
+
t.add_column("Err", justify="right", style="red")
|
|
531
|
+
t.add_column("Duración")
|
|
532
|
+
|
|
533
|
+
for e in items:
|
|
534
|
+
t.add_row(
|
|
535
|
+
str(e.get("id_ejecucion") or e.get("id", "—")),
|
|
536
|
+
e.get("codigo_habilidad", "—"),
|
|
537
|
+
str(e.get("fecha_inicio", "—"))[:16],
|
|
538
|
+
str(e.get("fecha_fin", "—"))[:16],
|
|
539
|
+
str(e.get("total_docs", "—")),
|
|
540
|
+
str(e.get("docs_ok", "—")),
|
|
541
|
+
str(e.get("docs_error", "—")),
|
|
542
|
+
e.get("duracion") or "—",
|
|
543
|
+
)
|
|
544
|
+
console.print(t)
|
|
545
|
+
console.print()
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
# ── cloud habilidad ──────────────────────────────────────────────────────────
|
|
549
|
+
|
|
550
|
+
@cloud.group("habilidad")
|
|
551
|
+
def cloud_habilidad():
|
|
552
|
+
"""Gestionar y ejecutar habilidades LLM del catálogo global."""
|
|
553
|
+
pass
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
@cloud_habilidad.command("listar")
|
|
557
|
+
@click.option("-o", "--output", type=click.Choice(["tabla", "json"]), default="tabla")
|
|
558
|
+
def cloud_habilidad_listar(output: str):
|
|
559
|
+
"""Listar todas las habilidades disponibles."""
|
|
560
|
+
from .cloud_commands import cloud_get
|
|
561
|
+
from .oop import CliCommand
|
|
562
|
+
|
|
563
|
+
cmd = CliCommand()
|
|
564
|
+
items = cmd.protegido(cloud_get, "/habilidades")
|
|
565
|
+
|
|
566
|
+
if output == "json":
|
|
567
|
+
console.print(json.dumps(items, indent=2, ensure_ascii=False, default=str))
|
|
568
|
+
return
|
|
569
|
+
|
|
570
|
+
console.print()
|
|
571
|
+
t = Table(title="Habilidades disponibles", border_style="dim")
|
|
572
|
+
t.add_column("Código", style="bold")
|
|
573
|
+
t.add_column("Nombre")
|
|
574
|
+
t.add_column("Tipo")
|
|
575
|
+
t.add_column("Salida")
|
|
576
|
+
t.add_column("Modelo")
|
|
577
|
+
|
|
578
|
+
for h in items:
|
|
579
|
+
t.add_row(
|
|
580
|
+
h.get("codigo_habilidad", "—"),
|
|
581
|
+
h.get("nombre_habilidad") or h.get("alias", "—"),
|
|
582
|
+
h.get("aplica_a", "—"),
|
|
583
|
+
h.get("salida_destino", "—"),
|
|
584
|
+
h.get("id_modelo") or "[dim]del invocador[/dim]",
|
|
585
|
+
)
|
|
586
|
+
console.print(t)
|
|
587
|
+
console.print()
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
@cloud_habilidad.command("ver")
|
|
591
|
+
@click.argument("codigo")
|
|
592
|
+
@click.option("-o", "--output", type=click.Choice(["tabla", "json"]), default="tabla")
|
|
593
|
+
def cloud_habilidad_ver(codigo: str, output: str):
|
|
594
|
+
"""Ver detalle de una habilidad."""
|
|
595
|
+
from .cloud_commands import cloud_get
|
|
596
|
+
from .oop import CliCommand
|
|
597
|
+
|
|
598
|
+
cmd = CliCommand()
|
|
599
|
+
h = cmd.protegido(cloud_get, f"/habilidades/{codigo}")
|
|
600
|
+
|
|
601
|
+
if output == "json":
|
|
602
|
+
console.print(json.dumps(h, indent=2, ensure_ascii=False, default=str))
|
|
603
|
+
return
|
|
604
|
+
|
|
605
|
+
console.print()
|
|
606
|
+
t = Table(show_header=False, border_style="dim",
|
|
607
|
+
title=f"Habilidad {codigo}")
|
|
608
|
+
t.add_column("Campo", style="bold")
|
|
609
|
+
t.add_column("Valor")
|
|
610
|
+
t.add_row("Código", h.get("codigo_habilidad", "—"))
|
|
611
|
+
t.add_row("Nombre", h.get("nombre_habilidad") or h.get("alias", "—"))
|
|
612
|
+
t.add_row("Tipo", h.get("aplica_a", "—"))
|
|
613
|
+
t.add_row("Modelo", h.get("id_modelo") or "[dim]del invocador[/dim]")
|
|
614
|
+
t.add_row("Salida", h.get("salida_destino", "—"))
|
|
615
|
+
t.add_row("Col. salida", h.get("salida_columna") or "—")
|
|
616
|
+
console.print(t)
|
|
617
|
+
|
|
618
|
+
if h.get("prompt_habilidad"):
|
|
619
|
+
console.print()
|
|
620
|
+
console.print("[bold]Prompt:[/bold]")
|
|
621
|
+
console.print(f" [dim]{h['prompt_habilidad'][:300]}[/dim]")
|
|
622
|
+
if h.get("system_prompt"):
|
|
623
|
+
console.print()
|
|
624
|
+
console.print("[bold]System prompt:[/bold]")
|
|
625
|
+
console.print(f" [dim]{h['system_prompt'][:200]}[/dim]")
|
|
626
|
+
console.print()
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
@cloud_habilidad.command("ejecutar")
|
|
630
|
+
@click.argument("codigo")
|
|
631
|
+
@click.option("--espacio", type=int, default=None, help="ID del Espacio de Trabajo")
|
|
632
|
+
@click.option("--documento", default=None, help="Código de documento único")
|
|
633
|
+
@click.option("--esperar", is_flag=True, help="Esperar a que termine y mostrar resultado")
|
|
634
|
+
@click.option("-o", "--output", type=click.Choice(["tabla", "json"]), default="tabla")
|
|
635
|
+
def cloud_habilidad_ejecutar(
|
|
636
|
+
codigo: str, espacio: int | None, documento: str | None,
|
|
637
|
+
esperar: bool, output: str
|
|
638
|
+
):
|
|
639
|
+
"""Ejecutar una habilidad sobre un Espacio de Trabajo o documento."""
|
|
640
|
+
from .cloud_commands import cloud_post
|
|
641
|
+
from .oop import CliCommand
|
|
642
|
+
|
|
643
|
+
cmd = CliCommand()
|
|
644
|
+
if not espacio and not documento:
|
|
645
|
+
cmd.salir("Debes indicar --espacio <ID> o --documento <CODIGO>", exit_code=1)
|
|
646
|
+
|
|
647
|
+
body: dict = {}
|
|
648
|
+
if espacio:
|
|
649
|
+
body["id_espacio"] = espacio
|
|
650
|
+
if documento:
|
|
651
|
+
body["codigo_documento"] = documento
|
|
652
|
+
|
|
653
|
+
resultado = cmd.protegido(cloud_post, f"/habilidades/{codigo}/ejecutar", body=body)
|
|
654
|
+
|
|
655
|
+
if output == "json":
|
|
656
|
+
console.print(json.dumps(resultado, indent=2, ensure_ascii=False, default=str))
|
|
657
|
+
return
|
|
658
|
+
|
|
659
|
+
# Contrato uniforme (SobreEjecucion): codigo_proceso + detalle.n_items_cola.
|
|
660
|
+
detalle = resultado.get("detalle") or {}
|
|
661
|
+
proceso = resultado.get("codigo_proceso", "—")
|
|
662
|
+
docs = detalle.get("n_items_cola", resultado.get("n_documentos", "—"))
|
|
663
|
+
no_proc = detalle.get("n_no_procesables", 0)
|
|
664
|
+
estado = resultado.get("estado", "PENDIENTE")
|
|
665
|
+
|
|
666
|
+
console.print()
|
|
667
|
+
console.print(f"[green]✓ Encolado[/green]" if resultado.get("aceptada", True)
|
|
668
|
+
else "[red]✗ No aceptada[/red]")
|
|
669
|
+
console.print(f" Proceso : {proceso}")
|
|
670
|
+
console.print(f" Documentos : {docs}")
|
|
671
|
+
if no_proc:
|
|
672
|
+
console.print(f" No procesables: {no_proc}")
|
|
673
|
+
console.print(f" Estado : {estado}")
|
|
674
|
+
if resultado.get("mensaje"):
|
|
675
|
+
console.print(f" [dim]{resultado.get('mensaje')}[/dim]")
|
|
676
|
+
console.print()
|
|
677
|
+
console.print(f" Sigue progreso: [dim]ragfly cloud cola ver[/dim]")
|
|
678
|
+
console.print()
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
682
|
+
# Sub-comando: cloud catalogo (capabilities — contrato multi-interfaz)
|
|
683
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
684
|
+
|
|
685
|
+
@cloud.command("catalogo")
|
|
686
|
+
@click.option("--tipo", type=click.Choice(["TODO", "FUNCIONES", "HABILIDADES"]),
|
|
687
|
+
default="TODO", show_default=True, help="Qué parte del catálogo listar")
|
|
688
|
+
@click.option("-o", "--output", type=click.Choice(["tabla", "json"]), default="tabla")
|
|
689
|
+
def cloud_catalogo(tipo: str, output: str):
|
|
690
|
+
"""Catálogo de capabilities: qué puede hacer el usuario (funciones + habilidades).
|
|
691
|
+
|
|
692
|
+
Mismo contrato que consumen el chat y MCP (GET /catalogo). Filtrado por el
|
|
693
|
+
rol, tipo de acceso, grupo y aplicación del usuario.
|
|
694
|
+
"""
|
|
695
|
+
from .cloud_commands import cloud_get
|
|
696
|
+
from .oop import CliCommand
|
|
697
|
+
|
|
698
|
+
cmd = CliCommand()
|
|
699
|
+
data = cmd.protegido(cloud_get, "/catalogo", params={"tipo": tipo})
|
|
700
|
+
|
|
701
|
+
if output == "json":
|
|
702
|
+
console.print(json.dumps(data, indent=2, ensure_ascii=False, default=str))
|
|
703
|
+
return
|
|
704
|
+
|
|
705
|
+
funciones = data.get("funciones", [])
|
|
706
|
+
habilidades = data.get("habilidades", [])
|
|
707
|
+
|
|
708
|
+
if funciones:
|
|
709
|
+
console.print()
|
|
710
|
+
t = Table(title=f"Funciones disponibles ({len(funciones)})", border_style="dim")
|
|
711
|
+
t.add_column("Código", style="bold")
|
|
712
|
+
t.add_column("Nombre")
|
|
713
|
+
t.add_column("Resumen")
|
|
714
|
+
t.add_column("Permisos")
|
|
715
|
+
for f in funciones:
|
|
716
|
+
perms = "".join([
|
|
717
|
+
"S" if f.get("perm_select") else "-",
|
|
718
|
+
"I" if f.get("perm_insert") else "-",
|
|
719
|
+
"U" if f.get("perm_update") else "-",
|
|
720
|
+
"D" if f.get("perm_delete") else "-",
|
|
721
|
+
])
|
|
722
|
+
t.add_row(
|
|
723
|
+
f.get("codigo_funcion", "—"),
|
|
724
|
+
f.get("nombre_funcion") or "—",
|
|
725
|
+
(f.get("descripcion_llm") or f.get("descripcion") or "")[:80],
|
|
726
|
+
perms,
|
|
727
|
+
)
|
|
728
|
+
console.print(t)
|
|
729
|
+
|
|
730
|
+
if habilidades:
|
|
731
|
+
console.print()
|
|
732
|
+
t = Table(title=f"Habilidades disponibles ({len(habilidades)})", border_style="dim")
|
|
733
|
+
t.add_column("Código", style="bold")
|
|
734
|
+
t.add_column("Nombre")
|
|
735
|
+
t.add_column("Resumen")
|
|
736
|
+
for h in habilidades:
|
|
737
|
+
t.add_row(
|
|
738
|
+
h.get("codigo_habilidad", "—"),
|
|
739
|
+
h.get("nombre_habilidad") or "—",
|
|
740
|
+
(h.get("descripcion_llm") or h.get("descripcion") or "")[:80],
|
|
741
|
+
)
|
|
742
|
+
console.print(t)
|
|
743
|
+
|
|
744
|
+
if not funciones and not habilidades:
|
|
745
|
+
console.print("[dim]Sin capabilities disponibles para este contexto.[/dim]")
|
|
746
|
+
console.print()
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
750
|
+
# Sub-grupo: cloud buscar (RAG one-shot)
|
|
751
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
752
|
+
|
|
753
|
+
@cloud.command("buscar")
|
|
754
|
+
@click.argument("consulta", nargs=-1, required=True)
|
|
755
|
+
@click.option("--limite", type=int, default=10, show_default=True,
|
|
756
|
+
help="Top-K final tras rerank")
|
|
757
|
+
@click.option("--min-similitud", type=float, default=0.0, show_default=True,
|
|
758
|
+
help="Umbral coseno mínimo")
|
|
759
|
+
@click.option("--entidad", default=None, help="Filtrar por código de entidad")
|
|
760
|
+
@click.option("-o", "--output", type=click.Choice(["tabla", "json"]), default="tabla")
|
|
761
|
+
def cloud_buscar(
|
|
762
|
+
consulta: tuple[str, ...], limite: int, min_similitud: float,
|
|
763
|
+
entidad: str | None, output: str,
|
|
764
|
+
):
|
|
765
|
+
"""Búsqueda semántica RAG sobre los documentos vectorizados del grupo."""
|
|
766
|
+
from .cloud_commands import cloud_post
|
|
767
|
+
from .oop import CliCommand
|
|
768
|
+
|
|
769
|
+
q = " ".join(consulta).strip()
|
|
770
|
+
if not q:
|
|
771
|
+
err_console.print("[red]La consulta no puede estar vacía.[/red]")
|
|
772
|
+
raise SystemExit(1)
|
|
773
|
+
|
|
774
|
+
body: dict = {"q": q, "limit": limite, "min_similitud": min_similitud}
|
|
775
|
+
if entidad:
|
|
776
|
+
body["codigo_entidad"] = entidad
|
|
777
|
+
|
|
778
|
+
cmd = CliCommand()
|
|
779
|
+
data = cmd.protegido(cloud_post, "/documentos/buscar-semantico", body=body)
|
|
780
|
+
|
|
781
|
+
if output == "json":
|
|
782
|
+
console.print(json.dumps(data, indent=2, ensure_ascii=False, default=str))
|
|
783
|
+
return
|
|
784
|
+
|
|
785
|
+
items = data.get("resultados") or data.get("items") or data if isinstance(data, list) else (
|
|
786
|
+
data.get("resultados") or data.get("items") or []
|
|
787
|
+
)
|
|
788
|
+
if not items:
|
|
789
|
+
console.print("[yellow]Sin resultados.[/yellow]")
|
|
790
|
+
return
|
|
791
|
+
|
|
792
|
+
tabla = Table(title=f"RAG: {q[:60]}", show_lines=False)
|
|
793
|
+
tabla.add_column("#", style="dim", width=3)
|
|
794
|
+
tabla.add_column("Documento", overflow="fold")
|
|
795
|
+
tabla.add_column("Score", justify="right", width=8)
|
|
796
|
+
tabla.add_column("Fragmento", overflow="fold")
|
|
797
|
+
for i, h in enumerate(items, 1):
|
|
798
|
+
score = h.get("score") or h.get("similitud") or h.get("rerank_score") or 0
|
|
799
|
+
nombre = h.get("nombre_documento") or h.get("codigo_documento") or "—"
|
|
800
|
+
frag = (h.get("texto") or h.get("chunk_texto") or "")[:200]
|
|
801
|
+
tabla.add_row(str(i), str(nombre), f"{float(score):.3f}" if isinstance(score, (int, float)) else str(score), frag)
|
|
802
|
+
console.print(tabla)
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
806
|
+
# Sub-grupo: cloud chat (RAG conversacional)
|
|
807
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
808
|
+
|
|
809
|
+
@cloud.group("chat")
|
|
810
|
+
def cloud_chat():
|
|
811
|
+
"""Conversar con tus documentos vía RAG."""
|
|
812
|
+
pass
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
@cloud_chat.command("preguntar")
|
|
816
|
+
@click.argument("mensaje", nargs=-1, required=True)
|
|
817
|
+
@click.option("--funcion", default="CHAT-USUARIO", show_default=True,
|
|
818
|
+
help="Código de función del chat")
|
|
819
|
+
@click.option("--conversacion", "id_conversacion", type=int, default=None,
|
|
820
|
+
help="ID de conversación existente (omitir = crear nueva)")
|
|
821
|
+
@click.option("--titulo", default=None, help="Título inicial (al crear nueva)")
|
|
822
|
+
@click.option("-o", "--output", type=click.Choice(["texto", "json"]), default="texto")
|
|
823
|
+
def cloud_chat_preguntar(
|
|
824
|
+
mensaje: tuple[str, ...], funcion: str, id_conversacion: int | None,
|
|
825
|
+
titulo: str | None, output: str,
|
|
826
|
+
):
|
|
827
|
+
"""Hacer una pregunta al chat RAG. Crea conversación si no se indica una."""
|
|
828
|
+
from .cloud_commands import cloud_post, _headers, CLOUD_URL, CloudError
|
|
829
|
+
from .oop import CliCommand
|
|
830
|
+
|
|
831
|
+
contenido = " ".join(mensaje).strip()
|
|
832
|
+
if not contenido:
|
|
833
|
+
err_console.print("[red]El mensaje no puede estar vacío.[/red]")
|
|
834
|
+
raise SystemExit(1)
|
|
835
|
+
|
|
836
|
+
cmd = CliCommand()
|
|
837
|
+
|
|
838
|
+
if not id_conversacion:
|
|
839
|
+
body_conv = {"codigo_funcion": funcion}
|
|
840
|
+
if titulo:
|
|
841
|
+
body_conv["titulo"] = titulo
|
|
842
|
+
nueva = cmd.protegido(cloud_post, "/chat/conversaciones", body=body_conv)
|
|
843
|
+
id_conversacion = int(nueva.get("id_conversacion") or 0)
|
|
844
|
+
if not id_conversacion:
|
|
845
|
+
err_console.print(f"[red]No se pudo crear conversación: {nueva}[/red]")
|
|
846
|
+
raise SystemExit(2)
|
|
847
|
+
|
|
848
|
+
url = f"{CLOUD_URL}/chat/conversaciones/{id_conversacion}/mensajes/stream"
|
|
849
|
+
try:
|
|
850
|
+
with httpx.stream(
|
|
851
|
+
"POST", url, headers=_headers(), json={"contenido": contenido}, timeout=120.0,
|
|
852
|
+
) as r:
|
|
853
|
+
if r.status_code >= 400:
|
|
854
|
+
err_console.print(f"[red]HTTP {r.status_code}: {r.read()[:300].decode('utf-8', errors='replace')}[/red]")
|
|
855
|
+
raise SystemExit(2)
|
|
856
|
+
partes: list[str] = []
|
|
857
|
+
meta: dict = {}
|
|
858
|
+
for raw in r.iter_lines():
|
|
859
|
+
if not raw or not raw.startswith("data:"):
|
|
860
|
+
continue
|
|
861
|
+
payload = raw[5:].strip()
|
|
862
|
+
if not payload:
|
|
863
|
+
continue
|
|
864
|
+
try:
|
|
865
|
+
evt = json.loads(payload)
|
|
866
|
+
except Exception:
|
|
867
|
+
continue
|
|
868
|
+
if "text" in evt:
|
|
869
|
+
chunk = str(evt["text"])
|
|
870
|
+
partes.append(chunk)
|
|
871
|
+
if output == "texto":
|
|
872
|
+
console.print(chunk, end="", soft_wrap=True)
|
|
873
|
+
elif "done" in evt:
|
|
874
|
+
meta = {k: v for k, v in evt.items() if k != "done"}
|
|
875
|
+
elif "error" in evt:
|
|
876
|
+
err_console.print(f"\n[red]Error del servidor: {evt['error']}[/red]")
|
|
877
|
+
raise SystemExit(2)
|
|
878
|
+
except httpx.RequestError as e:
|
|
879
|
+
raise CloudError(f"No se pudo conectar al servidor: {e}", exit_code=2)
|
|
880
|
+
|
|
881
|
+
if output == "json":
|
|
882
|
+
console.print(json.dumps({
|
|
883
|
+
"id_conversacion": id_conversacion,
|
|
884
|
+
"respuesta": "".join(partes),
|
|
885
|
+
**meta,
|
|
886
|
+
}, indent=2, ensure_ascii=False, default=str))
|
|
887
|
+
else:
|
|
888
|
+
console.print()
|
|
889
|
+
console.print(f"[dim]Conversación #{id_conversacion}[/dim]")
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
893
|
+
# Helpers internos
|
|
894
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
895
|
+
|
|
896
|
+
def _fmt_bytes(b: int | None) -> str:
|
|
897
|
+
if b is None:
|
|
898
|
+
return "—"
|
|
899
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
900
|
+
if b < 1024:
|
|
901
|
+
return f"{b:.0f} {unit}"
|
|
902
|
+
b //= 1024
|
|
903
|
+
return f"{b} TB"
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
@app.command()
|
|
907
|
+
def version():
|
|
908
|
+
"""Muestra la versión del cliente y avisa si hay actualización disponible."""
|
|
909
|
+
console.print(f"[bold blue]RAGfly[/bold blue] Cliente v{__version__}")
|
|
910
|
+
try:
|
|
911
|
+
from .version_check import chequear_actualizacion
|
|
912
|
+
aviso = chequear_actualizacion()
|
|
913
|
+
if aviso:
|
|
914
|
+
console.print(f"[yellow]{aviso}[/yellow]")
|
|
915
|
+
except Exception:
|
|
916
|
+
pass # silencioso: no romper version si no hay config/red
|
|
917
|
+
|
|
918
|
+
|