ragfly-cli 1.19.0__tar.gz → 1.20.0__tar.gz
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-1.19.0/ragfly_cli.egg-info → ragfly_cli-1.20.0}/PKG-INFO +19 -15
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/README.md +18 -14
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/pyproject.toml +1 -1
- ragfly_cli-1.20.0/ragfly_cli/__init__.py +1 -0
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli/_http.py +13 -11
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli/cli.py +114 -2
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli/grupo_activo.py +1 -1
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli/oop/cli_command.py +2 -2
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli/oop/http_client.py +2 -2
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli/version_check.py +2 -2
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0/ragfly_cli.egg-info}/PKG-INFO +19 -15
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli.egg-info/SOURCES.txt +3 -1
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/tests/test_agent_context_cli.py +13 -0
- ragfly_cli-1.20.0/tests/test_api_key_group_override.py +18 -0
- ragfly_cli-1.20.0/tests/test_surface_parity_cli.py +75 -0
- ragfly_cli-1.19.0/ragfly_cli/__init__.py +0 -1
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/LICENSE +0 -0
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli/__main__.py +0 -0
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli/_runtime.py +0 -0
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli/cloud_commands.py +0 -0
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli/config.py +0 -0
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli/keyring_store.py +0 -0
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli/oop/__init__.py +0 -0
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli.egg-info/dependency_links.txt +0 -0
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli.egg-info/entry_points.txt +0 -0
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli.egg-info/requires.txt +0 -0
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/ragfly_cli.egg-info/top_level.txt +0 -0
- {ragfly_cli-1.19.0 → ragfly_cli-1.20.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ragfly-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.20.0
|
|
4
4
|
Summary: RAGfly CLI — operate RAGfly from the terminal and CI (login + cloud API). Lightweight, no desktop dependencies.
|
|
5
5
|
Author: CAB Ltda
|
|
6
6
|
License: MIT
|
|
@@ -49,24 +49,24 @@ export RAGFLY_API_KEY=rf_xxxxxxxxxx
|
|
|
49
49
|
`cloud group` and `cloud api-key` manage a person's session and credentials: they
|
|
50
50
|
need `ragfly login`, and the server refuses them for an API key.
|
|
51
51
|
|
|
52
|
-
##
|
|
52
|
+
## Opening an original file on disk (optional)
|
|
53
53
|
|
|
54
|
-
Searching
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
path — documents uploaded via browser carry a *relative* `ruta_archivo`, and
|
|
59
|
-
the real path is `$RAGFLY_ROOT + ruta_archivo`.
|
|
54
|
+
Searching and citing need no local path setup. A document result may include an
|
|
55
|
+
`fs` object. For a local relative file, read `fs.home_var` and join that
|
|
56
|
+
environment variable's value with `fs.relative_path`. Each root has its own
|
|
57
|
+
variable; configure only the roots available on this machine.
|
|
60
58
|
|
|
61
59
|
```bash
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# "/MisDocumentos/letras/cancion.txt" → /Users/ana/Dropbox/MisDocumentos/letras/cancion.txt
|
|
60
|
+
export RAGFLY_HOME_442681="/Users/ana/Dropbox"
|
|
61
|
+
export RAGFLY_HOME_991203="/Volumes/Archive"
|
|
65
62
|
```
|
|
66
63
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
If `fs.is_cloud_only` is true, use the authorized cloud provider. If
|
|
65
|
+
`fs.is_public_url` is true, open its public URL. If `fs.is_absolute` is true,
|
|
66
|
+
use the absolute path on the machine where it exists. If `fs.home_var` is null
|
|
67
|
+
or unset, do not guess a local root. See the
|
|
68
|
+
[MCP guide](https://ragfly.ai/build/mcp#opening-a-document-on-disk-fs-block)
|
|
69
|
+
for the full order and examples.
|
|
70
70
|
|
|
71
71
|
## Command surface
|
|
72
72
|
|
|
@@ -78,10 +78,12 @@ ragfly
|
|
|
78
78
|
├── group list | switch | clear (signed-in person)
|
|
79
79
|
├── api-key create | list | revoke (signed-in person)
|
|
80
80
|
├── document list | show | edges
|
|
81
|
-
├── space list | show
|
|
81
|
+
├── space list | show | compose | read | refresh | promote
|
|
82
82
|
├── queue show | runs
|
|
83
83
|
├── skill list | show | run
|
|
84
84
|
├── catalog
|
|
85
|
+
├── document-type list
|
|
86
|
+
├── characteristic list
|
|
85
87
|
├── function show
|
|
86
88
|
├── search
|
|
87
89
|
├── chat ask
|
|
@@ -112,6 +114,8 @@ Output is English end to end (`--status VECTORIZED`, `-o json | jq`).
|
|
|
112
114
|
ragfly cloud document list --status VECTORIZED --limit 20 -o id
|
|
113
115
|
ragfly cloud skill run SUMMARIZE_DOCUMENT --space 12
|
|
114
116
|
ragfly cloud search "Q1 revenue"
|
|
117
|
+
ragfly cloud search "statements" --filter @filter.json --space 12
|
|
118
|
+
ragfly cloud chat ask "How do I release the entity of my key?" --mode help
|
|
115
119
|
```
|
|
116
120
|
|
|
117
121
|
Full reference: <https://api.ragfly.ai/docs>.
|
|
@@ -29,24 +29,24 @@ export RAGFLY_API_KEY=rf_xxxxxxxxxx
|
|
|
29
29
|
`cloud group` and `cloud api-key` manage a person's session and credentials: they
|
|
30
30
|
need `ragfly login`, and the server refuses them for an API key.
|
|
31
31
|
|
|
32
|
-
##
|
|
32
|
+
## Opening an original file on disk (optional)
|
|
33
33
|
|
|
34
|
-
Searching
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
path — documents uploaded via browser carry a *relative* `ruta_archivo`, and
|
|
39
|
-
the real path is `$RAGFLY_ROOT + ruta_archivo`.
|
|
34
|
+
Searching and citing need no local path setup. A document result may include an
|
|
35
|
+
`fs` object. For a local relative file, read `fs.home_var` and join that
|
|
36
|
+
environment variable's value with `fs.relative_path`. Each root has its own
|
|
37
|
+
variable; configure only the roots available on this machine.
|
|
40
38
|
|
|
41
39
|
```bash
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# "/MisDocumentos/letras/cancion.txt" → /Users/ana/Dropbox/MisDocumentos/letras/cancion.txt
|
|
40
|
+
export RAGFLY_HOME_442681="/Users/ana/Dropbox"
|
|
41
|
+
export RAGFLY_HOME_991203="/Volumes/Archive"
|
|
45
42
|
```
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
If `fs.is_cloud_only` is true, use the authorized cloud provider. If
|
|
45
|
+
`fs.is_public_url` is true, open its public URL. If `fs.is_absolute` is true,
|
|
46
|
+
use the absolute path on the machine where it exists. If `fs.home_var` is null
|
|
47
|
+
or unset, do not guess a local root. See the
|
|
48
|
+
[MCP guide](https://ragfly.ai/build/mcp#opening-a-document-on-disk-fs-block)
|
|
49
|
+
for the full order and examples.
|
|
50
50
|
|
|
51
51
|
## Command surface
|
|
52
52
|
|
|
@@ -58,10 +58,12 @@ ragfly
|
|
|
58
58
|
├── group list | switch | clear (signed-in person)
|
|
59
59
|
├── api-key create | list | revoke (signed-in person)
|
|
60
60
|
├── document list | show | edges
|
|
61
|
-
├── space list | show
|
|
61
|
+
├── space list | show | compose | read | refresh | promote
|
|
62
62
|
├── queue show | runs
|
|
63
63
|
├── skill list | show | run
|
|
64
64
|
├── catalog
|
|
65
|
+
├── document-type list
|
|
66
|
+
├── characteristic list
|
|
65
67
|
├── function show
|
|
66
68
|
├── search
|
|
67
69
|
├── chat ask
|
|
@@ -92,6 +94,8 @@ Output is English end to end (`--status VECTORIZED`, `-o json | jq`).
|
|
|
92
94
|
ragfly cloud document list --status VECTORIZED --limit 20 -o id
|
|
93
95
|
ragfly cloud skill run SUMMARIZE_DOCUMENT --space 12
|
|
94
96
|
ragfly cloud search "Q1 revenue"
|
|
97
|
+
ragfly cloud search "statements" --filter @filter.json --space 12
|
|
98
|
+
ragfly cloud chat ask "How do I release the entity of my key?" --mode help
|
|
95
99
|
```
|
|
96
100
|
|
|
97
101
|
Full reference: <https://api.ragfly.ai/docs>.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ragfly-cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.20.0"
|
|
8
8
|
description = "RAGfly CLI — operate RAGfly from the terminal and CI (login + cloud API). Lightweight, no desktop dependencies."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.20.0"
|
|
@@ -41,9 +41,8 @@ def default_headers(
|
|
|
41
41
|
Returns:
|
|
42
42
|
dict de headers listo para pasar a httpx.
|
|
43
43
|
|
|
44
|
-
El header `X-Override-Grupo` se envía
|
|
45
|
-
|
|
46
|
-
que el dropdown de grupo del frontend web).
|
|
44
|
+
El header `X-Override-Grupo` solo se envía para una sesión humana. Una API
|
|
45
|
+
key tiene un grupo fijo en el servidor y nunca hereda el grupo local del CLI.
|
|
47
46
|
"""
|
|
48
47
|
from ragfly_cli import __version__ as _client_version
|
|
49
48
|
|
|
@@ -53,14 +52,17 @@ def default_headers(
|
|
|
53
52
|
if content_type:
|
|
54
53
|
headers["Content-Type"] = "application/json"
|
|
55
54
|
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
# El override pertenece a la sesión humana. Enviar el grupo guardado en
|
|
56
|
+
# otra sesión con una API key puede dar 403 y nunca debe cambiar su alcance.
|
|
57
|
+
grupo = None
|
|
58
|
+
if not (token and token.startswith(("rf_", "slm_live_"))):
|
|
59
|
+
grupo = grupo_override
|
|
60
|
+
if grupo is None:
|
|
61
|
+
try:
|
|
62
|
+
from .config import get_config
|
|
63
|
+
grupo = get_config().codigo_grupo or None
|
|
64
|
+
except Exception:
|
|
65
|
+
grupo = None
|
|
64
66
|
if grupo:
|
|
65
67
|
headers["X-Override-Grupo"] = grupo
|
|
66
68
|
|
|
@@ -236,6 +236,32 @@ def cloud_me(output: str):
|
|
|
236
236
|
("Active group", "group"), ("Active entity", "entity")])
|
|
237
237
|
|
|
238
238
|
|
|
239
|
+
# ── cloud entity ────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
@cloud.group("entity")
|
|
242
|
+
def cloud_entity():
|
|
243
|
+
"""Active entity focus for the authenticated credential."""
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@cloud_entity.command("set")
|
|
247
|
+
@click.argument("entity_code", required=False)
|
|
248
|
+
@click.option("--release", is_flag=True, help="Release the focus and use all authorized entities.")
|
|
249
|
+
@click.option("-o", "--output", type=OUTPUT_TABLE, default="table")
|
|
250
|
+
def cloud_entity_set(entity_code: str | None, release: bool, output: str):
|
|
251
|
+
"""Set ENTITY_CODE as active, or use --release to clear the focus."""
|
|
252
|
+
if release == (entity_code is not None):
|
|
253
|
+
raise click.UsageError("Pass exactly one of ENTITY_CODE or --release.")
|
|
254
|
+
data = _v1(
|
|
255
|
+
"POST", "/v1/session/active-entity",
|
|
256
|
+
body={"entity_code": None if release else entity_code},
|
|
257
|
+
)
|
|
258
|
+
if output == "json":
|
|
259
|
+
_emit_json(data)
|
|
260
|
+
return
|
|
261
|
+
active = data.get("active_entity") or "all authorized entities"
|
|
262
|
+
console.print(f"[green]✓ Active entity:[/green] [bold]{active}[/bold]")
|
|
263
|
+
|
|
264
|
+
|
|
239
265
|
# ── cloud group (signed-in person only) ──────────────────────────────────────
|
|
240
266
|
|
|
241
267
|
@cloud.group("group")
|
|
@@ -413,6 +439,48 @@ def cloud_space_show(space_id: int, document_limit: int, output: str):
|
|
|
413
439
|
_table("Documents", data.get("documents") or [], [("Code", "code"), ("Name", "name"), ("Status", "status")])
|
|
414
440
|
|
|
415
441
|
|
|
442
|
+
@cloud_space.command("compose")
|
|
443
|
+
@click.argument("operation", type=click.Choice(["union", "intersection", "difference", "symmetric_difference"]))
|
|
444
|
+
@click.argument("space_a", type=int)
|
|
445
|
+
@click.argument("space_b", type=int)
|
|
446
|
+
@click.option("--name", default="", help="Name of the result (generated when empty)")
|
|
447
|
+
@click.option("--type", "space_type", type=click.Choice(["AREA", "SPACE"]), default="AREA", show_default=True)
|
|
448
|
+
@click.option("-o", "--output", type=OUTPUT_TABLE, default="json")
|
|
449
|
+
def cloud_space_compose(operation: str, space_a: int, space_b: int, name: str, space_type: str, output: str):
|
|
450
|
+
"""Combine two working spaces by set algebra into a new one."""
|
|
451
|
+
data = _v1("POST", "/v1/spaces/compose", body={
|
|
452
|
+
"operation": operation, "space_id_a": space_a, "space_id_b": space_b,
|
|
453
|
+
"name": name, "space_type": space_type,
|
|
454
|
+
})
|
|
455
|
+
_emit_json(data) if output == "json" else _fields("New working space", data, [
|
|
456
|
+
("ID", "space_id"), ("Name", "name"), ("Type", "type"), ("Documents", "total_documents")])
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@cloud_space.command("read")
|
|
460
|
+
@click.argument("space_id", type=int)
|
|
461
|
+
@click.option("--resolution", type=click.Choice(["count", "manifest", "chunks", "text"]), default="manifest", show_default=True)
|
|
462
|
+
@click.option("--query", default="", help="Sub-question to rank by (required with --resolution chunks)")
|
|
463
|
+
@click.option("--limit", type=int, default=50, show_default=True)
|
|
464
|
+
def cloud_space_read(space_id: int, resolution: str, query: str, limit: int):
|
|
465
|
+
"""Read a working space at the chosen resolution (JSON output)."""
|
|
466
|
+
_emit_json(_v1("POST", f"/v1/spaces/{space_id}/read",
|
|
467
|
+
body={"resolution": resolution, "query": query, "limit": limit}))
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@cloud_space.command("refresh")
|
|
471
|
+
@click.argument("space_id", type=int)
|
|
472
|
+
def cloud_space_refresh(space_id: int):
|
|
473
|
+
"""Recompute the members of a working space (JSON output)."""
|
|
474
|
+
_emit_json(_v1("POST", f"/v1/spaces/{space_id}/refresh"))
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@cloud_space.command("promote")
|
|
478
|
+
@click.argument("space_id", type=int)
|
|
479
|
+
def cloud_space_promote(space_id: int):
|
|
480
|
+
"""Turn a temporary working space into a permanent one (JSON output)."""
|
|
481
|
+
_emit_json(_v1("POST", f"/v1/spaces/{space_id}/promote"))
|
|
482
|
+
|
|
483
|
+
|
|
416
484
|
# ── cloud queue ──────────────────────────────────────────────────────────────
|
|
417
485
|
|
|
418
486
|
@cloud.group("queue")
|
|
@@ -508,6 +576,38 @@ def cloud_catalog(type_: str, output: str):
|
|
|
508
576
|
_table("Skills", data["skills"], [("Code", "code"), ("Name", "name"), ("Applies to", "applies_to")])
|
|
509
577
|
|
|
510
578
|
|
|
579
|
+
@cloud.group("document-type")
|
|
580
|
+
def cloud_document_type():
|
|
581
|
+
"""Document types of an entity: the codes `search --filter` accepts."""
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
@cloud_document_type.command("list")
|
|
585
|
+
@click.option("--entity", default=None, help="Entity code (defaults to the active entity)")
|
|
586
|
+
@click.option("-o", "--output", type=OUTPUT_LIST, default="table")
|
|
587
|
+
def cloud_document_type_list(entity: str | None, output: str):
|
|
588
|
+
"""List the document types of the entity, with their parent type."""
|
|
589
|
+
data = _v1("GET", "/v1/catalog/document-types", params={"entity_code": entity})
|
|
590
|
+
_list(data, output, key="document_types", id_key="code", title="Document types",
|
|
591
|
+
columns=[("Code", "code"), ("Name", "name"), ("Parent", "parent_code"), ("Documents", "document_count")])
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@cloud.group("characteristic")
|
|
595
|
+
def cloud_characteristic():
|
|
596
|
+
"""Content characteristics of the document types: the codes `search --filter` accepts."""
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
@cloud_characteristic.command("list")
|
|
600
|
+
@click.option("--document-type", "document_types", multiple=True, help="Document type code (repeat for several)")
|
|
601
|
+
@click.option("--entity", default=None, help="Entity code (defaults to the active entity)")
|
|
602
|
+
@click.option("-o", "--output", type=OUTPUT_LIST, default="table")
|
|
603
|
+
def cloud_characteristic_list(document_types: tuple[str, ...], entity: str | None, output: str):
|
|
604
|
+
"""List the characteristics available for those types (all when none is given)."""
|
|
605
|
+
data = _v1("GET", "/v1/catalog/characteristics", params={
|
|
606
|
+
"document_types": ",".join(document_types) or None, "entity_code": entity})
|
|
607
|
+
_list(data, output, key="characteristics", id_key="code", title="Characteristics",
|
|
608
|
+
columns=[("Category", "category_code"), ("Code", "code"), ("Name", "name"), ("Role", "role")])
|
|
609
|
+
|
|
610
|
+
|
|
511
611
|
@cloud.group("function")
|
|
512
612
|
def cloud_function():
|
|
513
613
|
"""Functions (screens) of the RAGfly application."""
|
|
@@ -531,8 +631,12 @@ def cloud_function_show(function_code: str, output: str):
|
|
|
531
631
|
@click.option("--limit", type=int, default=10, show_default=True)
|
|
532
632
|
@click.option("--min-similarity", type=float, default=0.0, show_default=True)
|
|
533
633
|
@click.option("--entity", default=None, help="Filter by entity code")
|
|
634
|
+
@click.option("--space", "space_id", type=int, default=None, help="Search only inside this working space")
|
|
635
|
+
@click.option("--filter", "filter_json", default=None,
|
|
636
|
+
help="Structured filter as JSON, @file or - (document_types, attributes, characteristics)")
|
|
534
637
|
@click.option("-o", "--output", type=OUTPUT_TABLE, default="table")
|
|
535
|
-
def cloud_search(query: tuple[str, ...], limit: int, min_similarity: float, entity: str | None,
|
|
638
|
+
def cloud_search(query: tuple[str, ...], limit: int, min_similarity: float, entity: str | None,
|
|
639
|
+
space_id: int | None, filter_json: str | None, output: str):
|
|
536
640
|
"""Semantic search over the group's vectorized documents."""
|
|
537
641
|
text = " ".join(query).strip()
|
|
538
642
|
if not text:
|
|
@@ -540,6 +644,10 @@ def cloud_search(query: tuple[str, ...], limit: int, min_similarity: float, enti
|
|
|
540
644
|
body = {"query": text, "limit": limit, "min_similarity": min_similarity}
|
|
541
645
|
if entity:
|
|
542
646
|
body["entity_code"] = entity
|
|
647
|
+
if space_id is not None:
|
|
648
|
+
body["space_id"] = space_id
|
|
649
|
+
if filter_json:
|
|
650
|
+
body["filter"] = _json_argument(filter_json, "--filter")
|
|
543
651
|
data = _v1("POST", "/v1/documents/search", body=body)
|
|
544
652
|
if output == "json":
|
|
545
653
|
_emit_json(data)
|
|
@@ -561,8 +669,10 @@ def cloud_chat():
|
|
|
561
669
|
@click.argument("message", nargs=-1, required=True)
|
|
562
670
|
@click.option("--function", "function_code", default="CHAT-USER", show_default=True, help="Chat function code")
|
|
563
671
|
@click.option("--conversation", "conversation_id", type=int, default=None, help="Continue an existing conversation")
|
|
672
|
+
@click.option("--mode", type=click.Choice(["default", "help"]), default=None,
|
|
673
|
+
help="help: answer questions about RAGfly itself (how to use or integrate it), without links to web screens")
|
|
564
674
|
@click.option("-o", "--output", type=click.Choice(["text", "json"]), default="text")
|
|
565
|
-
def cloud_chat_ask(message: tuple[str, ...], function_code: str, conversation_id: int | None, output: str):
|
|
675
|
+
def cloud_chat_ask(message: tuple[str, ...], function_code: str, conversation_id: int | None, mode: str | None, output: str):
|
|
566
676
|
"""Ask a question. Creates a conversation unless --conversation is given."""
|
|
567
677
|
question = " ".join(message).strip()
|
|
568
678
|
if not question:
|
|
@@ -570,6 +680,8 @@ def cloud_chat_ask(message: tuple[str, ...], function_code: str, conversation_id
|
|
|
570
680
|
body = {"question": question, "function_code": function_code}
|
|
571
681
|
if conversation_id:
|
|
572
682
|
body["conversation_id"] = conversation_id
|
|
683
|
+
if mode:
|
|
684
|
+
body["mode"] = mode
|
|
573
685
|
data = _v1("POST", "/v1/ask", body=body)
|
|
574
686
|
if output == "json":
|
|
575
687
|
_emit_json(data)
|
|
@@ -49,7 +49,7 @@ def set_grupo_activo(codigo_grupo: str) -> None:
|
|
|
49
49
|
path = _config_path()
|
|
50
50
|
if not path.exists():
|
|
51
51
|
raise FileNotFoundError(
|
|
52
|
-
f"
|
|
52
|
+
f"{path} does not exist. Run `ragfly setup` first."
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
content = path.read_text()
|
|
@@ -74,10 +74,10 @@ class CliCommand:
|
|
|
74
74
|
except SystemExit:
|
|
75
75
|
raise
|
|
76
76
|
except KeyboardInterrupt:
|
|
77
|
-
self.aviso("
|
|
77
|
+
self.aviso("Interrupted.")
|
|
78
78
|
raise SystemExit(130)
|
|
79
79
|
except Exception as e:
|
|
80
|
-
self.error(f"
|
|
80
|
+
self.error(f"Unexpected error: {e}")
|
|
81
81
|
raise SystemExit(on_unexpected_exit_code)
|
|
82
82
|
|
|
83
83
|
def salir(self, mensaje: str, exit_code: int = 1) -> NoReturn:
|
|
@@ -9,7 +9,7 @@ Encapsula el patrón repetido en `cloud_commands.py`:
|
|
|
9
9
|
except httpx.HTTPStatusError as e:
|
|
10
10
|
_manejar_http_error(e)
|
|
11
11
|
except httpx.RequestError as e:
|
|
12
|
-
raise CloudError(f"
|
|
12
|
+
raise CloudError(f"Could not connect to the server: {e}", exit_code=2)
|
|
13
13
|
|
|
14
14
|
Ejemplo:
|
|
15
15
|
from ragfly_cli.oop import CloudHttpClient
|
|
@@ -99,7 +99,7 @@ class CloudHttpClient:
|
|
|
99
99
|
except httpx.HTTPStatusError as e:
|
|
100
100
|
_manejar_http_error(e)
|
|
101
101
|
except httpx.RequestError as e:
|
|
102
|
-
raise CloudError(f"
|
|
102
|
+
raise CloudError(f"Could not connect to the server: {e}", exit_code=2)
|
|
103
103
|
|
|
104
104
|
# ── Verbos HTTP ───────────────────────────────────────────────────────────
|
|
105
105
|
|
|
@@ -88,9 +88,9 @@ def chequear_actualizacion(
|
|
|
88
88
|
|
|
89
89
|
obligatoria = data.get("obligatoria", False)
|
|
90
90
|
notas = data.get("notas") or ""
|
|
91
|
-
prefix = "⚠️
|
|
91
|
+
prefix = "⚠️ MANDATORY UPDATE" if obligatoria else "ℹ️ A new version is available"
|
|
92
92
|
|
|
93
|
-
msg = f"{prefix}: RAGfly Desktop v{latest} (
|
|
93
|
+
msg = f"{prefix}: RAGfly Desktop v{latest} (your version: v{__version__})"
|
|
94
94
|
if notas:
|
|
95
95
|
msg += f"\n {notas}"
|
|
96
96
|
return msg
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ragfly-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.20.0
|
|
4
4
|
Summary: RAGfly CLI — operate RAGfly from the terminal and CI (login + cloud API). Lightweight, no desktop dependencies.
|
|
5
5
|
Author: CAB Ltda
|
|
6
6
|
License: MIT
|
|
@@ -49,24 +49,24 @@ export RAGFLY_API_KEY=rf_xxxxxxxxxx
|
|
|
49
49
|
`cloud group` and `cloud api-key` manage a person's session and credentials: they
|
|
50
50
|
need `ragfly login`, and the server refuses them for an API key.
|
|
51
51
|
|
|
52
|
-
##
|
|
52
|
+
## Opening an original file on disk (optional)
|
|
53
53
|
|
|
54
|
-
Searching
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
path — documents uploaded via browser carry a *relative* `ruta_archivo`, and
|
|
59
|
-
the real path is `$RAGFLY_ROOT + ruta_archivo`.
|
|
54
|
+
Searching and citing need no local path setup. A document result may include an
|
|
55
|
+
`fs` object. For a local relative file, read `fs.home_var` and join that
|
|
56
|
+
environment variable's value with `fs.relative_path`. Each root has its own
|
|
57
|
+
variable; configure only the roots available on this machine.
|
|
60
58
|
|
|
61
59
|
```bash
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# "/MisDocumentos/letras/cancion.txt" → /Users/ana/Dropbox/MisDocumentos/letras/cancion.txt
|
|
60
|
+
export RAGFLY_HOME_442681="/Users/ana/Dropbox"
|
|
61
|
+
export RAGFLY_HOME_991203="/Volumes/Archive"
|
|
65
62
|
```
|
|
66
63
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
If `fs.is_cloud_only` is true, use the authorized cloud provider. If
|
|
65
|
+
`fs.is_public_url` is true, open its public URL. If `fs.is_absolute` is true,
|
|
66
|
+
use the absolute path on the machine where it exists. If `fs.home_var` is null
|
|
67
|
+
or unset, do not guess a local root. See the
|
|
68
|
+
[MCP guide](https://ragfly.ai/build/mcp#opening-a-document-on-disk-fs-block)
|
|
69
|
+
for the full order and examples.
|
|
70
70
|
|
|
71
71
|
## Command surface
|
|
72
72
|
|
|
@@ -78,10 +78,12 @@ ragfly
|
|
|
78
78
|
├── group list | switch | clear (signed-in person)
|
|
79
79
|
├── api-key create | list | revoke (signed-in person)
|
|
80
80
|
├── document list | show | edges
|
|
81
|
-
├── space list | show
|
|
81
|
+
├── space list | show | compose | read | refresh | promote
|
|
82
82
|
├── queue show | runs
|
|
83
83
|
├── skill list | show | run
|
|
84
84
|
├── catalog
|
|
85
|
+
├── document-type list
|
|
86
|
+
├── characteristic list
|
|
85
87
|
├── function show
|
|
86
88
|
├── search
|
|
87
89
|
├── chat ask
|
|
@@ -112,6 +114,8 @@ Output is English end to end (`--status VECTORIZED`, `-o json | jq`).
|
|
|
112
114
|
ragfly cloud document list --status VECTORIZED --limit 20 -o id
|
|
113
115
|
ragfly cloud skill run SUMMARIZE_DOCUMENT --space 12
|
|
114
116
|
ragfly cloud search "Q1 revenue"
|
|
117
|
+
ragfly cloud search "statements" --filter @filter.json --space 12
|
|
118
|
+
ragfly cloud chat ask "How do I release the entity of my key?" --mode help
|
|
115
119
|
```
|
|
116
120
|
|
|
117
121
|
Full reference: <https://api.ragfly.ai/docs>.
|
|
@@ -20,4 +20,6 @@ ragfly_cli.egg-info/top_level.txt
|
|
|
20
20
|
ragfly_cli/oop/__init__.py
|
|
21
21
|
ragfly_cli/oop/cli_command.py
|
|
22
22
|
ragfly_cli/oop/http_client.py
|
|
23
|
-
tests/test_agent_context_cli.py
|
|
23
|
+
tests/test_agent_context_cli.py
|
|
24
|
+
tests/test_api_key_group_override.py
|
|
25
|
+
tests/test_surface_parity_cli.py
|
|
@@ -47,6 +47,19 @@ def test_every_cloud_call_goes_to_v1_with_the_cli_surface(wire):
|
|
|
47
47
|
assert call["headers"]["X-RAGfly-Client"] == "cli"
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
def test_entity_set_and_release_use_the_single_public_session_operation(wire):
|
|
51
|
+
wire.reply = httpx.Response(200, json={"active_entity": "E1"})
|
|
52
|
+
result = run("cloud", "entity", "set", "E1", "-o", "json")
|
|
53
|
+
assert result.exit_code == 0, result.output
|
|
54
|
+
assert wire.calls[0]["url"].endswith("/v1/session/active-entity")
|
|
55
|
+
assert wire.calls[0]["json"] == {"entity_code": "E1"}
|
|
56
|
+
|
|
57
|
+
wire.reply = httpx.Response(200, json={"active_entity": None})
|
|
58
|
+
result = run("cloud", "entity", "set", "--release", "-o", "json")
|
|
59
|
+
assert result.exit_code == 0, result.output
|
|
60
|
+
assert wire.calls[1]["json"] == {"entity_code": None}
|
|
61
|
+
|
|
62
|
+
|
|
50
63
|
def test_agent_context_and_tool(wire):
|
|
51
64
|
wire.reply = httpx.Response(200, json={"function_profile": "support_chat", "system_prompt_hash": "abc", "tools": []})
|
|
52
65
|
result = run("cloud", "agent", "context", "--profile", "support_chat")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
|
|
3
|
+
from ragfly_cli import config
|
|
4
|
+
from ragfly_cli._http import default_headers
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_api_key_never_inherits_a_saved_group(monkeypatch):
|
|
8
|
+
monkeypatch.setattr(config, "get_config", lambda: SimpleNamespace(codigo_grupo="OTHER-GROUP"))
|
|
9
|
+
for token in ("rf_fixture", "slm_live_fixture"):
|
|
10
|
+
headers = default_headers(token=token)
|
|
11
|
+
assert headers["Authorization"] == f"Bearer {token}"
|
|
12
|
+
assert "X-Override-Grupo" not in headers
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_human_session_keeps_its_saved_group(monkeypatch):
|
|
16
|
+
monkeypatch.setattr(config, "get_config", lambda: SimpleNamespace(codigo_grupo="MY-GROUP"))
|
|
17
|
+
headers = default_headers(token="jwt.header.signature")
|
|
18
|
+
assert headers["X-Override-Grupo"] == "MY-GROUP"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""The CLI exposes the same /v1 operations as the SDKs and MCP: ask mode, filtered search, catalogs, spaces."""
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
import pytest
|
|
6
|
+
from click.testing import CliRunner
|
|
7
|
+
|
|
8
|
+
from ragfly_cli.cli import app
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def wire(monkeypatch):
|
|
13
|
+
monkeypatch.setenv("RAGFLY_API_KEY", "rf_test")
|
|
14
|
+
monkeypatch.delenv("RAGFLY_TOKEN", raising=False)
|
|
15
|
+
monkeypatch.setattr("ragfly_cli.config.get_config", lambda: type("C", (), {"codigo_grupo": ""})())
|
|
16
|
+
|
|
17
|
+
class Wire:
|
|
18
|
+
reply = httpx.Response(200, json={})
|
|
19
|
+
calls: list = []
|
|
20
|
+
|
|
21
|
+
def fake_request(method, url, **kwargs):
|
|
22
|
+
Wire.calls.append({"method": method, "url": url, **kwargs})
|
|
23
|
+
response = Wire.reply
|
|
24
|
+
response.request = httpx.Request(method, url)
|
|
25
|
+
return response
|
|
26
|
+
|
|
27
|
+
monkeypatch.setattr(httpx, "request", fake_request)
|
|
28
|
+
Wire.calls = []
|
|
29
|
+
return Wire
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run(*args):
|
|
33
|
+
return CliRunner().invoke(app, list(args))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_ask_sends_mode_only_when_given(wire):
|
|
37
|
+
wire.reply = httpx.Response(200, json={"answer": "A", "conversation_id": 1})
|
|
38
|
+
assert run("cloud", "chat", "ask", "how do I release the entity?", "--mode", "help").exit_code == 0
|
|
39
|
+
assert wire.calls[0]["json"] == {"question": "how do I release the entity?", "function_code": "CHAT-USER", "mode": "help"}
|
|
40
|
+
assert run("cloud", "chat", "ask", "hello").exit_code == 0
|
|
41
|
+
assert "mode" not in wire.calls[1]["json"]
|
|
42
|
+
assert run("cloud", "chat", "ask", "hello", "--mode", "support").exit_code != 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_search_takes_space_and_structured_filter(wire):
|
|
46
|
+
wire.reply = httpx.Response(200, json={"documents": []})
|
|
47
|
+
flt = {"document_types": ["TDOC_cartola"], "attributes": [{"field": "language", "operator": "EQ", "value": "es"}]}
|
|
48
|
+
result = run("cloud", "search", "cartolas", "--space", "12", "--filter", json.dumps(flt), "-o", "json")
|
|
49
|
+
assert result.exit_code == 0, result.output
|
|
50
|
+
body = wire.calls[0]["json"]
|
|
51
|
+
assert wire.calls[0]["url"].endswith("/v1/documents/search")
|
|
52
|
+
assert body["query"] == "cartolas" and body["space_id"] == 12 and body["filter"] == flt
|
|
53
|
+
assert run("cloud", "search", "x", "--filter", "[1]").exit_code != 0 # a filter is a JSON object
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_catalog_commands_call_the_public_catalog_routes(wire):
|
|
57
|
+
wire.reply = httpx.Response(200, json={"document_types": [{"code": "T1", "name": "Statement", "parent_code": None, "document_count": 3}]})
|
|
58
|
+
assert run("cloud", "document-type", "list", "--entity", "E1", "-o", "id").output.strip() == "T1"
|
|
59
|
+
assert wire.calls[0]["url"].endswith("/v1/catalog/document-types") and wire.calls[0]["params"] == {"entity_code": "E1"}
|
|
60
|
+
wire.reply = httpx.Response(200, json={"characteristics": []})
|
|
61
|
+
assert run("cloud", "characteristic", "list", "--document-type", "T1", "--document-type", "T2", "-o", "json").exit_code == 0
|
|
62
|
+
assert wire.calls[1]["url"].endswith("/v1/catalog/characteristics")
|
|
63
|
+
assert wire.calls[1]["params"] == {"document_types": "T1,T2"}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_space_commands_cover_compose_read_refresh_and_promote(wire):
|
|
67
|
+
wire.reply = httpx.Response(200, json={"space_id": 9})
|
|
68
|
+
assert run("cloud", "space", "compose", "difference", "1", "2", "--name", "N", "--type", "SPACE").exit_code == 0
|
|
69
|
+
assert wire.calls[0]["url"].endswith("/v1/spaces/compose")
|
|
70
|
+
assert wire.calls[0]["json"] == {"operation": "difference", "space_id_a": 1, "space_id_b": 2, "name": "N", "space_type": "SPACE"}
|
|
71
|
+
assert run("cloud", "space", "read", "9", "--resolution", "chunks", "--query", "q", "--limit", "5").exit_code == 0
|
|
72
|
+
assert wire.calls[1]["url"].endswith("/v1/spaces/9/read")
|
|
73
|
+
assert wire.calls[1]["json"] == {"resolution": "chunks", "query": "q", "limit": 5}
|
|
74
|
+
assert run("cloud", "space", "refresh", "9").exit_code == 0 and wire.calls[2]["url"].endswith("/v1/spaces/9/refresh")
|
|
75
|
+
assert run("cloud", "space", "promote", "9").exit_code == 0 and wire.calls[3]["url"].endswith("/v1/spaces/9/promote")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.19.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|