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 +1 -1
- jvcli/cli.py +2 -0
- jvcli/client/__init__.py +1 -0
- jvcli/client/app.py +160 -0
- jvcli/client/lib/__init__.py +1 -0
- jvcli/client/lib/page.py +68 -0
- jvcli/client/lib/utils.py +334 -0
- jvcli/client/lib/widgets.py +252 -0
- jvcli/client/pages/__init__.py +1 -0
- jvcli/client/pages/analytics_page.py +178 -0
- jvcli/client/pages/chat_page.py +143 -0
- jvcli/client/pages/dashboard_page.py +119 -0
- jvcli/client/pages/graph_page.py +20 -0
- jvcli/commands/client.py +50 -0
- jvcli/commands/create.py +5 -5
- jvcli/commands/publish.py +12 -1
- jvcli/commands/studio.py +1 -1
- {jvcli-2.0.1.dist-info → jvcli-2.0.3.dist-info}/METADATA +5 -1
- jvcli-2.0.3.dist-info/RECORD +44 -0
- jvcli-2.0.1.dist-info/RECORD +0 -32
- /jvcli/{client → studio}/assets/index-BtFItD2q.js +0 -0
- /jvcli/{client → studio}/assets/index-CIEsu-TC.css +0 -0
- /jvcli/{client → studio}/index.html +0 -0
- /jvcli/{client → studio}/jac_logo.png +0 -0
- /jvcli/{client → studio}/tauri.svg +0 -0
- /jvcli/{client → studio}/vite.svg +0 -0
- {jvcli-2.0.1.dist-info → jvcli-2.0.3.dist-info}/LICENSE +0 -0
- {jvcli-2.0.1.dist-info → jvcli-2.0.3.dist-info}/WHEEL +0 -0
- {jvcli-2.0.1.dist-info → jvcli-2.0.3.dist-info}/entry_points.txt +0 -0
- {jvcli-2.0.1.dist-info → jvcli-2.0.3.dist-info}/top_level.txt +0 -0
@@ -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)
|