jvcli 2.0.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.
@@ -0,0 +1,207 @@
1
+ """Publish command group for the Jivas Package Repository CLI."""
2
+
3
+ import os
4
+ import tempfile
5
+
6
+ import click
7
+ from pyaml import yaml
8
+
9
+ from jvcli.api import RegistryAPI
10
+ from jvcli.auth import load_token
11
+ from jvcli.utils import (
12
+ compress_package_to_tgz,
13
+ validate_dependencies,
14
+ validate_package_name,
15
+ validate_yaml_format,
16
+ )
17
+
18
+
19
+ # Main `publish` group command
20
+ @click.group()
21
+ def publish() -> None:
22
+ """
23
+ Publish resources to the Jivas environment.
24
+ Available subcommands: action, agent.
25
+ """
26
+ pass # pragma: no cover
27
+
28
+
29
+ @publish.command(name="action")
30
+ @click.option(
31
+ "--path",
32
+ required=True,
33
+ help="Path to the directory containing the action to publish.",
34
+ )
35
+ @click.option(
36
+ "--visibility",
37
+ type=click.Choice(["public", "private"], case_sensitive=False),
38
+ default="public",
39
+ show_default=True,
40
+ help="Visibility of the published action (public or private).",
41
+ )
42
+ @click.option(
43
+ "--package-only",
44
+ is_flag=True,
45
+ default=False,
46
+ show_default=True,
47
+ help="Only generate the package without publishing.",
48
+ )
49
+ @click.option(
50
+ "--output",
51
+ "-o",
52
+ default=None,
53
+ required=False,
54
+ help="Output path for generated package.",
55
+ )
56
+ @click.option(
57
+ "--namespace",
58
+ required=False, # Initially not required
59
+ help="Namespace of the package (required when --path is a tarball).",
60
+ )
61
+ def publish_action(
62
+ path: str, visibility: str, package_only: bool, output: str, namespace: str
63
+ ) -> None:
64
+ """Publish an action to the Jivas environment."""
65
+
66
+ if path.endswith(".tar.gz") and not namespace:
67
+ raise ValueError("--namespace is required when --path is a tarball (.tar.gz).")
68
+
69
+ _publish_common(path, visibility, package_only, output, "action", namespace)
70
+
71
+
72
+ @publish.command(name="agent")
73
+ @click.option(
74
+ "--path",
75
+ required=True,
76
+ help="Path to the directory containing the agent to publish.",
77
+ )
78
+ @click.option(
79
+ "--visibility",
80
+ type=click.Choice(["public", "private"], case_sensitive=False),
81
+ default="public",
82
+ show_default=True,
83
+ help="Visibility of the published agent (public or private).",
84
+ )
85
+ @click.option(
86
+ "--package-only",
87
+ is_flag=True,
88
+ default=False,
89
+ show_default=True,
90
+ help="Only generate the package without publishing.",
91
+ )
92
+ @click.option(
93
+ "--output",
94
+ "-o",
95
+ required=False,
96
+ help="Output path for generated package.",
97
+ )
98
+ @click.option(
99
+ "--namespace",
100
+ required=False, # Initially not required
101
+ help="Namespace of the package (required when --path is a tarball).",
102
+ )
103
+ def publish_agent(
104
+ path: str, visibility: str, package_only: bool, output: str, namespace: str
105
+ ) -> None:
106
+ """Publish an agent to the Jivas environment."""
107
+
108
+ if path.endswith(".tar.gz") and not namespace:
109
+ raise ValueError("--namespace is required when --path is a tarball (.tar.gz).")
110
+
111
+ _publish_common(path, visibility, package_only, output, "agent", namespace)
112
+
113
+
114
+ def _publish_common(
115
+ path: str,
116
+ visibility: str,
117
+ package_only: bool,
118
+ output: str,
119
+ publish_type: str,
120
+ namespace: str,
121
+ ) -> None:
122
+
123
+ token = load_token().get("token")
124
+ if not token and not package_only:
125
+ click.secho("You need to login first.", fg="red")
126
+ return
127
+
128
+ # Check if path is directory
129
+ if os.path.isdir(path):
130
+
131
+ info_path = os.path.join(path, "info.yaml")
132
+ if not os.path.exists(info_path):
133
+ click.secho(
134
+ f"Error: 'info.yaml' not found in the directory '{path}'.", fg="red"
135
+ )
136
+ return
137
+
138
+ click.secho(f"Preparing {publish_type} from directory: {path}", fg="yellow")
139
+
140
+ with open(info_path, "r") as info_file:
141
+ info_data = yaml.safe_load(info_file)
142
+
143
+ if validate_yaml_format(info_data, type=publish_type):
144
+ click.secho("info.yaml validated successfully.", fg="yellow")
145
+ else:
146
+ click.secho(f"Error validating 'info.yaml' for {publish_type}.", fg="red")
147
+ return
148
+
149
+ try:
150
+ package_name = info_data["package"].get("name")
151
+ validate_package_name(package_name)
152
+ click.secho(
153
+ f"Package name '{package_name}' validated successfully.", fg="yellow"
154
+ )
155
+ except ValueError as e:
156
+ click.secho(f"Error validating package name: {e}", fg="red")
157
+ return
158
+
159
+ try:
160
+ validate_dependencies(info_data["package"].get("dependencies", {}))
161
+ click.secho("Dependencies validated successfully.", fg="yellow")
162
+ except ValueError as e:
163
+ click.secho(f"Error validating dependencies: {e}", fg="red")
164
+ return
165
+
166
+ namespace, name = package_name.split("/", 1)
167
+
168
+ if package_only and not output:
169
+ output = "."
170
+
171
+ tgz_file_path = _prepare_package(namespace, name, path, publish_type, output)
172
+ click.secho(f"Compressed {publish_type} to: {tgz_file_path}", fg="yellow")
173
+
174
+ # check if path is already a tgz file
175
+ elif path.endswith(".tar.gz"):
176
+ click.secho(f"Preparing {publish_type} from tgz file: {path}", fg="yellow")
177
+ tgz_file_path = path
178
+
179
+ else:
180
+ click.secho(
181
+ f"Unable to publish {publish_type} from the path: {path}, unsupported file format"
182
+ )
183
+ return
184
+
185
+ if not package_only:
186
+ click.secho(
187
+ f"Publishing {publish_type} with visibility: {visibility}", fg="blue"
188
+ )
189
+ response = RegistryAPI.publish_action(
190
+ tgz_file_path, visibility, str(token), namespace or ""
191
+ )
192
+ if response:
193
+ click.secho(
194
+ f"{publish_type.capitalize()} published successfully!", fg="green"
195
+ )
196
+
197
+
198
+ def _prepare_package(
199
+ namespace: str, name: str, path: str, publish_type: str, output: str
200
+ ) -> str:
201
+ """Prepare the package for publishing."""
202
+ tgz_filename = os.path.join(
203
+ output if output else tempfile.gettempdir(), f"{namespace}_{name}.tar.gz"
204
+ )
205
+ tgz_file_path = compress_package_to_tgz(path, tgz_filename)
206
+ click.secho(f"Compressed {publish_type} to: {tgz_file_path}", fg="yellow")
207
+ return tgz_file_path
@@ -0,0 +1,96 @@
1
+ """Studio command group for deploying and interfacing with the Jivas Studio."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import click
7
+ import jaclang # noqa: F401
8
+ from bson import ObjectId
9
+ from jac_cloud.core.architype import NodeAnchor
10
+ from uvicorn import run
11
+
12
+
13
+ @click.group()
14
+ def studio() -> None:
15
+ """Group for managing Jivas Studio resources."""
16
+ pass # pragma: no cover
17
+
18
+
19
+ def get_graph(root: str) -> dict:
20
+ """Fetches a graph structure from the database."""
21
+ nodes = []
22
+ edges = []
23
+
24
+ edge_collection = NodeAnchor.Collection.get_collection("edge")
25
+ node_collection = NodeAnchor.Collection.get_collection("node")
26
+ node_docs = node_collection.find({"root": ObjectId(root)})
27
+ edge_docs = edge_collection.find({"root": ObjectId(root)})
28
+
29
+ for node in node_docs:
30
+ nodes.append(
31
+ {
32
+ "id": node["_id"],
33
+ "data": node["architype"],
34
+ "name": node["name"],
35
+ }
36
+ )
37
+ for edge in edge_docs:
38
+ edges.append(
39
+ {
40
+ "id": edge["_id"],
41
+ "name": edge["name"],
42
+ "source": edge["source"],
43
+ "target": edge["target"],
44
+ "data": edge["architype"],
45
+ }
46
+ )
47
+
48
+ return {
49
+ "nodes": json.loads(json.dumps(nodes, default=str)),
50
+ "edges": json.loads(json.dumps(edges, default=str)),
51
+ }
52
+
53
+
54
+ def get_users() -> list:
55
+ """Fetches users from the database."""
56
+ users = []
57
+ user_collection = NodeAnchor.Collection.get_collection("user")
58
+ user_docs = user_collection.find()
59
+
60
+ for user in user_docs:
61
+ users.append(
62
+ {
63
+ "id": user["_id"],
64
+ "root_id": user["root_id"],
65
+ "email": user["email"],
66
+ }
67
+ )
68
+
69
+ return json.loads(json.dumps(users, default=str))
70
+
71
+
72
+ @studio.command()
73
+ @click.option("--port", default=8989, help="Port for the studio to launch on.")
74
+ def launch(port: int) -> None:
75
+ """Launch the Jivas Studio on the specified port."""
76
+ click.echo(f"Launching Jivas Studio on port {port}...")
77
+ from fastapi import FastAPI
78
+ from fastapi.middleware.cors import CORSMiddleware
79
+ from fastapi.staticfiles import StaticFiles
80
+
81
+ app = FastAPI()
82
+ app.add_middleware(
83
+ CORSMiddleware,
84
+ allow_origins=["*"],
85
+ allow_credentials=True,
86
+ allow_methods=["*"],
87
+ allow_headers=["*"],
88
+ )
89
+
90
+ app.add_api_route("/graph", endpoint=get_graph, methods=["GET"])
91
+ app.add_api_route("/users", endpoint=get_users, methods=["GET"])
92
+
93
+ client_dir = Path(__file__).resolve().parent.parent.joinpath("client")
94
+ app.mount("/", StaticFiles(directory=client_dir, html=True), name="studio")
95
+
96
+ run(app, host="0.0.0.0", port=port)
@@ -0,0 +1,67 @@
1
+ """Update command group operations for Jivas Package Repository CLI tool."""
2
+
3
+ import click
4
+
5
+ from jvcli.api import RegistryAPI
6
+ from jvcli.auth import load_token
7
+
8
+
9
+ @click.group()
10
+ def update() -> None:
11
+ """Group for updating resources like namespaces."""
12
+ pass # pragma: no cover
13
+
14
+
15
+ @update.command(name="namespace")
16
+ @click.argument("namespace", required=True)
17
+ @click.option(
18
+ "--invite",
19
+ type=str,
20
+ help="Invite a user to the namespace by their email.",
21
+ metavar="EMAIL",
22
+ )
23
+ @click.option(
24
+ "--transfer",
25
+ type=str,
26
+ help="Transfer ownership of the namespace to a specified user by their email.",
27
+ metavar="EMAIL",
28
+ )
29
+ @click.pass_context
30
+ def namespace(ctx: click.Context, namespace: str, invite: str, transfer: str) -> None:
31
+ """
32
+ Update operations for a specified namespace.
33
+ Use one of the available options: --invite, --transfer.
34
+ """
35
+
36
+ token = load_token().get("token")
37
+ if not token:
38
+ click.secho("You need to login first.", fg="red")
39
+ ctx.exit(1)
40
+
41
+ # Validate mutually exclusive options
42
+ if invite and transfer:
43
+ click.secho(
44
+ "You can only use one of --invite or --transfer at a time.", fg="red"
45
+ )
46
+ ctx.exit(1)
47
+
48
+ # Handle different operations
49
+ if invite:
50
+ click.secho(f"Inviting '{invite}' to namespace '{namespace}'...", fg="yellow")
51
+ # Logic to invite a user to the namespace
52
+ RegistryAPI.invite_user_to_namespace(
53
+ namespace_name=namespace, user_email=invite, token=str(token)
54
+ )
55
+ elif transfer:
56
+ click.secho(
57
+ f"Transferring ownership of namespace '{namespace}' to '{transfer}'...",
58
+ fg="yellow",
59
+ )
60
+ # Logic to transfer ownership of the namespace
61
+ RegistryAPI.transfer_namespace_ownership(
62
+ namespace_name=namespace, new_owner_email=transfer, token=str(token)
63
+ )
64
+
65
+ click.secho(
66
+ f"Operation on namespace '{namespace}' completed successfully.", fg="green"
67
+ )
@@ -0,0 +1,14 @@
1
+ package:
2
+ name: "{{name}}"
3
+ author: "{{author}}"
4
+ architype: "{{architype}}"
5
+ version: "{{version}}"
6
+ meta:
7
+ title: "{{title}}"
8
+ description: "{{description}}"
9
+ group: "{{group}}"
10
+ type: "{{type}}"
11
+ config:
12
+ singleton: "{{singleton}}"
13
+ dependencies:
14
+ jivas: "~{{jivas_version}}"
@@ -0,0 +1,30 @@
1
+ published: true
2
+ name: "{{name}}"
3
+ description: |-
4
+ {{description}}
5
+ actions:
6
+ - action: jivas/intro_interact_action
7
+ context:
8
+ version: "0.0.1"
9
+ enabled: true
10
+ prompt: "In a natural and brief manner:\na. Introduce yourself by name and explain your role."
11
+ - action: jivas/persona_interact_action
12
+ context:
13
+ version: "0.0.1"
14
+ enabled: true
15
+ anchors: []
16
+ timezone: "UTC"
17
+ history: true
18
+ user_model: true
19
+ history_size: 2
20
+ max_statement_length: 800
21
+ model_name: "gpt-4o"
22
+ - action: jivas/agent_utils_action
23
+ context:
24
+ version: "0.0.1"
25
+ enabled: true
26
+ - action: jivas/langchain_model_action
27
+ context:
28
+ version: "0.0.1"
29
+ enabled: true
30
+ api_key: ""
@@ -0,0 +1,10 @@
1
+ package:
2
+ name: "{{name}}"
3
+ author: "{{author}}"
4
+ version: "{{version}}"
5
+ meta:
6
+ title: "{{title}}"
7
+ description: "{{description}}"
8
+ type: "agent"
9
+ dependencies:
10
+ jivas: "~{{jivas_version}}"
@@ -0,0 +1,14 @@
1
+ - metadata:
2
+ knode_index: Example, Knode, Index
3
+ anchors:
4
+ - What does this do?
5
+ - What is JIVAS?
6
+ text: |2-
7
+
8
+ # Example
9
+
10
+ ## Example Questions:
11
+ - What is JIVAS?
12
+
13
+ ## Answer:
14
+ JIVAS is a framework for building agents that can interact with the world.
@@ -0,0 +1 @@
1
+ - memory:
@@ -0,0 +1,2 @@
1
+ ## {{version}}
2
+ - Initialized {{package_type}} package using jvcli
@@ -0,0 +1,4 @@
1
+ ## {{name}}
2
+ {{description}}
3
+
4
+ version: {{version}}
jvcli/utils.py ADDED
@@ -0,0 +1,209 @@
1
+ """Utility functions for the Jivas Package Repository CLI tool."""
2
+
3
+ import os
4
+ import re
5
+ import tarfile
6
+
7
+ import click
8
+ import yaml
9
+ from packaging.specifiers import SpecifierSet
10
+ from packaging.version import parse as parse_version
11
+
12
+ from jvcli import __supported__jivas__versions__
13
+ from jvcli.api import RegistryAPI
14
+ from jvcli.auth import load_token
15
+
16
+ TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), "templates")
17
+
18
+
19
+ def validate_snake_case(ctx: click.Context, param: click.Parameter, value: str) -> str:
20
+ """Validate that the input is in snake_case."""
21
+ if not re.match(r"^[a-z0-9_]+$", value):
22
+ raise click.BadParameter(
23
+ "must be snake_case (lowercase letters, numbers, and underscores only)."
24
+ )
25
+ return value
26
+
27
+
28
+ def validate_name(ctx: click.Context, param: click.Parameter, value: str) -> str:
29
+ """Validate that the input only contains lowercase letters and numbers. Used for validating names."""
30
+ if not re.match(r"^[a-z0-9]+$", value):
31
+ raise click.BadParameter("must be lowercase letters and numbers only.")
32
+ return value
33
+
34
+
35
+ def validate_yaml_format(info_data: dict, type: str, version: str = "latest") -> bool:
36
+ """Validate if the info.yaml data matches the corresponding version template."""
37
+ if version == "latest":
38
+ version = max(__supported__jivas__versions__)
39
+
40
+ if type == "action" or type.endswith("action"):
41
+ template_path = os.path.join(TEMPLATES_DIR, version, "action_info.yaml")
42
+
43
+ if type == "daf" or type == "agent":
44
+ template_path = os.path.join(TEMPLATES_DIR, version, "agent_info.yaml")
45
+
46
+ if not os.path.exists(template_path):
47
+ click.secho(f"Template for version {version} not found.", fg="red")
48
+ return False
49
+
50
+ # Load template
51
+ with open(template_path, "r") as template_file:
52
+ # Fill placeholders to avoid YAML error
53
+ template_content = template_file.read().format(
54
+ dict.fromkeys(info_data.keys(), "")
55
+ )
56
+ template_data = yaml.safe_load(template_content)
57
+
58
+ # Compare keys
59
+ if set(info_data.keys()) != set(template_data.keys()):
60
+ missing_keys = set(template_data.keys()) - set(info_data.keys())
61
+ extra_keys = set(info_data.keys()) - set(template_data.keys())
62
+
63
+ if extra_keys:
64
+ click.secho(
65
+ f"Warning: Extra keys: {extra_keys} found in info.yaml, the jivas package repository may ignore them.",
66
+ fg="yellow",
67
+ )
68
+
69
+ if missing_keys:
70
+ click.secho(
71
+ f"info.yaml validation failed. Missing keys: {missing_keys}",
72
+ fg="red",
73
+ )
74
+ return False
75
+ return True
76
+
77
+
78
+ def validate_package_name(name: str) -> None:
79
+ """Ensure the package name includes a namespace and matches user access."""
80
+ if "/" not in name:
81
+ raise ValueError(
82
+ f"Package name '{name}' must include a namespace (e.g., 'namespace/action_name')."
83
+ )
84
+
85
+ namespace, _ = name.split("/", 1)
86
+ namespaces = load_token().get("namespaces", {}).get("groups", [])
87
+ if namespace not in namespaces:
88
+ raise ValueError(
89
+ f"Namespace '{namespace}' is not accessible to the current user."
90
+ )
91
+
92
+
93
+ def is_version_compatible(version: str, specifiers: str) -> bool:
94
+ """
95
+ Compares the provided version to a given set of specifications/modifiers or exact version match.
96
+
97
+ Args:
98
+ - version (str): The version to be compared. E.g., "2.1.0".
99
+ - specifiers (str): The version specifier set or exact version. E.g., "2.1.0" or ">=0.2,<0.3" or "0.0.1" or "^2.0.0"
100
+
101
+ Returns:
102
+ - bool: True if the version satisfies the specifier set or exact match, False otherwise.
103
+ """
104
+ try:
105
+ # Parse the version to check
106
+ version = parse_version(version)
107
+
108
+ # Check if specifiers is a simple exact version match
109
+ try:
110
+ exact_version = parse_version(specifiers)
111
+ return version == exact_version
112
+ except Exception:
113
+ # If parsing fails, treat it as a specifier set
114
+ pass
115
+
116
+ # Handle "~" shorthand by translating it to a compatible range
117
+ if specifiers.startswith("~"):
118
+ base_version = specifiers[1:]
119
+ parsed_base = parse_version(base_version)
120
+ major = parsed_base.major
121
+ minor = parsed_base.minor
122
+ # Assuming the next release constraint is on minor bump
123
+ upper_bound = f"<{major}.{minor + 1}"
124
+ specifiers = f">={base_version},{upper_bound}"
125
+
126
+ # Handle "^" shorthand to translate to a compatible range
127
+ if specifiers.startswith("^"):
128
+ base_version = specifiers[1:]
129
+ parsed_base = parse_version(base_version)
130
+ major = parsed_base.major
131
+ minor = parsed_base.minor
132
+ patch = parsed_base.micro
133
+ if major > 0:
134
+ upper_bound = f"<{major + 1}.0.0"
135
+ elif minor > 0:
136
+ upper_bound = f"<0.{minor + 1}.0"
137
+ else:
138
+ upper_bound = f"<0.0.{patch + 1}"
139
+ specifiers = f">={base_version},{upper_bound}"
140
+
141
+ # Create a SpecifierSet with the given specifiers
142
+ spec_set = SpecifierSet(specifiers)
143
+
144
+ # Check if the version matches the specifier set
145
+ return version in spec_set
146
+
147
+ except Exception as e:
148
+ # Handle exceptions if the inputs are malformed or invalid
149
+ click.secho(f"Error comparing versions: {e}", fg="red")
150
+ return False
151
+
152
+
153
+ def validate_dependencies(dependencies: dict) -> None:
154
+ """Ensure all dependencies exist in the registry."""
155
+ missing_dependencies = []
156
+ for dep, specifier in dependencies.items():
157
+ if dep == "jivas":
158
+ # Check if the version is in list of supported versions
159
+ def supported(spec: str) -> bool:
160
+ return any(
161
+ is_version_compatible(version, spec)
162
+ for version in __supported__jivas__versions__
163
+ )
164
+
165
+ if not supported(specifier):
166
+ missing_dependencies.append(f"{dep} {specifier}")
167
+ elif dep == "actions":
168
+ # Check if action exists in the registry
169
+ for name, spec in specifier.items():
170
+ package = RegistryAPI.download_package(
171
+ name=name, version=spec, suppress_error=True
172
+ )
173
+
174
+ if not package:
175
+ missing_dependencies.append(f"{dep} {specifier}")
176
+ elif dep == "pip":
177
+ # TODO: Add support for pip dependencies
178
+ continue
179
+ else:
180
+ raise ValueError(f"Unknown dependency type: {dep}")
181
+
182
+ if missing_dependencies:
183
+ raise ValueError(f"Dependencies not found in registry: {missing_dependencies}")
184
+
185
+
186
+ def compress_package_to_tgz(source_path: str, output_filename: str) -> str:
187
+ """
188
+ Compress the action folder into a .tgz file with the required structure,
189
+ excluding the __jac_gen__ folder.
190
+
191
+ Args:
192
+ source_path (str): Path to the action directory.
193
+ output_filename (str): Desired name of the output .tgz file.
194
+
195
+ Returns:
196
+ str: Path to the .tgz file.
197
+ """
198
+ with tarfile.open(output_filename, "w:gz") as tar:
199
+ for root, dirs, files in os.walk(source_path):
200
+ # Exclude the __jac_gen__ folder
201
+ if "__jac_gen__" in dirs:
202
+ dirs.remove("__jac_gen__")
203
+ if "__pycache__" in dirs:
204
+ dirs.remove("__pycache__")
205
+ for file in files:
206
+ file_path = os.path.join(root, file)
207
+ arcname = os.path.relpath(file_path, start=source_path)
208
+ tar.add(file_path, arcname=arcname)
209
+ return output_filename