rosetta-cli 3.0.4__tar.gz → 3.0.6__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.
Files changed (33) hide show
  1. {rosetta_cli-3.0.4/rosetta_cli.egg-info → rosetta_cli-3.0.6}/PKG-INFO +1 -1
  2. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/pyproject.toml +1 -1
  3. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/cli.py +2 -1
  4. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/commands/cleanup_command.py +10 -1
  5. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/commands/parse_command.py +43 -44
  6. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/ragflow_client.py +33 -15
  7. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/rosetta_config.py +49 -15
  8. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/rosetta_publisher.py +8 -3
  9. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/services/dataset_service.py +16 -3
  10. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6/rosetta_cli.egg-info}/PKG-INFO +1 -1
  11. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/MANIFEST.in +0 -0
  12. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/README.md +0 -0
  13. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/env.template +0 -0
  14. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/__init__.py +0 -0
  15. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/__main__.py +0 -0
  16. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/commands/__init__.py +0 -0
  17. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/commands/base_command.py +0 -0
  18. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/commands/list_command.py +0 -0
  19. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/commands/publish_command.py +0 -0
  20. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/commands/verify_command.py +0 -0
  21. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/rosetta_auth.py +0 -0
  22. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/rosetta_utils.py +0 -0
  23. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/services/__init__.py +0 -0
  24. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/services/auth_service.py +0 -0
  25. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/services/document_data.py +0 -0
  26. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/services/document_service.py +0 -0
  27. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli/typing_utils.py +0 -0
  28. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli.egg-info/SOURCES.txt +0 -0
  29. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli.egg-info/dependency_links.txt +0 -0
  30. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli.egg-info/entry_points.txt +0 -0
  31. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli.egg-info/requires.txt +0 -0
  32. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/rosetta_cli.egg-info/top_level.txt +0 -0
  33. {rosetta_cli-3.0.4 → rosetta_cli-3.0.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rosetta-cli
3
- Version: 3.0.4
3
+ Version: 3.0.6
4
4
  Summary: Rosetta CLI for publishing knowledge base content to RAGFlow
5
5
  Author-email: Rosetta Team <rosetta-support@griddynamics.com>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rosetta-cli"
7
- version = "3.0.4"
7
+ version = "3.0.6"
8
8
  description = "Rosetta CLI for publishing knowledge base content to RAGFlow"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -378,7 +378,8 @@ Frontmatter Metadata (publish flow):
378
378
  chunk_method=config.chunk_method,
379
379
  parser_config=config.parser_config,
380
380
  page_size=config.page_size,
381
- timeout=config.timeout
381
+ dataset_default=config.dataset_default,
382
+ timeout=config.timeout,
382
383
  )
383
384
 
384
385
  return execute_command(args.command, args, client, config)
@@ -87,7 +87,16 @@ class CleanupCommand(BaseCommand):
87
87
  dataset_service: DatasetService,
88
88
  args: CommandArgs,
89
89
  ) -> list[DocumentLike]:
90
- """Get documents to delete based on filters."""
90
+ """Get documents to delete based on filters.
91
+
92
+ Accepted limitation: every branch below reads one page, bounded by
93
+ RAGFLOW_PAGE_SIZE (default 1000). On a dataset larger than that, this
94
+ command deletes the documents on the first page and reports success -
95
+ the remainder is left in place with no warning. The instruction set is
96
+ not expected to approach the cap, and this command serves the optional
97
+ MCP publishing pipeline rather than plugin delivery. Anyone pointing
98
+ `--dataset` at a larger dataset should know the delete is partial.
99
+ """
91
100
  document_service = DocumentService(self.client)
92
101
 
93
102
  if args.tags:
@@ -11,7 +11,7 @@ from ..services.auth_service import AuthService
11
11
 
12
12
 
13
13
  from .base_command import BaseCommand
14
- from ..typing_utils import CommandArgs, DatasetLike, JsonDict
14
+ from ..typing_utils import CommandArgs, DatasetLike, DocumentLike, JsonDict
15
15
 
16
16
 
17
17
  class ParseCommand(BaseCommand):
@@ -102,58 +102,57 @@ class ParseCommand(BaseCommand):
102
102
  traceback.print_exc()
103
103
  return 1
104
104
 
105
+ # RAGFlow `run` values that mean a document still needs parsing.
106
+ PARSE_REQUIRED_STATUSES = ("FAIL", "UNSTART", "CANCEL")
107
+
105
108
  def _get_documents_to_parse(self, dataset: DatasetLike, args: CommandArgs) -> tuple[list[JsonDict], dict[str, int]]:
106
109
  """Get documents that need parsing."""
107
- document_service = DocumentService(self.client)
108
110
  docs_to_parse: list[JsonDict] = []
109
111
  status_counts = {'done': 0, 'running': 0}
110
112
 
111
113
  print(f"Checking parsing status for documents...")
112
114
 
115
+ # One unfiltered list call serves both modes: force selects everything,
116
+ # default partitions the same list client-side on `run`. Default mode used
117
+ # to make two calls - a server-side run= filter, plus a full list purely to
118
+ # tally done/running for the summary.
119
+ #
120
+ # Deliberate tradeoff, not a no-op: the server-filtered call surfaced up to
121
+ # page_size documents *needing parsing*, whereas page 1 of an unfiltered
122
+ # list surfaces only the needing-parse documents that happen to fall inside
123
+ # it. Once a dataset exceeds RAGFLOW_PAGE_SIZE this can therefore select
124
+ # fewer documents than before.
125
+ #
126
+ # Accepted limitation, not a deferred fix: every list path in this command
127
+ # reads page 1 only, bounded by RAGFLOW_PAGE_SIZE (default 1000). The
128
+ # instruction set is not expected to approach that cap - it is roughly 470
129
+ # files - and this command serves the optional MCP publishing pipeline,
130
+ # which is secondary to plugin delivery. The page-1-only read is also
131
+ # pre-existing: it already applied to the force branch and to the
132
+ # done/running tally.
133
+ documents = dataset.list_documents(page_size=self.config.page_size)
134
+
135
+ selected: list[tuple[DocumentLike, str]] = []
136
+ for document in documents:
137
+ status = self._count_parse_status(status_counts, document)
138
+ if args.force or status in self.PARSE_REQUIRED_STATUSES:
139
+ selected.append((document, status))
140
+
113
141
  if args.force:
114
- # Force mode: all documents
115
- documents = dataset.list_documents(page_size=self.config.page_size)
116
-
117
- print(f"Found {len(documents)} document(s) (force mode - parsing all)\n")
118
-
119
- for document in documents:
120
- status = self._count_parse_status(status_counts, document)
121
- doc_id = getattr(document, 'id', None)
122
- if doc_id:
123
- docs_to_parse.append({
124
- "id": doc_id,
125
- "name": getattr(document, 'name', 'Untitled'),
126
- "dataset_id": dataset.id,
127
- "folder": ".",
128
- "status": status
129
- })
142
+ print(f"Found {len(selected)} document(s) (force mode - parsing all)\n")
130
143
  else:
131
- # Default mode: filter by status (need parsing)
132
- documents_needing_parse = document_service.list_documents_by_status(
133
- dataset,
134
- statuses=["FAIL", "UNSTART", "CANCEL"],
135
- limit=self.config.page_size
136
- )
137
-
138
- # Tally skipped documents from the full list so the summary reflects
139
- # what this run will not re-trigger.
140
- all_documents = dataset.list_documents(page_size=self.config.page_size)
141
- for document in all_documents:
142
- self._count_parse_status(status_counts, document)
143
-
144
- print(f"Found {len(documents_needing_parse)} document(s) needing parsing\n")
145
-
146
- for document in documents_needing_parse:
147
- status = self._count_parse_status(status_counts, document)
148
- doc_id = getattr(document, 'id', None)
149
- if doc_id:
150
- docs_to_parse.append({
151
- "id": doc_id,
152
- "name": getattr(document, 'name', 'Untitled'),
153
- "dataset_id": dataset.id,
154
- "folder": ".",
155
- "status": status
156
- })
144
+ print(f"Found {len(selected)} document(s) needing parsing\n")
145
+
146
+ for document, status in selected:
147
+ doc_id = getattr(document, 'id', None)
148
+ if doc_id:
149
+ docs_to_parse.append({
150
+ "id": doc_id,
151
+ "name": getattr(document, 'name', 'Untitled'),
152
+ "dataset_id": dataset.id,
153
+ "folder": ".",
154
+ "status": status
155
+ })
157
156
 
158
157
  return docs_to_parse, status_counts
159
158
 
@@ -116,7 +116,8 @@ class RAGFlowClient:
116
116
  embedding_model: str | None = None,
117
117
  chunk_method: str = "naive",
118
118
  parser_config: JsonDict | None = None,
119
- page_size: int = 1000
119
+ page_size: int = 1000,
120
+ dataset_default: str = "aia",
120
121
  ):
121
122
  """
122
123
  Initialize RAGFlow client.
@@ -130,6 +131,7 @@ class RAGFlowClient:
130
131
  chunk_method: Chunking method (default: naive)
131
132
  parser_config: Parser configuration dict for chunk_method settings
132
133
  page_size: Default page size for list operations (default: 1000)
134
+ dataset_default: Default dataset name when no release-specific or explicit dataset name is available
133
135
 
134
136
  Raises:
135
137
  ValueError: If api_key or base_url is empty
@@ -147,6 +149,7 @@ class RAGFlowClient:
147
149
  self.chunk_method = chunk_method
148
150
  self.parser_config = parser_config or {}
149
151
  self.page_size = page_size
152
+ self.dataset_default = dataset_default
150
153
 
151
154
  # Initialize RAGFlow SDK client
152
155
  self._client = RAGFlow(api_key=api_key, base_url=base_url, version=version)
@@ -469,26 +472,38 @@ class RAGFlowClient:
469
472
  # Dataset doesn't exist, create it (gated by dry_run)
470
473
  return self.create_dataset(name, description, dry_run=dry_run)
471
474
 
472
- def _resolve_dataset_name(self, template: str, release: str | None) -> str:
475
+ def _resolve_dataset_name(
476
+ self,
477
+ dataset_template: str,
478
+ release: str | None,
479
+ dataset_name: str | None,
480
+ ) -> str:
473
481
  """
474
- Resolve dataset name from template.
482
+ Resolve dataset name using release + dataset_template, explicit dataset_name or default.
475
483
 
476
484
  Args:
477
- template: Name template (e.g., "aia-{release}")
485
+ dataset_template: Name template (e.g., "aia-{release}")
478
486
  release: Release identifier (e.g., "r1")
487
+ dataset_name: Explicit dataset name, if provided (e.g., "aia-r1")
479
488
 
480
489
  Returns:
481
490
  Resolved dataset name
482
491
 
483
492
  Examples:
484
- >>> _resolve_dataset_name("aia-{release}", "r1")
493
+ >>> _resolve_dataset_name("aia-{release}", "r1", "aia-r2")
485
494
  "aia-r1"
486
- >>> _resolve_dataset_name("aia", None)
487
- "aia"
495
+ >>> _resolve_dataset_name("aia-{release}", None, "aia-r2")
496
+ "aia-r2"
488
497
  """
489
- if release and "{release}" in template:
490
- return template.format(release=release)
491
- return template
498
+ if release and "{release}" in dataset_template:
499
+ resolved_name = dataset_template.format(release=release)
500
+ else:
501
+ resolved_name = dataset_name or self.dataset_default
502
+
503
+ if "{release}" in resolved_name:
504
+ raise ValueError(f"Unresolved release placeholder in dataset name: {resolved_name}")
505
+
506
+ return resolved_name
492
507
 
493
508
  def _build_title_with_tags(self, tags: list[str], filename: str) -> str:
494
509
  """
@@ -521,7 +536,7 @@ class RAGFlowClient:
521
536
  OPTIMIZED: Now accepts pre-read content to avoid redundant file I/O.
522
537
 
523
538
  This method:
524
- 1. Resolves dataset name from template + release
539
+ 1. Resolves the target dataset name
525
540
  2. Ensures dataset exists
526
541
  3. Builds title with tag prefixes
527
542
  4. Checks if document exists (by ims_doc_id)
@@ -529,6 +544,12 @@ class RAGFlowClient:
529
544
  6. Deletes existing document if changed
530
545
  7. Uploads new document with metadata
531
546
 
547
+ Dataset name resolution precedence:
548
+ 1. If metadata.release is present, and dataset_template contains "{release}",
549
+ resolve dataset_template with the release.
550
+ 2. Otherwise, use dataset_name when provided.
551
+ 3. Otherwise, fall back to dataset_default.
552
+
532
553
  Args:
533
554
  file_path: Path to file (for filename, backward compatibility)
534
555
  metadata: Document metadata with pre-calculated hash
@@ -567,10 +588,7 @@ class RAGFlowClient:
567
588
  actual_hash = metadata.content_hash
568
589
 
569
590
  # Resolve dataset name
570
- resolved_name = self._resolve_dataset_name(
571
- dataset_template if "{release}" in dataset_template else (dataset_name or dataset_template),
572
- metadata.release
573
- )
591
+ resolved_name = self._resolve_dataset_name(dataset_template, metadata.release, dataset_name)
574
592
 
575
593
  # Ensure dataset exists (dry_run gates the underlying create_dataset call)
576
594
  dataset = self._ensure_dataset(
@@ -9,7 +9,6 @@ import logging
9
9
  import os
10
10
  from dataclasses import dataclass
11
11
  from pathlib import Path
12
- from urllib.parse import urlparse
13
12
 
14
13
  from dotenv import load_dotenv
15
14
 
@@ -69,14 +68,40 @@ def find_env_file(env_name: str | None = None) -> Path | None:
69
68
  return None
70
69
 
71
70
 
72
- def _first_subdomain(url: str) -> str | None:
73
- """Extract first subdomain from URL. 'https://ims-dev.example.com/' -> 'ims-dev'."""
74
- try:
75
- hostname = urlparse(url).hostname or ""
76
- parts = hostname.split(".")
77
- return parts[0] if len(parts) > 1 else None
78
- except Exception:
79
- return None
71
+ # Environment keywords searched for in the RAGFlow URL, in scan order.
72
+ #
73
+ # ORDER IS LOAD-BEARING - do not sort or alphabetize this tuple. The scan takes the
74
+ # first keyword that appears anywhere in the URL, so a keyword that contains another
75
+ # keyword must be listed before the one it contains. "prod" is a substring of
76
+ # "preprod" - the only such pair in this set - so putting "prod" first would make
77
+ # preprod.example.com resolve to "prod". test_environment_keyword_order_is_safe in
78
+ # tests/test_rosetta_config_environment.py enforces this invariant.
79
+ ENVIRONMENT_KEYWORDS: tuple[str, ...] = (
80
+ "preprod", # must precede "prod", which it contains
81
+ "prod",
82
+ "stag",
83
+ "qa",
84
+ "dev",
85
+ "test",
86
+ "uat",
87
+ "sandbox",
88
+ "perf",
89
+ )
90
+
91
+
92
+ def _environment_from_url(url: str) -> str | None:
93
+ """Detect an environment name from keywords in a RAGFlow URL.
94
+
95
+ Case-insensitive substring scan over the URL exactly as given (scheme, host,
96
+ port and path all count). The first keyword in ENVIRONMENT_KEYWORDS to appear
97
+ wins, and the resolved value is the keyword itself, not the surrounding label:
98
+ 'https://ims-dev.example.com/' -> 'dev'. Returns None when no keyword appears.
99
+ """
100
+ lowered = (url or "").lower()
101
+ for keyword in ENVIRONMENT_KEYWORDS:
102
+ if keyword in lowered:
103
+ return keyword
104
+ return None
80
105
 
81
106
 
82
107
  @dataclass
@@ -98,7 +123,12 @@ class RosettaConfig:
98
123
  RAGFLOW_PAGE_SIZE: Page size for listing operations (default: 1000)
99
124
  RAGFLOW_PARSE_TIMEOUT: Timeout for parsing operations in seconds (default: 1200)
100
125
  RAGFLOW_TIMEOUT: HTTP request timeout in seconds (default: 30)
101
- ENVIRONMENT: Environment name (default: "local")
126
+ ENVIRONMENT: Environment display label. Takes precedence over URL keyword
127
+ detection, but not over an explicit --env argument. When unset, the
128
+ label is detected from RAGFLOW_BASE_URL by a case-insensitive scan for
129
+ the first of preprod, prod, stag, qa, dev, test, uat, sandbox, perf to
130
+ appear in it (the resolved value is that keyword itself, so
131
+ "https://ims-dev.example.com" yields "dev"); "local" if none appear.
102
132
 
103
133
  Examples:
104
134
  >>> config = RosettaConfig.from_env(".env")
@@ -194,9 +224,11 @@ class RosettaConfig:
194
224
  Args:
195
225
  environment: Optional explicit environment name (e.g., "local",
196
226
  "dev", "remote"). If provided, this value is used and takes
197
- precedence over the ENVIRONMENT environment variable. If not
198
- provided, the ENVIRONMENT variable is used, defaulting to
199
- "local" when unset.
227
+ precedence over everything else. If not provided, the
228
+ ENVIRONMENT variable is used; failing that, the label is
229
+ detected from a keyword in RAGFLOW_BASE_URL (see
230
+ ENVIRONMENT_KEYWORDS), defaulting to "local" when no keyword
231
+ appears.
200
232
 
201
233
  Returns:
202
234
  RosettaConfig instance
@@ -213,9 +245,11 @@ class RosettaConfig:
213
245
  api_key = os.getenv("RAGFLOW_API_KEY", "")
214
246
  dataset_default = os.getenv("RAGFLOW_DATASET_DEFAULT", "aia")
215
247
  dataset_template = os.getenv("RAGFLOW_DATASET_TEMPLATE", "aia-{release}")
216
- # fallback to ENVIRONMENT env var, or default to "local"
248
+ # Environment is a display label only - nothing branches on its value.
249
+ # Precedence: explicit argument (--env) > ENVIRONMENT > keyword detected in
250
+ # the RAGFlow URL > "local".
217
251
  if not environment:
218
- environment = os.getenv("ENVIRONMENT") or _first_subdomain(base_url) or "local"
252
+ environment = os.getenv("ENVIRONMENT") or _environment_from_url(base_url) or "local"
219
253
 
220
254
  # Dataset creation settings
221
255
  embedding_model = os.getenv("RAGFLOW_EMBEDDING_MODEL") or None
@@ -641,13 +641,16 @@ class ContentPublisher:
641
641
  return name[: -len(path.name)] + new_name
642
642
 
643
643
  # Collect all docs to delete into one list: (doc, label)
644
- # Start with unmanaged: incomplete metadata (ims_doc_id or original_path absent)
644
+ # Start with managed docs carrying incomplete metadata: original_path is
645
+ # set (so the doc is ours) but ims_doc_id is absent. A doc with no
646
+ # original_path is custom/unmanaged and is never touched - see the
647
+ # docstring guarantee, which _cleanup_orphans enforces the same way.
645
648
  duplicates: list[tuple[DocumentLike, str]] = []
646
649
  for doc in all_docs:
647
650
  meta = getattr(doc, "meta_fields", {}) or {}
648
651
  ims_doc_id = meta.get("ims_doc_id") if isinstance(meta, dict) else getattr(meta, "ims_doc_id", None)
649
652
  doc_original_path = meta.get("original_path", "") if isinstance(meta, dict) else getattr(meta, "original_path", "")
650
- if not ims_doc_id or not doc_original_path:
653
+ if doc_original_path and not ims_doc_id:
651
654
  doc_name = getattr(doc, "name", "") or doc.id
652
655
  duplicates.append((doc, doc_name))
653
656
 
@@ -674,8 +677,10 @@ class ContentPublisher:
674
677
  duplicates.append((doc, original_path))
675
678
 
676
679
  # Name duplicates: foo.md + foo(1).md + foo(2).md ...
680
+ # Grouped over managed_docs, not all_docs: a custom doc whose stripped
681
+ # name happens to collide with a managed one must not be deleted.
677
682
  name_groups: dict[str, list[DocumentLike]] = defaultdict(list)
678
- for doc in all_docs:
683
+ for doc in managed_docs:
679
684
  doc_name = getattr(doc, "name", "") or ""
680
685
  if not doc_name:
681
686
  continue
@@ -27,14 +27,27 @@ class DatasetService:
27
27
 
28
28
  def resolve_dataset_name(self, args_dataset: Optional[str]) -> tuple[Optional[str], bool]:
29
29
  """
30
- Resolve dataset name with auto-detection.
31
-
30
+ Resolve which dataset to act on.
31
+
32
+ How the name is picked:
33
+ 1. --dataset given -> use it (False)
34
+ 2. else list server datasets, keep those starting with the template
35
+ prefix ("aia-" from "aia-{release}")
36
+ 3. exactly one match -> use it (True)
37
+ 4. several matches -> give up, ask user (None, False)
38
+ 5. no matches -> use dataset_default (True)
39
+
32
40
  Args:
33
41
  args_dataset: Dataset name from arguments (can be None)
34
-
42
+
35
43
  Returns:
36
44
  Tuple of (dataset_name: str or None, auto_detected: bool)
37
45
  Returns (None, False) if resolution fails
46
+
47
+ auto_detected answers "did the caller supply this name?", not "did
48
+ we find it on the server". False only for step 1. True for steps 3
49
+ and 5 alike: the caller supplied nothing and this method chose.
50
+ True on step 5 is correct.
38
51
  """
39
52
  # Explicit dataset provided
40
53
  if args_dataset:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rosetta-cli
3
- Version: 3.0.4
3
+ Version: 3.0.6
4
4
  Summary: Rosetta CLI for publishing knowledge base content to RAGFlow
5
5
  Author-email: Rosetta Team <rosetta-support@griddynamics.com>
6
6
  License-Expression: Apache-2.0
File without changes
File without changes
File without changes
File without changes