jvcli 2.0.1__py3-none-any.whl → 2.0.3__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.
jvcli/__init__.py CHANGED
@@ -4,5 +4,5 @@ jvcli package initialization.
4
4
  This package provides the CLI tool for Jivas Package Repository.
5
5
  """
6
6
 
7
- __version__ = "2.0.1"
7
+ __version__ = "2.0.3"
8
8
  __supported__jivas__versions__ = ["2.0.0"]
jvcli/cli.py CHANGED
@@ -4,6 +4,7 @@ import click
4
4
 
5
5
  from jvcli import __version__
6
6
  from jvcli.commands.auth import login, logout, signup
7
+ from jvcli.commands.client import client
7
8
  from jvcli.commands.create import create
8
9
  from jvcli.commands.download import download
9
10
  from jvcli.commands.info import info
@@ -26,6 +27,7 @@ jvcli.add_command(download)
26
27
  jvcli.add_command(publish)
27
28
  jvcli.add_command(info)
28
29
  jvcli.add_command(studio)
30
+ jvcli.add_command(client)
29
31
 
30
32
  # Register standalone commands
31
33
  jvcli.add_command(signup)
@@ -0,0 +1 @@
1
+ """This package contains the client-side code for the JVCLI client streamlit app."""
jvcli/client/app.py ADDED
@@ -0,0 +1,160 @@
1
+ """This module contains the main application logic for the JVCLI client."""
2
+
3
+ import os
4
+
5
+ import requests
6
+ import streamlit as st
7
+ from streamlit_router import StreamlitRouter
8
+
9
+ from jvcli.client.lib.page import Page
10
+ from jvcli.client.lib.utils import call_list_actions, call_list_agents, load_function
11
+ from jvcli.client.pages import analytics_page, chat_page, dashboard_page, graph_page
12
+
13
+ JIVAS_URL = os.environ.get("JIVAS_URL", "http://localhost:8000")
14
+
15
+ JIVAS_STUDIO_URL = os.environ.get("JIVAS_STUDIO_URL", "http://localhost:8989")
16
+
17
+
18
+ def handle_agent_selection() -> None:
19
+ """Handle the selection of an agent."""
20
+ if "selected_agent" in st.session_state:
21
+ st.query_params["agent"] = st.session_state.selected_agent["id"]
22
+ st.session_state.messages = {}
23
+
24
+
25
+ def login_form() -> None:
26
+ """Render the login form and handle login logic."""
27
+ login_url = f"{JIVAS_URL}/user/login"
28
+
29
+ with st.container(border=True):
30
+ st.header("Login")
31
+
32
+ email = st.text_input("Email")
33
+ password = st.text_input("Password", type="password")
34
+
35
+ if st.button("Login"):
36
+ response = requests.post(
37
+ login_url, json={"email": email, "password": password}
38
+ )
39
+
40
+ if response.status_code == 200:
41
+ st.session_state.ROOT_ID = response.json()["user"]["root_id"]
42
+ st.session_state.TOKEN = response.json()["token"]
43
+ st.session_state.EXPIRATION = response.json()["user"]["expiration"]
44
+ st.rerun()
45
+
46
+
47
+ def main() -> None:
48
+ """Main function to render the Streamlit app."""
49
+ hide_sidebar = st.query_params.get("hide_sidebar")
50
+ router = StreamlitRouter()
51
+
52
+ # Initialize session state
53
+ for key in [
54
+ "messages",
55
+ "session_id",
56
+ "EXPIRATION",
57
+ "agents",
58
+ "actions_data",
59
+ "TOKEN",
60
+ "ROOT_ID",
61
+ "EXPIRATION",
62
+ ]:
63
+ if key not in st.session_state:
64
+ if key == "messages":
65
+ st.session_state[key] = {}
66
+ else:
67
+ st.session_state[key] = [] if key in ["actions_data"] else ""
68
+
69
+ if hide_sidebar == "true":
70
+ st.markdown(
71
+ """
72
+ <style>
73
+ [data-testid="stSidebar"] {
74
+ display: none;
75
+ }
76
+ [data-testid="stSidebarCollapsedControl"] {
77
+ display: none;
78
+ }
79
+ </style>
80
+ """,
81
+ unsafe_allow_html=True,
82
+ )
83
+
84
+ # Setup the sidebar
85
+ with st.sidebar:
86
+ st.title("✧ JIVAS Manager")
87
+ # retrieve agent list
88
+ agents = call_list_agents()
89
+
90
+ try:
91
+ selected_agent_id = st.query_params["agent"]
92
+ except KeyError:
93
+ st.query_params["agent"] = agents[0]["id"] if agents else None
94
+ selected_agent_id = st.query_params["agent"]
95
+
96
+ selected_agent_index = next(
97
+ (i for i, item in enumerate(agents) if item["id"] == selected_agent_id),
98
+ len(agents) - 1 if agents else None,
99
+ )
100
+
101
+ # Render the ComboBox using streamlit-elements
102
+ st.sidebar.selectbox(
103
+ "Agent",
104
+ agents,
105
+ index=selected_agent_index,
106
+ placeholder="Select agent...",
107
+ format_func=lambda x: x["label"] if "label" in x else x,
108
+ on_change=handle_agent_selection,
109
+ key="selected_agent",
110
+ )
111
+
112
+ # Expander for the menu
113
+ with st.expander("Menu", True):
114
+ Page(router).item(analytics_page.render, "Dashboard", "/").st_button()
115
+ Page(router).item(chat_page.render, "Chat", "/chat").st_button()
116
+ Page(router).item(dashboard_page.render, "Actions", "/actions").st_button()
117
+ Page(router).item(graph_page.render, "Graph", "/graph").st_button()
118
+ st.button(
119
+ "Logout", on_click=dashboard_page.logout, use_container_width=True
120
+ )
121
+
122
+ with st.expander("Action Apps", False):
123
+ if selected_agent_id and (
124
+ actions_data := call_list_actions(agent_id=selected_agent_id)
125
+ ):
126
+ st.session_state.actions_data = actions_data
127
+
128
+ for action in actions_data:
129
+ package = action.get("_package", {})
130
+
131
+ if package.get("config", {}).get("app", False):
132
+ func = load_function(
133
+ f"{package['config']['path']}/app/app.py",
134
+ "render",
135
+ router=router,
136
+ agent_id=selected_agent_id,
137
+ action_id=action["id"],
138
+ info=package,
139
+ )
140
+ # register the route to the app
141
+ Page(router).item(
142
+ callback=func,
143
+ label=package["meta"]["title"],
144
+ path=f'/{Page.normalize_label(package["meta"]["title"])}',
145
+ ).st_button()
146
+ router.serve()
147
+
148
+
149
+ # Initialize Streamlit app config
150
+ if __name__ == "__main__":
151
+ token_query = st.query_params.get("token")
152
+ if token_query:
153
+ st.session_state.TOKEN = token_query
154
+
155
+ if "TOKEN" not in st.session_state:
156
+ st.set_page_config(page_title="JIVAS Manager", page_icon="💠")
157
+ login_form()
158
+ else:
159
+ st.set_page_config(page_title="JIVAS Manager", page_icon="💠", layout="wide")
160
+ main()
@@ -0,0 +1 @@
1
+ """jvcli client library functions."""
@@ -0,0 +1,68 @@
1
+ """This module contains the Page class used for managing pages in the JVCLI client."""
2
+
3
+ from typing import Callable, Dict, Optional
4
+
5
+ import streamlit as st
6
+ from streamlit_router import StreamlitRouter
7
+
8
+
9
+ class Page:
10
+ """Class to manage pages in the JVCLI client."""
11
+
12
+ def __init__(self, router: StreamlitRouter) -> None:
13
+ """Initialize the Page with a router."""
14
+ self._router: StreamlitRouter = router
15
+ self._callback: Optional[Callable] = None
16
+ self._label: Optional[str] = None
17
+ self._path: Optional[str] = None
18
+ self._key: Optional[str] = None
19
+ self._args: Optional[Dict] = None
20
+
21
+ def item(
22
+ self, callback: Callable, label: str, path: str, args: Optional[Dict] = None
23
+ ) -> "Page":
24
+ """
25
+ Register the page callable on the given route.
26
+
27
+ Args:
28
+ callback (Callable): The function to call for the page.
29
+ label (str): The label for the page.
30
+ path (str): The path for the page.
31
+ args (Optional[Dict], optional): Additional arguments for the page. Defaults to None.
32
+
33
+ Returns:
34
+ Page: The current Page instance.
35
+ """
36
+ if args is None:
37
+ args = {}
38
+ self._callback = callback
39
+ self._label = label
40
+ self._path = path
41
+ self._args = args
42
+ self._key = f"{Page.normalize_label(label)}"
43
+ self._router.register(func=self._callback, path=self._path, endpoint=self._key)
44
+ return self
45
+
46
+ def st_button(self) -> None:
47
+ """Add the Streamlit link for this page wherever it is called."""
48
+ if st.button(self._label, key=self._key, use_container_width=True):
49
+ self._router.redirect(*self._router.build(self._key, self._args))
50
+
51
+ @staticmethod
52
+ def normalize_label(label: str) -> str:
53
+ """
54
+ Normalize the label to be used as a key.
55
+
56
+ Args:
57
+ label (str): The label to normalize.
58
+
59
+ Returns:
60
+ str: The normalized label.
61
+ """
62
+ return (
63
+ "".join(char.lower() for char in label if char.isascii())
64
+ .strip()
65
+ .replace(" ", "-")
66
+ .replace("/", "-")
67
+ .replace(":", "-")
68
+ )
@@ -0,0 +1,334 @@
1
+ """This module contains utility functions for the JVCLI client."""
2
+
3
+ import base64
4
+ import json
5
+ import os
6
+ from importlib.util import module_from_spec, spec_from_file_location
7
+ from io import BytesIO
8
+ from typing import Any, Callable, Dict, List, Optional
9
+
10
+ import requests
11
+ import streamlit as st
12
+ import yaml
13
+ from PIL import Image
14
+
15
+ JIVAS_URL = os.environ.get("JIVAS_URL", "http://localhost:8000")
16
+
17
+
18
+ def load_function(file_path: str, function_name: str, **kwargs: Any) -> Callable:
19
+ """Dynamically loads and returns a function from a Python file, with optional keyword arguments."""
20
+
21
+ if not os.path.exists(file_path):
22
+ raise FileNotFoundError(f"No file found at {file_path}")
23
+
24
+ # Get the module name from the file name
25
+ module_name = os.path.splitext(os.path.basename(file_path))[0]
26
+
27
+ # Load the module specification
28
+ spec = spec_from_file_location(module_name, file_path)
29
+ if spec is None:
30
+ raise ImportError(f"Could not load specification for module {module_name}")
31
+
32
+ # Create the module
33
+ module = module_from_spec(spec)
34
+ if spec.loader is None:
35
+ raise ImportError(f"Could not load module {module_name}")
36
+
37
+ # Execute the module
38
+ spec.loader.exec_module(module)
39
+
40
+ # Get the function
41
+ if not hasattr(module, function_name):
42
+ raise AttributeError(f"Function '{function_name}' not found in {file_path}")
43
+
44
+ func = getattr(module, function_name)
45
+
46
+ # Ensure the returned callable can accept any kwargs passed to it
47
+ def wrapped_func(*args: Any, **func_kwargs: Any) -> Any:
48
+ return func(*args, **{**kwargs, **func_kwargs})
49
+
50
+ return wrapped_func
51
+
52
+
53
+ def call_list_agents() -> list:
54
+ """Call the API to list agents."""
55
+
56
+ ctx = get_user_info()
57
+
58
+ endpoint = f"{JIVAS_URL}/walker/list_agents"
59
+
60
+ if ctx["token"]:
61
+ try:
62
+ headers = {"Authorization": "Bearer " + ctx["token"]}
63
+ json = {"reporting": True}
64
+
65
+ # call interact
66
+ response = requests.post(endpoint, json=json, headers=headers)
67
+
68
+ if response.status_code == 200:
69
+ result = response.json().get("reports", [])
70
+ if len(result) > 0:
71
+ return [
72
+ {"id": agent["id"], "label": agent["name"]} for agent in result
73
+ ]
74
+
75
+ if response.status_code == 401:
76
+ st.session_state.EXPIRATION = ""
77
+ return []
78
+
79
+ except Exception as e:
80
+ st.session_state.EXPIRATION = ""
81
+ print("Exception occurred: ", e)
82
+
83
+ return []
84
+
85
+
86
+ def call_list_actions(agent_id: str) -> list:
87
+ """Call the API to list actions for a given agent."""
88
+
89
+ ctx = get_user_info()
90
+
91
+ endpoint = f"{JIVAS_URL}/walker/list_actions"
92
+
93
+ if ctx["token"]:
94
+ try:
95
+ headers = {"Authorization": "Bearer " + ctx["token"]}
96
+ json = {"agent_id": agent_id}
97
+
98
+ # call interact
99
+ response = requests.post(endpoint, json=json, headers=headers)
100
+
101
+ if response.status_code == 200:
102
+ result = (response.json()).get("reports", [])
103
+ if len(result) > 0:
104
+ return result[0]
105
+ else:
106
+ return []
107
+
108
+ if response.status_code == 401:
109
+ st.session_state.EXPIRATION = ""
110
+ return []
111
+
112
+ except Exception as e:
113
+ st.session_state.EXPIRATION = ""
114
+ print("Exception occurred: ", e)
115
+
116
+ return []
117
+
118
+
119
+ def call_get_action(agent_id: str, action_id: str) -> list:
120
+ """Call the API to get a specific action for a given agent."""
121
+
122
+ ctx = get_user_info()
123
+
124
+ endpoint = f"{JIVAS_URL}/walker/get_action"
125
+
126
+ if ctx["token"]:
127
+ try:
128
+ headers = {"Authorization": "Bearer " + ctx["token"]}
129
+ json = {"agent_id": agent_id, "action_id": action_id}
130
+
131
+ # call interact
132
+ response = requests.post(endpoint, json=json, headers=headers)
133
+
134
+ if response.status_code == 200:
135
+ result = (response.json()).get("reports", [])
136
+ if len(result) > 0:
137
+ return result[0]
138
+ else:
139
+ return []
140
+
141
+ if response.status_code == 401:
142
+ st.session_state.EXPIRATION = ""
143
+ return []
144
+
145
+ except Exception as e:
146
+ st.session_state.EXPIRATION = ""
147
+ print("Exception occurred: ", e)
148
+
149
+ return []
150
+
151
+
152
+ def call_update_action(agent_id: str, action_id: str, action_data: dict) -> dict:
153
+ """Call the API to update a specific action for a given agent."""
154
+
155
+ ctx = get_user_info()
156
+
157
+ endpoint = f"{JIVAS_URL}/walker/update_action"
158
+
159
+ if ctx["token"]:
160
+ try:
161
+ headers = {"Authorization": "Bearer " + ctx["token"]}
162
+ json = {
163
+ "agent_id": agent_id,
164
+ "action_id": action_id,
165
+ "action_data": action_data,
166
+ }
167
+
168
+ # call interact
169
+ response = requests.post(endpoint, json=json, headers=headers)
170
+
171
+ if response.status_code == 200:
172
+ result = (response.json()).get("reports", [])
173
+ if len(result) > 0:
174
+ return result[0]
175
+ else:
176
+ return {}
177
+
178
+ if response.status_code == 401:
179
+ st.session_state.EXPIRATION = ""
180
+ return {}
181
+
182
+ except Exception as e:
183
+ st.session_state.EXPIRATION = ""
184
+ print("Exception occurred: ", e)
185
+
186
+ return {}
187
+
188
+
189
+ def call_action_walker_exec(
190
+ agent_id: str,
191
+ module_root: str,
192
+ walker: str,
193
+ args: Optional[Dict] = None,
194
+ files: Optional[List] = None,
195
+ headers: Optional[Dict] = None,
196
+ ) -> list:
197
+ """Call the API to execute a walker action for a given agent."""
198
+
199
+ ctx = get_user_info()
200
+
201
+ endpoint = f"{JIVAS_URL}/walker/action/walker"
202
+
203
+ if ctx.get("token"):
204
+ try:
205
+ headers = headers if headers else {}
206
+ headers["Authorization"] = "Bearer " + ctx["token"]
207
+
208
+ # Create form data
209
+ data = {"agent_id": agent_id, "module_root": module_root, "walker": walker}
210
+
211
+ if args:
212
+ data["args"] = json.dumps(args)
213
+
214
+ if files:
215
+ file_list = []
216
+
217
+ for file in files:
218
+ file_list.append(("attachments", (file[0], file[1], file[2])))
219
+
220
+ # Dispatch request
221
+ response = requests.post(
222
+ url=endpoint, headers=headers, data=data, files=file_list
223
+ )
224
+
225
+ if response.status_code == 200:
226
+ result = response.json()
227
+ return result if result else []
228
+
229
+ if response.status_code == 401:
230
+ st.session_state.EXPIRATION = ""
231
+ return []
232
+
233
+ except Exception as e:
234
+ st.session_state.EXPIRATION = ""
235
+ st.write(e)
236
+
237
+ return []
238
+
239
+
240
+ def call_import_agent(
241
+ descriptor: str,
242
+ headers: Optional[Dict] = None,
243
+ ) -> list:
244
+ """Call the API to import an agent."""
245
+
246
+ ctx = get_user_info()
247
+
248
+ endpoint = f"{JIVAS_URL}/walker/import_agent"
249
+
250
+ if ctx.get("token"):
251
+ try:
252
+ headers = headers if headers else {}
253
+ headers["Authorization"] = "Bearer " + ctx["token"]
254
+ headers["Content-Type"] = "application/json"
255
+ headers["Accept"] = "application/json"
256
+
257
+ data = {"descriptor": descriptor}
258
+
259
+ # Dispatch request
260
+ response = requests.post(endpoint, headers=headers, json=data)
261
+
262
+ if response.status_code == 200:
263
+ result = response.json()
264
+ return result if result else []
265
+
266
+ if response.status_code == 401:
267
+ st.session_state.EXPIRATION = ""
268
+ return []
269
+
270
+ except Exception as e:
271
+ st.session_state.EXPIRATION = ""
272
+ st.write(e)
273
+
274
+ return []
275
+
276
+
277
+ def get_user_info() -> dict:
278
+ """Get user information from the session state."""
279
+ return {
280
+ "root_id": st.session_state.ROOT_ID,
281
+ "token": st.session_state.TOKEN,
282
+ "expiration": st.session_state.EXPIRATION,
283
+ }
284
+
285
+
286
+ def decode_base64_image(base64_string: str) -> Image:
287
+ """Decode a base64 string into an image."""
288
+ # Decode the base64 string
289
+ image_data = base64.b64decode(base64_string)
290
+
291
+ # Create a bytes buffer from the decoded bytes
292
+ image_buffer = BytesIO(image_data)
293
+
294
+ # Open the image using PIL
295
+ return Image.open(image_buffer)
296
+
297
+
298
+ class LongStringDumper(yaml.SafeDumper):
299
+ """Custom YAML dumper to handle long strings."""
300
+
301
+ def represent_scalar(
302
+ self, tag: str, value: str, style: Optional[str] = None
303
+ ) -> yaml.ScalarNode:
304
+ """Represent scalar values, using block style for long strings."""
305
+ # Replace any escape sequences to format the output as desired
306
+ if (
307
+ len(value) > 150 or "\n" in value
308
+ ): # Adjust the threshold for long strings as needed
309
+ style = "|"
310
+ # converts all newline escapes to actual representations
311
+ value = "\n".join([line.rstrip() for line in value.split("\n")])
312
+ else:
313
+ # converts all newline escapes to actual representations
314
+ value = "\n".join([line.rstrip() for line in value.split("\n")]).rstrip()
315
+
316
+ return super().represent_scalar(tag, value, style)
317
+
318
+
319
+ def jac_yaml_dumper(
320
+ data: Any,
321
+ indent: int = 2,
322
+ default_flow_style: bool = False,
323
+ allow_unicode: bool = True,
324
+ sort_keys: bool = False,
325
+ ) -> str:
326
+ """Dumps YAML data using LongStringDumper with customizable options."""
327
+ return yaml.dump(
328
+ data,
329
+ Dumper=LongStringDumper,
330
+ indent=indent,
331
+ default_flow_style=default_flow_style,
332
+ allow_unicode=allow_unicode,
333
+ sort_keys=sort_keys,
334
+ )