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,452 @@
1
+ """Create commands for the CLI tool."""
2
+
3
+ import os
4
+
5
+ import click
6
+
7
+ from jvcli import __supported__jivas__versions__, __version__
8
+ from jvcli.api import RegistryAPI
9
+ from jvcli.auth import load_token, save_token
10
+ from jvcli.utils import TEMPLATES_DIR, validate_name, validate_snake_case
11
+
12
+ TYPE_SUFFIX_MAP = {
13
+ "action": "_action",
14
+ "interact_action": "_interact_action",
15
+ "vector_store_action": "_vector_store_action",
16
+ }
17
+
18
+
19
+ @click.group()
20
+ def create() -> None:
21
+ """Group for creating resources like actions"""
22
+ pass # pragma: no cover
23
+
24
+
25
+ @create.command(name="action")
26
+ @click.option(
27
+ "--name",
28
+ prompt=True,
29
+ callback=validate_snake_case,
30
+ help="Name of the action (must be snake_case).",
31
+ )
32
+ @click.option(
33
+ "--version", default="0.0.1", show_default=True, help="Version of the action."
34
+ )
35
+ @click.option(
36
+ "--jivas_version",
37
+ default=max(__supported__jivas__versions__),
38
+ show_default=True,
39
+ help="Version of Jivas.",
40
+ )
41
+ @click.option(
42
+ "--description",
43
+ default="No description provided.",
44
+ help="Description of the action.",
45
+ )
46
+ @click.option(
47
+ "--type",
48
+ "--type",
49
+ default="action",
50
+ type=click.Choice(TYPE_SUFFIX_MAP.keys(), case_sensitive=False),
51
+ show_default=True,
52
+ help="Type of the action.",
53
+ )
54
+ @click.option(
55
+ "--singleton",
56
+ default=True,
57
+ type=bool,
58
+ show_default=True,
59
+ help="Indicate if the action is singleton.",
60
+ )
61
+ @click.option(
62
+ "--path",
63
+ default="./actions",
64
+ show_default=True,
65
+ help="Directory to create the action folder in.",
66
+ )
67
+ @click.option(
68
+ "--namespace",
69
+ default=None,
70
+ help="Namespace for the action. Defaults to the username in the token.",
71
+ )
72
+ @click.option(
73
+ "--singleton",
74
+ default=True,
75
+ type=bool,
76
+ show_default=True,
77
+ help="Indicate if the action is singleton.",
78
+ )
79
+ @click.option(
80
+ "--path",
81
+ default="./actions",
82
+ show_default=True,
83
+ help="Directory to create the action folder in.",
84
+ )
85
+ def create_action(
86
+ name: str,
87
+ version: str,
88
+ jivas_version: str,
89
+ description: str,
90
+ singleton: bool,
91
+ type: str,
92
+ path: str,
93
+ namespace: str,
94
+ ) -> None:
95
+ """Create a new action with its folder, associated files, and an app folder."""
96
+
97
+ # Retrieve the email from the token or set it as blank
98
+ token = load_token()
99
+ namespace = namespace or token.get("namespaces", {}).get("default", "anonymous")
100
+ author = token.get("email", "unknown@example.com")
101
+
102
+ suffix = TYPE_SUFFIX_MAP[type]
103
+ if not name.endswith(suffix):
104
+ name += suffix
105
+
106
+ # Format the full action name with namespace
107
+ full_name = f"{namespace}/{name}"
108
+
109
+ # Generate class name (CamelCase)
110
+ architype = "".join(word.capitalize() for word in name.split("_"))
111
+
112
+ # Validate the Jivas version
113
+ if str(jivas_version) not in __supported__jivas__versions__:
114
+ click.secho(
115
+ f"Jivas version {jivas_version} is not supported. Supported versions are: {__supported__jivas__versions__}.",
116
+ fg="red",
117
+ )
118
+ return
119
+
120
+ # Prepare the template path
121
+ template_path = os.path.join(TEMPLATES_DIR, jivas_version, "action_info.yaml")
122
+ if not os.path.exists(template_path):
123
+ click.secho(
124
+ f"Template for version {jivas_version} not found in {TEMPLATES_DIR}.",
125
+ fg="red",
126
+ )
127
+ return
128
+
129
+ # Prepare target folder
130
+ namespace_dir = os.path.join(path, namespace)
131
+ action_dir = os.path.join(namespace_dir, name)
132
+ os.makedirs(action_dir, exist_ok=True)
133
+
134
+ # Load and substitute YAML template
135
+ with open(template_path, "r") as file:
136
+ template = file.read()
137
+
138
+ title = name.replace("_", " ").title()
139
+
140
+ data = {
141
+ "name": full_name, # Include namespace in the package name
142
+ "author": author,
143
+ "architype": architype,
144
+ "version": version,
145
+ "title": title,
146
+ "description": description,
147
+ "group": "contrib",
148
+ "type": type,
149
+ "singleton": str(singleton).lower(),
150
+ "jivas_version": jivas_version,
151
+ }
152
+
153
+ yaml_content = template
154
+ for key, value in data.items():
155
+ yaml_content = yaml_content.replace(f"{{{{{key}}}}}", str(value))
156
+
157
+ # Write info.yaml
158
+ yaml_path = os.path.join(action_dir, "info.yaml")
159
+ with open(yaml_path, "w") as file:
160
+ file.write(yaml_content)
161
+
162
+ # Create lib.jac
163
+ lib_path = os.path.join(action_dir, "lib.jac")
164
+ with open(lib_path, "w") as file:
165
+ file.write(f"include:jac {name};\n")
166
+
167
+ # Create action-specific .jac file
168
+ action_jac_path = os.path.join(action_dir, f"{name}.jac")
169
+ with open(action_jac_path, "w") as file:
170
+ node_class = {
171
+ "action": "Action",
172
+ "interact_action": "InteractAction",
173
+ "vector_store_action": "VectorStoreAction",
174
+ }[type]
175
+
176
+ import_statement = f"import:jac from agent.action.{type} {{ {node_class} }}"
177
+
178
+ abilities = """
179
+ #* (Abilities - Uncomment and implement as needed)
180
+ can on_register {
181
+ # override to execute operations upon registration of action
182
+ }
183
+
184
+ can post_register {
185
+ # override to execute any setup code when all actions are in place
186
+ }
187
+
188
+ can on_enable {
189
+ # override to execute operations upon enabling of action
190
+ }
191
+
192
+ can on_disable {
193
+ # override to execute operations upon disabling of action
194
+ }
195
+
196
+ can on_deregister {
197
+ # override to execute operations upon deregistration of action
198
+ }
199
+
200
+ can touch(visitor: interact_graph_walker) -> bool {
201
+ # override to authorize, redirect or deny the interact walker from running execute
202
+ }
203
+
204
+ can execute(visitor: interact_graph_walker) -> dict {
205
+ # override to implement action execution
206
+ }
207
+
208
+ can pulse() {
209
+ # override to implement pulse operation
210
+ }
211
+ *#
212
+ """
213
+ node_content = f"""
214
+ # Define your custom action code here
215
+ {import_statement}
216
+
217
+ node {architype} :{node_class}: {{
218
+ # Declare your has variables to be persisted here
219
+ # e.g has var_a : str = "string";
220
+
221
+ {abilities}
222
+ }}
223
+ """
224
+ file.write(node_content.strip())
225
+
226
+ # Create the 'app' folder and default 'app.py'
227
+ app_dir = os.path.join(action_dir, "app")
228
+ os.makedirs(app_dir, exist_ok=True)
229
+ app_file_path = os.path.join(app_dir, "app.py")
230
+ with open(app_file_path, "w") as app_file:
231
+ app_code = """
232
+ \"\"\" This module renders the streamlit app for the {title}. \"\"\"
233
+
234
+ from jvclient.client.lib.widgets import app_controls, app_header, app_update_action
235
+
236
+ from streamlit_router import StreamlitRouter
237
+
238
+ def render(router: StreamlitRouter, agent_id: str, action_id: str, info: dict) -> None:
239
+ \"\"\"Render the Streamlit app for the {title}.
240
+ :param router: The StreamlitRouter instance
241
+ :param agent_id: The agent ID
242
+ :param action_id: The action ID
243
+ :param info: The action info dict
244
+ \"\"\"
245
+
246
+ # Add app header controls
247
+ (model_key, module_root) = app_header(agent_id, action_id, info)
248
+
249
+ # Add app main controls
250
+ app_controls(agent_id, action_id)
251
+
252
+ # Add update button to apply changes
253
+ app_update_action(agent_id, action_id)
254
+ """
255
+ app_code = app_code.replace("{title}", title)
256
+ app_file.write(app_code)
257
+
258
+ create_docs(action_dir, title, version, "action", description)
259
+
260
+ click.secho(
261
+ f"Action '{name}' created successfully in {action_dir}!", fg="green", bold=True
262
+ )
263
+
264
+
265
+ @create.command(name="namespace")
266
+ @click.option(
267
+ "--name", prompt=True, callback=validate_name, help="Name of the namespace."
268
+ )
269
+ def create_namespace(name: str) -> None:
270
+ """
271
+ Create a new namespace through the API and update the local token file.
272
+
273
+ name: The name of the new namespace to create.
274
+ """
275
+ # Load the token data
276
+ token_data = load_token()
277
+ if not token_data:
278
+ click.secho(
279
+ "You are not logged in. Please log in before creating a namespace.",
280
+ fg="red",
281
+ )
282
+ return
283
+
284
+ # Extract the token
285
+ token = token_data.get("token")
286
+ if not token:
287
+ click.secho(
288
+ "Token missing from the local configuration. Please log in again.", fg="red"
289
+ )
290
+ return
291
+
292
+ # Call the API to create the namespace
293
+ updated_namespaces = RegistryAPI.create_namespace(name, token)
294
+
295
+ if updated_namespaces:
296
+
297
+ # Fetch namespace object
298
+ namespaces = token_data.get(
299
+ "namespaces", {"default": "anonymous", "groups": []}
300
+ ) # TODO: Remove default when API is updated
301
+
302
+ if name not in namespaces["groups"]:
303
+ namespaces["groups"].append(name)
304
+
305
+ click.secho(f"Namespace '{name}' created successfully!", fg="green", bold=True)
306
+
307
+ # Update the local token file with the new namespaces
308
+ save_token(token, namespaces, str(token_data.get("email")))
309
+
310
+
311
+ @create.command(name="agent")
312
+ @click.option(
313
+ "--name",
314
+ prompt=True,
315
+ callback=validate_snake_case,
316
+ help="Name of the agent (must be snake_case).",
317
+ )
318
+ @click.option(
319
+ "--version", default="0.0.1", show_default=True, help="Version of the agent."
320
+ )
321
+ @click.option(
322
+ "--jivas_version",
323
+ default=max(__supported__jivas__versions__),
324
+ show_default=True,
325
+ help="Version of Jivas.",
326
+ )
327
+ @click.option(
328
+ "--description",
329
+ default="A jivas agent autocreated by the jvcli.",
330
+ help="Description of the agent.",
331
+ )
332
+ @click.option(
333
+ "--path",
334
+ default="./dafs",
335
+ show_default=True,
336
+ help="Directory to create the agent.",
337
+ )
338
+ @click.option(
339
+ "--namespace",
340
+ default=None,
341
+ help="Namespace for the agent. Defaults to the username in the token.",
342
+ )
343
+ def create_agent(
344
+ name: str,
345
+ version: str,
346
+ jivas_version: str,
347
+ description: str,
348
+ path: str,
349
+ namespace: str,
350
+ ) -> None:
351
+ """Create a new agent with its folder and associated files."""
352
+
353
+ # Retrieve token info
354
+ token = load_token()
355
+ namespace = namespace or token.get("namespaces", {}).get("default", "anonymous")
356
+ author = token.get("email", "unknown@example.com")
357
+
358
+ # Validate Jivas version
359
+ if str(jivas_version) not in __supported__jivas__versions__:
360
+ click.secho(
361
+ f"Jivas version {jivas_version} is not supported. Supported versions are: {__supported__jivas__versions__}.",
362
+ fg="red",
363
+ )
364
+ return
365
+
366
+ # Prepare paths
367
+ namespace_dir = os.path.join(path, namespace)
368
+ daf_dir = os.path.join(namespace_dir, name)
369
+ os.makedirs(daf_dir, exist_ok=True)
370
+
371
+ # Load templates
372
+ template_paths = {
373
+ "info.yaml": os.path.join(TEMPLATES_DIR, __version__, "agent_info.yaml"),
374
+ "descriptor.yaml": os.path.join(
375
+ TEMPLATES_DIR, __version__, "agent_descriptor.yaml"
376
+ ),
377
+ "knowledge.yaml": os.path.join(
378
+ TEMPLATES_DIR, __version__, "agent_knowledge.yaml"
379
+ ),
380
+ "memory.yaml": os.path.join(TEMPLATES_DIR, __version__, "agent_memory.yaml"),
381
+ }
382
+
383
+ # Check if all templates exist
384
+ for filename, template_path in template_paths.items():
385
+ if not os.path.exists(template_path):
386
+ click.secho(f"Template {filename} not found in TEMPLATES_DIR.", fg="red")
387
+ return
388
+
389
+ # Read templates
390
+ templates = {}
391
+ for key, path in template_paths.items():
392
+ with open(path, "r") as file:
393
+ templates[key] = file.read()
394
+
395
+ # Replace placeholders
396
+ data = {
397
+ "name": f"{namespace}/{name}",
398
+ "author": author,
399
+ "version": version,
400
+ "title": name.replace("_", " ").title(),
401
+ "description": description,
402
+ "type": "agent",
403
+ "jivas_version": jivas_version,
404
+ }
405
+
406
+ for filename, template_content in templates.items():
407
+ for key, value in data.items():
408
+ template_content = template_content.replace(f"{{{{{key}}}}}", str(value))
409
+
410
+ with open(os.path.join(daf_dir, filename), "w") as file:
411
+ file.write(template_content)
412
+
413
+ # Create documentation
414
+ create_docs(daf_dir, name, version, "agent", description)
415
+
416
+ # Success message
417
+ click.secho(
418
+ f"Agent '{name}' created successfully in {daf_dir}!", fg="green", bold=True
419
+ )
420
+
421
+
422
+ def create_docs(
423
+ path: str, name: str, version: str, package_type: str, description: str = ""
424
+ ) -> None:
425
+ """Update README.md and CHANGELOG.md templates with name and version."""
426
+
427
+ # Create README
428
+ readme_template = os.path.join(TEMPLATES_DIR, "README.md")
429
+ if os.path.exists(readme_template):
430
+ with open(readme_template, "r") as file:
431
+ readme_content = file.read()
432
+
433
+ readme_content = readme_content.replace("{{version}}", version)
434
+ readme_content = readme_content.replace("{{name}}", name)
435
+ readme_content = readme_content.replace("{{description}}", description)
436
+
437
+ target_readme = os.path.join(path, "README.md")
438
+ with open(target_readme, "w") as file:
439
+ file.write(readme_content)
440
+
441
+ # Create CHANGELOG
442
+ changelog_template = os.path.join(TEMPLATES_DIR, "CHANGELOG.md")
443
+ if os.path.exists(changelog_template):
444
+ with open(changelog_template, "r") as file:
445
+ changelog_content = file.read()
446
+
447
+ changelog_content = changelog_content.replace("{{version}}", version)
448
+ changelog_content = changelog_content.replace("{{package_type}}", package_type)
449
+
450
+ target_changelog = os.path.join(path, "CHANGELOG.md")
451
+ with open(target_changelog, "w") as file:
452
+ file.write(changelog_content)
@@ -0,0 +1,111 @@
1
+ """Download commands for the Jivas Package Repository CLI."""
2
+
3
+ import io
4
+ import os
5
+ import tarfile
6
+
7
+ import click
8
+ import requests
9
+ from pyaml import yaml
10
+
11
+ from jvcli.api import RegistryAPI
12
+ from jvcli.auth import load_token
13
+
14
+
15
+ @click.group()
16
+ def download() -> None:
17
+ """Group for downloading resources like actions."""
18
+ pass # pragma: no cover
19
+
20
+
21
+ @download.command(name="action")
22
+ @click.argument("name")
23
+ @click.argument("version", required=False)
24
+ @click.option(
25
+ "--path",
26
+ required=False,
27
+ help="Directory to download the action.",
28
+ )
29
+ def download_action(name: str, version: str, path: str) -> None:
30
+ """Download a JIVAS action package."""
31
+ _download_package(name, version, path, "action")
32
+
33
+
34
+ @download.command(name="agent")
35
+ @click.argument("name")
36
+ @click.argument("version", required=False)
37
+ @click.option(
38
+ "--path",
39
+ required=False,
40
+ help="Directory to download the agent.",
41
+ )
42
+ def download_agent(name: str, version: str, path: str) -> None:
43
+ """Download a JIVAS agent package."""
44
+ _download_package(name, version, path, "agent")
45
+
46
+
47
+ def _download_package(name: str, version: str, path: str, pkg_type: str) -> None:
48
+ token = load_token().get("token")
49
+
50
+ if not version:
51
+ version = "latest"
52
+
53
+ click.echo(f"Downloading {name} version {version}...")
54
+
55
+ try:
56
+ package_data = RegistryAPI.download_package(name, version, token=token)
57
+ if not package_data:
58
+ click.secho("Failed to download the package.", fg="red")
59
+ return
60
+
61
+ package_file = requests.get(package_data["file"])
62
+ info_file = None
63
+ target_dir = None
64
+
65
+ with tarfile.open(
66
+ fileobj=io.BytesIO(package_file.content), mode="r:gz"
67
+ ) as tar_file:
68
+ for member in tar_file.getmembers():
69
+
70
+ if "__MACOSX" in member.name:
71
+ continue
72
+
73
+ if member.name in [
74
+ "info.yaml",
75
+ "info.yml",
76
+ "./info.yaml",
77
+ "./info.yml",
78
+ ]:
79
+ info_file = tar_file.extractfile(member)
80
+ break
81
+
82
+ if info_file:
83
+ info_content = yaml.safe_load(info_file)
84
+ package_type = (
85
+ info_content.get("package", {}).get("meta", {}).get("type")
86
+ )
87
+
88
+ # checking for both daf and agent to maintain backward compatibility
89
+ if pkg_type == "agent" and package_type in ["agent", "daf"]:
90
+ base_dir = "dafs"
91
+ elif pkg_type == "action" and package_type.endswith("action"):
92
+ base_dir = "actions"
93
+ else:
94
+ click.secho(
95
+ f"Invalid package type for {pkg_type} download", fg="red"
96
+ )
97
+ return
98
+
99
+ target_dir = os.path.join(path if path else f"./{base_dir}", name)
100
+ os.makedirs(target_dir, exist_ok=True)
101
+ tar_file.extractall(target_dir)
102
+ else:
103
+ click.echo("No info.yaml file found in the package.")
104
+
105
+ if target_dir:
106
+ click.secho(
107
+ f"Package '{name}' (version: {version}) downloaded to {target_dir}!",
108
+ fg="green",
109
+ )
110
+ except Exception as e:
111
+ click.secho(f"Error downloading the package: {e}", fg="red")
jvcli/commands/info.py ADDED
@@ -0,0 +1,91 @@
1
+ """Info command group for getting info about resources on the Jivas Package Repository."""
2
+
3
+ import sys
4
+
5
+ import click
6
+ from pyaml import yaml
7
+
8
+ from jvcli.api import RegistryAPI
9
+ from jvcli.auth import load_token
10
+
11
+
12
+ @click.group()
13
+ def info() -> None:
14
+ """Group for getting info about resources like actions."""
15
+ pass # pragma: no cover
16
+
17
+
18
+ @info.command(name="action")
19
+ @click.argument("name")
20
+ @click.argument("version", required=False)
21
+ def get_action_info(name: str, version: str) -> None:
22
+ """
23
+ Get info for an action package by name and version.
24
+ If version is not provided, the latest version will be fetched.
25
+ """
26
+
27
+ token = load_token().get("token")
28
+
29
+ # If version is not provided, fetch latest version
30
+ if not version:
31
+ click.echo("Checking the latest version of the action...")
32
+ version = "latest"
33
+
34
+ # Use the API function to fetch the action
35
+ try:
36
+ package_info = RegistryAPI.get_package_info(name, version, token=token)
37
+
38
+ if not package_info:
39
+ click.secho("Failed to locate the action package.", fg="red")
40
+ return
41
+
42
+ click.secho("======= PACKAGE INFO ========", fg="green")
43
+ yaml.safe_dump(
44
+ package_info,
45
+ sys.stdout,
46
+ width=100,
47
+ allow_unicode=True,
48
+ default_flow_style=False,
49
+ )
50
+ click.secho("=============================", fg="green")
51
+
52
+ except Exception as e:
53
+ click.secho(f"Error retrieving the action info: {e}", fg="red")
54
+
55
+
56
+ @info.command(name="agent")
57
+ @click.argument("name")
58
+ @click.argument("version", required=False)
59
+ def get_agent_info(name: str, version: str) -> None:
60
+ """
61
+ Get info for an agent package by name and version.
62
+ If version is not provided, the latest version will be fetched.
63
+ """
64
+
65
+ token = load_token().get("token")
66
+
67
+ # If version is not provided, fetch latest version
68
+ if not version:
69
+ click.echo("Checking the latest version of the agent package...")
70
+ version = "latest"
71
+
72
+ # Use the API function to fetch the agent
73
+ try:
74
+ package_info = RegistryAPI.get_package_info(name, version, token=token)
75
+
76
+ if not package_info:
77
+ click.secho("Failed to locate the agent package.", fg="red")
78
+ return
79
+
80
+ click.secho("======= PACKAGE INFO ========", fg="green")
81
+ yaml.safe_dump(
82
+ package_info,
83
+ sys.stdout,
84
+ width=100,
85
+ allow_unicode=True,
86
+ default_flow_style=False,
87
+ )
88
+ click.secho("=============================", fg="green")
89
+
90
+ except Exception as e:
91
+ click.secho(f"Error retrieving the agent package info: {e}", fg="red")