jvcli 2.0.2__py3-none-any.whl → 2.0.4__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,252 @@
1
+ """Streamlit widgets for JVCLI client app."""
2
+
3
+ from typing import Any, Optional
4
+
5
+ import streamlit as st
6
+ import yaml
7
+
8
+ from jvcli.client.lib.utils import call_get_action, call_update_action
9
+
10
+
11
+ def app_header(agent_id: str, action_id: str, info: dict) -> tuple:
12
+ """Render the app header and return model key and module root."""
13
+
14
+ # Create a dynamic key for the session state using the action_id
15
+ model_key = f"model_{agent_id}_{action_id}"
16
+ module_root = info.get("config", {}).get("module_root")
17
+
18
+ # Initialize session state if not already
19
+ if model_key not in st.session_state:
20
+ # Copy original data to prevent modification of original_data
21
+ st.session_state[model_key] = call_get_action(
22
+ agent_id=agent_id, action_id=action_id
23
+ )
24
+
25
+ # add standard action app header
26
+ st.header(
27
+ st.session_state[model_key]
28
+ .get("_package", {})
29
+ .get("meta", {})
30
+ .get("title", "Action"),
31
+ divider=True,
32
+ )
33
+
34
+ # Display the description from the model
35
+ if description := st.session_state[model_key].get("description", False):
36
+ st.text(description)
37
+
38
+ # Manage the 'enabled' field
39
+ st.session_state[model_key]["enabled"] = st.checkbox(
40
+ "Enabled", key="enabled", value=st.session_state[model_key]["enabled"]
41
+ )
42
+
43
+ return model_key, module_root
44
+
45
+
46
+ def snake_to_title(snake_str: str) -> str:
47
+ """Convert a snake_case string to Title Case."""
48
+ return snake_str.replace("_", " ").title()
49
+
50
+
51
+ def app_controls(agent_id: str, action_id: str) -> None:
52
+ """Render the app controls for a given agent and action."""
53
+ # Generate a dynamic key for the session state using the action_id
54
+ model_key = f"model_{agent_id}_{action_id}"
55
+
56
+ # Recursive function to handle nested dictionaries
57
+ def render_fields(item_key: str, value: Any, parent_key: str = "") -> None:
58
+ """Render fields based on their type."""
59
+
60
+ field_type = type(value)
61
+ label = snake_to_title(item_key) # Convert item_key to Title Case
62
+
63
+ if item_key not in st.session_state.get("model_key", {}).keys():
64
+ # Special case for 'api_key' to render as a password field
65
+ if item_key == "api_key":
66
+ st.session_state[model_key][item_key] = st.text_input(
67
+ label, value=value, type="password", key=item_key
68
+ )
69
+
70
+ elif field_type == int:
71
+ st.session_state[model_key][item_key] = st.number_input(
72
+ label, value=value, step=1, key=item_key
73
+ )
74
+
75
+ elif field_type == float:
76
+ st.session_state[model_key][item_key] = st.number_input(
77
+ label, value=value, step=0.01, key=item_key
78
+ )
79
+
80
+ elif field_type == bool:
81
+ st.session_state[model_key][item_key] = st.checkbox(
82
+ label, value=value, key=item_key
83
+ )
84
+
85
+ elif field_type == list:
86
+ yaml_str = st.text_area(
87
+ label + " (YAML format)", value=yaml.dump(value), key=item_key
88
+ )
89
+ try:
90
+ # Update the list with the user-defined YAML
91
+ loaded_value = yaml.safe_load(yaml_str)
92
+ if not isinstance(loaded_value, list):
93
+ raise ValueError("The provided YAML does not produce a list.")
94
+ st.session_state[model_key][item_key] = loaded_value
95
+ except (yaml.YAMLError, ValueError) as e:
96
+ st.error(f"Error parsing YAML for {item_key}: {e}")
97
+
98
+ elif field_type == str:
99
+ if len(value) > 100:
100
+ st.session_state[model_key][item_key] = st.text_area(
101
+ label, value=value, key=item_key
102
+ )
103
+ else:
104
+ st.session_state[model_key][item_key] = st.text_input(
105
+ label, value=value, key=item_key
106
+ )
107
+
108
+ elif field_type == dict:
109
+ yaml_str = st.text_area(
110
+ label + " (YAML format)", value=yaml.dump(value), key=item_key
111
+ )
112
+ try:
113
+ # Update the dictionary with the user-defined YAML
114
+ st.session_state[model_key][item_key] = (
115
+ yaml.safe_load(yaml_str) or {}
116
+ )
117
+ except yaml.YAMLError as e:
118
+ st.error(f"Error parsing YAML for {item_key}: {e}")
119
+
120
+ else:
121
+ st.write(f"Unsupported type for {item_key}: {field_type}")
122
+
123
+ # Iterate over keys of context except specific keys
124
+ keys_to_iterate = [
125
+ key
126
+ for key in (st.session_state[model_key]).keys()
127
+ if key not in ["id", "version", "label", "description", "enabled", "_package"]
128
+ ]
129
+
130
+ for item_key in keys_to_iterate:
131
+ render_fields(item_key, st.session_state[model_key][item_key])
132
+
133
+
134
+ def app_update_action(agent_id: str, action_id: str) -> None:
135
+ """Add a standard update button to apply changes."""
136
+
137
+ model_key = f"model_{agent_id}_{action_id}"
138
+
139
+ st.divider()
140
+
141
+ if st.button("Update"):
142
+ result = call_update_action(
143
+ agent_id=agent_id,
144
+ action_id=action_id,
145
+ action_data=st.session_state[model_key],
146
+ )
147
+ if result and result.get("id", "") == action_id:
148
+ st.success("Changes saved")
149
+ else:
150
+ st.error("Unable to save changes")
151
+
152
+
153
+ def dynamic_form(
154
+ field_definitions: list,
155
+ initial_data: Optional[list] = None,
156
+ session_key: str = "dynamic_form",
157
+ ) -> list:
158
+ """
159
+ Create a dynamic form widget with add/remove functionality.
160
+
161
+ Parameters:
162
+ - field_definitions: A list of dictionaries where each dictionary defines a field
163
+ with 'name', 'type', and any specific 'options' if needed.
164
+ - initial_data: A list of dictionaries to initialize the form with predefined values.
165
+ - session_key: A unique key to store and manage session state of the form.
166
+
167
+ Returns:
168
+ - list: The current value of the dynamic form.
169
+ """
170
+ if session_key not in st.session_state:
171
+ if initial_data is not None:
172
+ st.session_state[session_key] = []
173
+ for idx, row_data in enumerate(initial_data):
174
+ fields = {
175
+ field["name"]: row_data.get(field["name"], "")
176
+ for field in field_definitions
177
+ }
178
+ st.session_state[session_key].append({"id": idx, "fields": fields})
179
+ else:
180
+ st.session_state[session_key] = [
181
+ {"id": 0, "fields": {field["name"]: "" for field in field_definitions}}
182
+ ]
183
+
184
+ def add_row() -> None:
185
+ """Add a new row to the dynamic form."""
186
+ new_id = (
187
+ max((item["id"] for item in st.session_state[session_key]), default=-1) + 1
188
+ )
189
+ new_row = {
190
+ "id": new_id,
191
+ "fields": {field["name"]: "" for field in field_definitions},
192
+ }
193
+ st.session_state[session_key].append(new_row)
194
+
195
+ def remove_row(id_to_remove: int) -> None:
196
+ """Remove a row from the dynamic form."""
197
+ st.session_state[session_key] = [
198
+ item for item in st.session_state[session_key] if item["id"] != id_to_remove
199
+ ]
200
+
201
+ for item in st.session_state[session_key]:
202
+ # Display fields in a row
203
+ row_cols = st.columns(len(field_definitions))
204
+ for i, field in enumerate(field_definitions):
205
+ field_name = field["name"]
206
+ field_type = field.get("type", "text")
207
+ options = field.get("options", [])
208
+
209
+ if field_type == "text":
210
+ item["fields"][field_name] = row_cols[i].text_input(
211
+ field_name,
212
+ value=item["fields"][field_name],
213
+ key=f"{session_key}_{item['id']}_{field_name}",
214
+ )
215
+ elif field_type == "number":
216
+ field_value = item["fields"][field_name]
217
+ if field_value == "":
218
+ field_value = 0
219
+ item["fields"][field_name] = row_cols[i].number_input(
220
+ field_name,
221
+ value=int(field_value),
222
+ key=f"{session_key}_{item['id']}_{field_name}",
223
+ )
224
+ elif field_type == "select":
225
+ item["fields"][field_name] = row_cols[i].selectbox(
226
+ field_name,
227
+ options,
228
+ index=(
229
+ options.index(item["fields"][field_name])
230
+ if item["fields"][field_name] in options
231
+ else 0
232
+ ),
233
+ key=f"{session_key}_{item['id']}_{field_name}",
234
+ )
235
+
236
+ # Add a remove button in a new row beneath the fields, aligned to the left
237
+ with st.container():
238
+ if st.button(
239
+ "Remove",
240
+ key=f"remove_{item['id']}",
241
+ on_click=lambda id=item["id"]: remove_row(id),
242
+ ):
243
+ pass
244
+
245
+ # Add a divider above the "Add Row" button
246
+ st.divider()
247
+
248
+ # Button to add a new row
249
+ st.button("Add Row", on_click=add_row)
250
+
251
+ # Return the current value of the dynamic form
252
+ return [item["fields"] for item in st.session_state[session_key]]
@@ -0,0 +1 @@
1
+ """Streamlit pages for the JVCLI client module."""
@@ -0,0 +1,178 @@
1
+ """Renders the analytics page of the JVCLI client."""
2
+
3
+ import calendar
4
+ import datetime
5
+ import os
6
+
7
+ import pandas as pd
8
+ import requests
9
+ import streamlit as st
10
+ from streamlit.delta_generator import DeltaGenerator
11
+ from streamlit_javascript import st_javascript
12
+ from streamlit_router import StreamlitRouter
13
+
14
+ from jvcli.client.lib.utils import get_user_info
15
+
16
+ JIVAS_URL = os.environ.get("JIVAS_URL", "http://localhost:8000")
17
+
18
+
19
+ def render(router: StreamlitRouter) -> None:
20
+ """Render the analytics page."""
21
+ ctx = get_user_info()
22
+
23
+ st.header("Analytics", divider=True)
24
+ today = datetime.date.today()
25
+ last_day = calendar.monthrange(today.year, today.month)[1]
26
+
27
+ date_range = st.date_input(
28
+ "Period",
29
+ (
30
+ datetime.date(today.year, today.month, 1),
31
+ datetime.date(today.year, today.month, last_day),
32
+ ),
33
+ )
34
+
35
+ (start_date, end_date) = date_range
36
+
37
+ # rerender_metrics = render_metrics()
38
+ col1, col2, col3 = st.columns(3)
39
+ timezone = st_javascript(
40
+ """await (async () => {
41
+ const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
42
+ console.log(userTimezone)
43
+ return userTimezone
44
+ })().then(returnValue => returnValue)"""
45
+ )
46
+
47
+ try:
48
+ selected_agent = st.session_state.get("selected_agent")
49
+ if selected_agent and end_date > start_date:
50
+ interactions_chart(
51
+ token=ctx["token"],
52
+ agent_id=selected_agent["id"],
53
+ start_date=start_date,
54
+ end_date=end_date,
55
+ metric_col=col1,
56
+ timezone=timezone,
57
+ )
58
+ users_chart(
59
+ token=ctx["token"],
60
+ agent_id=selected_agent["id"],
61
+ start_date=start_date,
62
+ end_date=end_date,
63
+ metric_col=col2,
64
+ timezone=timezone,
65
+ )
66
+ channels_chart(
67
+ token=ctx["token"],
68
+ agent_id=selected_agent["id"],
69
+ start_date=start_date,
70
+ end_date=end_date,
71
+ metric_col=col3,
72
+ timezone=timezone,
73
+ )
74
+ else:
75
+ st.text("Invalid date range")
76
+ except Exception as e:
77
+ st.text("Unable to render charts")
78
+ print(e)
79
+
80
+
81
+ def interactions_chart(
82
+ start_date: datetime.date,
83
+ end_date: datetime.date,
84
+ agent_id: str,
85
+ token: str,
86
+ metric_col: DeltaGenerator,
87
+ timezone: str,
88
+ ) -> None:
89
+ """Render the interactions chart."""
90
+ url = f"{JIVAS_URL}/walker/get_interactions_by_date"
91
+
92
+ with st.container(border=True):
93
+ st.subheader("Interactions by Date")
94
+ response = requests.post(
95
+ url=url,
96
+ json={
97
+ "agent_id": agent_id,
98
+ "reporting": True,
99
+ "start_date": start_date.isoformat(),
100
+ "end_date": end_date.isoformat(),
101
+ "timezone": timezone,
102
+ },
103
+ headers={"Authorization": f"Bearer {token}"},
104
+ )
105
+ if response.status_code == 200:
106
+ if response_data := response.json():
107
+ chart_data = pd.DataFrame(
108
+ data=response_data["reports"][0]["data"],
109
+ )
110
+ st.line_chart(chart_data, x="date", y="count")
111
+ total = response_data["reports"][0]["total"]
112
+ metric_col.metric("Interactions", total)
113
+
114
+
115
+ def users_chart(
116
+ start_date: datetime.date,
117
+ end_date: datetime.date,
118
+ agent_id: str,
119
+ token: str,
120
+ metric_col: DeltaGenerator,
121
+ timezone: str,
122
+ ) -> None:
123
+ """Render the users chart."""
124
+ url = f"{JIVAS_URL}/walker/get_users_by_date"
125
+ with st.container(border=True):
126
+ st.subheader("Users by Date")
127
+ response = requests.post(
128
+ url=url,
129
+ json={
130
+ "agent_id": agent_id,
131
+ "reporting": True,
132
+ "start_date": start_date.isoformat(),
133
+ "end_date": end_date.isoformat(),
134
+ "timezone": timezone,
135
+ },
136
+ headers={"Authorization": f"Bearer {token}"},
137
+ )
138
+ if response.status_code == 200:
139
+ if response_data := response.json():
140
+ chart_data = pd.DataFrame(
141
+ data=response_data["reports"][0]["data"],
142
+ )
143
+ st.line_chart(chart_data, x="date", y="count")
144
+ total = response_data["reports"][0]["total"]
145
+ metric_col.metric("Users", total)
146
+
147
+
148
+ def channels_chart(
149
+ start_date: datetime.date,
150
+ end_date: datetime.date,
151
+ agent_id: str,
152
+ token: str,
153
+ metric_col: DeltaGenerator,
154
+ timezone: str,
155
+ ) -> None:
156
+ """Render the channels chart."""
157
+ url = f"{JIVAS_URL}/walker/get_channels_by_date"
158
+ with st.container(border=True):
159
+ st.subheader("Channels by Date")
160
+ response = requests.post(
161
+ url=url,
162
+ json={
163
+ "agent_id": agent_id,
164
+ "reporting": True,
165
+ "start_date": start_date.isoformat(),
166
+ "end_date": end_date.isoformat(),
167
+ "timezone": timezone,
168
+ },
169
+ headers={"Authorization": f"Bearer {token}"},
170
+ )
171
+ if response.status_code == 200:
172
+ if response_data := response.json():
173
+ chart_data = pd.DataFrame(
174
+ data=response_data["reports"][0]["data"],
175
+ )
176
+ st.line_chart(chart_data, x="date", y="count")
177
+ total = response_data["reports"][0]["total"]
178
+ metric_col.metric("Channels", total)
@@ -0,0 +1,143 @@
1
+ """Renders the chat page for the JVCLI client."""
2
+
3
+ import os
4
+ from typing import Optional
5
+
6
+ import requests
7
+ import streamlit as st
8
+ from streamlit_router import StreamlitRouter
9
+
10
+ from jvcli.client.lib.utils import get_user_info
11
+
12
+ JIVAS_URL = os.environ.get("JIVAS_URL", "http://localhost:8000")
13
+
14
+
15
+ def transcribe_audio(token: str, agent_id: str, file: bytes) -> dict:
16
+ """Transcribe audio using the walker API."""
17
+ action_walker_url = f"{JIVAS_URL}/action/walker"
18
+
19
+ # Create form data
20
+ files = {"attachments": ("audio.wav", file, "audio/wav")}
21
+
22
+ data = {
23
+ "args": "{}",
24
+ "module_root": "actions.jivas.deepgram_stt_action",
25
+ "agent_id": agent_id,
26
+ "walker": "transcribe_audio",
27
+ }
28
+
29
+ headers = {"Authorization": f"Bearer {token}"}
30
+
31
+ # Make the POST request
32
+ response = requests.post(
33
+ f"{action_walker_url}", headers=headers, data=data, files=files
34
+ )
35
+
36
+ # Parse JSON response
37
+ result = response.json()
38
+
39
+ return result
40
+
41
+
42
+ def render(router: StreamlitRouter) -> None:
43
+ """Render the chat page."""
44
+ url = f"{JIVAS_URL}/interact"
45
+ ctx = get_user_info()
46
+
47
+ st.header("Chat", divider=True)
48
+ tts_on = st.toggle("TTS")
49
+
50
+ audio_value = st.audio_input("Record a voice message")
51
+ if audio_value:
52
+ selected_agent = st.session_state.get("selected_agent")
53
+ result = transcribe_audio(ctx["token"], selected_agent, audio_value)
54
+ if result.get("success", False):
55
+ send_message(
56
+ result["transcript"], url, ctx["token"], selected_agent, tts_on
57
+ )
58
+
59
+ if selected_agent := st.query_params.get("agent"):
60
+ chat_messages = st.session_state.messages.get(selected_agent, [])
61
+
62
+ # Display chat messages from history on app rerun
63
+ for message in chat_messages:
64
+ with st.chat_message(message["role"]):
65
+ st.markdown(message["content"])
66
+ if payload := message.get("payload"):
67
+ with st.expander("...", False):
68
+ st.json(payload)
69
+
70
+ # Accept user input
71
+ if prompt := st.chat_input("Type your message here"):
72
+ send_message(prompt, url, ctx["token"], selected_agent, tts_on)
73
+
74
+
75
+ def send_message(
76
+ prompt: str,
77
+ url: str,
78
+ token: str,
79
+ selected_agent: Optional[str] = None,
80
+ tts_on: bool = False,
81
+ ) -> None:
82
+ """Send a message to the walker API and display the response."""
83
+ # Add user message to chat history
84
+ add_agent_message(selected_agent, {"role": "user", "content": prompt})
85
+
86
+ # Display user message in chat message container
87
+ with st.chat_message("user"):
88
+ st.markdown(prompt)
89
+
90
+ with st.chat_message("assistant"):
91
+ # Call walker API
92
+ response = requests.post(
93
+ url=url,
94
+ json={
95
+ "utterance": prompt,
96
+ "session_id": st.session_state.session_id,
97
+ "agent_id": selected_agent,
98
+ "tts": tts_on,
99
+ "verbose": True,
100
+ },
101
+ headers={"Authorization": f"Bearer {token}"},
102
+ )
103
+ if response.status_code == 200:
104
+ if response_data := response.json():
105
+ st.markdown(
106
+ response_data.get("response", {})
107
+ .get("message", {})
108
+ .get("content", "...")
109
+ )
110
+ if "audio_url" in response_data.get("response", {}) and tts_on:
111
+ audio_url = response_data.get("response", {}).get(
112
+ "audio_url", "..."
113
+ )
114
+ st.audio(audio_url, autoplay=True)
115
+ with st.expander("...", False):
116
+ st.json(response_data)
117
+
118
+ # Add assistant response to chat history
119
+ add_agent_message(
120
+ selected_agent,
121
+ {
122
+ "role": "assistant",
123
+ "content": response_data.get("response", {})
124
+ .get("message", {})
125
+ .get("content", "..."),
126
+ "payload": response_data,
127
+ },
128
+ )
129
+ if "session_id" in response_data.get("response", {}):
130
+ st.session_state.session_id = response_data["response"]["session_id"]
131
+
132
+
133
+ def add_agent_message(agent_id: Optional[str], message: dict) -> None:
134
+ """Add a message to the chat history for a specific agent."""
135
+ all_messages = st.session_state.messages
136
+ agent_messages = all_messages.get(agent_id, [])
137
+ agent_messages.append(message)
138
+ st.session_state.messages[agent_id] = agent_messages
139
+
140
+
141
+ def clear_messages() -> None:
142
+ """Clear all chat messages."""
143
+ st.session_state.messages = {}
@@ -0,0 +1,119 @@
1
+ """Render the dashboard page of the jvclient with actions data."""
2
+
3
+ import streamlit as st
4
+ from streamlit_elements import dashboard, elements, mui
5
+ from streamlit_router import StreamlitRouter
6
+
7
+ from jvcli.client.lib.page import Page
8
+
9
+
10
+ def render(router: StreamlitRouter) -> None:
11
+ """Render the dashboard page."""
12
+ if actions_data := st.session_state.get("actions_data"):
13
+ with elements("dashboard"):
14
+ columns = 4
15
+ layout = []
16
+
17
+ # Compute the position of each card component in the layout
18
+ for idx, _ in enumerate(actions_data):
19
+ x = (idx % columns) * 3
20
+ y = (idx // columns) * 2
21
+ width = 3
22
+ height = 2
23
+ # Add an item to the dashboard manually without using `with` if it's not a context manager
24
+ layout.append(
25
+ dashboard.Item(
26
+ f"card_{idx}",
27
+ x,
28
+ y,
29
+ width,
30
+ height,
31
+ isDraggable=False,
32
+ isResizable=False,
33
+ )
34
+ )
35
+
36
+ # now populate the actual cards with content
37
+ with dashboard.Grid(layout):
38
+ for idx, action in enumerate(actions_data):
39
+ package = action.get("_package", {})
40
+ title = package.get("meta", {}).get("title", action.get("label"))
41
+ description = action.get("description", "")
42
+ version = action.get("version", "0.0.0")
43
+ action_type = package.get("meta", {}).get("type", "action")
44
+ key = Page.normalize_label(title)
45
+ enabled_color = "red"
46
+ enabled_text = "(disabled)"
47
+ avatar_text = "A"
48
+
49
+ if action_type == "interact_action":
50
+ avatar_text = "I"
51
+
52
+ if action.get("enabled", False):
53
+ enabled_color = "green"
54
+ enabled_text = ""
55
+
56
+ # create the card
57
+ with mui.Card(
58
+ key=f"card_{idx}",
59
+ sx={
60
+ "display": "flex",
61
+ "flexDirection": "column",
62
+ "borderRadius": 2,
63
+ "overflow": "scroll",
64
+ },
65
+ elevation=2,
66
+ ):
67
+ # Card header with title
68
+ mui.CardHeader(
69
+ title=f"{title} {enabled_text}",
70
+ subheader=f"{version}",
71
+ avatar=mui.Avatar(
72
+ avatar_text, sx={"bgcolor": enabled_color}
73
+ ),
74
+ action=mui.IconButton(mui.icon.MoreVert),
75
+ )
76
+
77
+ # Card body
78
+ with mui.CardContent(sx={"flex": 1}):
79
+ mui.Typography(description, variant="body2")
80
+
81
+ # Card footer with action buttons
82
+ with mui.CardActions(disableSpacing=True):
83
+ query = st.query_params.to_dict()
84
+ query_str = ""
85
+ for k, v in query.items():
86
+ if k != "request":
87
+ query_str += f"{k}={v}&"
88
+
89
+ if package.get("config", {}).get("app", False):
90
+ with mui.Stack(
91
+ direction="row",
92
+ spacing=2,
93
+ alignItems="center",
94
+ sx={"padding": "10px"},
95
+ ):
96
+ mui.Button(
97
+ "Configure",
98
+ variant="outlined",
99
+ href=(
100
+ (
101
+ f"/?request=GET:/{key}&${query_str.rstrip('&')}"
102
+ if query_str
103
+ else f"/?request=GET:/{key}"
104
+ )
105
+ + f"&token={st.session_state.TOKEN}"
106
+ ),
107
+ target="_blank",
108
+ )
109
+
110
+
111
+ def logout() -> None:
112
+ """Logout the user by clearing the session token."""
113
+ del st.session_state["TOKEN"]
114
+ token_query = st.query_params.get("token")
115
+
116
+ if token_query:
117
+ query_params = st.query_params.to_dict()
118
+ del query_params["token"]
119
+ st.query_params.from_dict(query_params)