jvcli 2.0.31__py3-none-any.whl → 2.1.1__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 +4 -2
- jvcli/cli.py +2 -6
- jvcli/commands/client.py +14 -38
- jvcli/commands/create.py +91 -86
- jvcli/commands/server.py +0 -6
- jvcli/commands/startproject.py +1 -1
- jvcli/templates/{2.0.0 → 2.1.1}/project/README.md +4 -40
- jvcli/templates/{2.0.0 → 2.1.1}/project/actions/README.md +1 -1
- jvcli/templates/{2.0.0 → 2.1.1}/project/daf/README.md +1 -1
- jvcli/templates/{2.0.0 → 2.1.1}/project/env.example +7 -4
- jvcli/templates/2.1.1/project/main.jac +2 -0
- jvcli/templates/2.1.1/sourcefiles/action_app.py +23 -0
- jvcli/templates/2.1.1/sourcefiles/action_archetype.jac +49 -0
- jvcli/templates/{2.0.0 → 2.1.1/sourcefiles}/action_info.yaml +1 -1
- jvcli/templates/2.1.1/sourcefiles/action_lib.jac +3 -0
- jvcli/templates/{2.0.0 → 2.1.1/sourcefiles}/agent_descriptor.yaml +4 -4
- jvcli/templates/2.1.1/sourcefiles/interact_action_archetype.jac +58 -0
- jvcli/utils.py +1 -1
- {jvcli-2.0.31.dist-info → jvcli-2.1.1.dist-info}/METADATA +8 -47
- jvcli-2.1.1.dist-info/RECORD +40 -0
- jvcli/client/__init__.py +0 -1
- jvcli/client/app.py +0 -188
- jvcli/client/lib/__init__.py +0 -1
- jvcli/client/lib/page.py +0 -68
- jvcli/client/lib/utils.py +0 -312
- jvcli/client/lib/widgets.py +0 -295
- jvcli/client/pages/__init__.py +0 -1
- jvcli/client/pages/action_dashboard_page.py +0 -120
- jvcli/client/pages/analytics_page.py +0 -245
- jvcli/client/pages/chat_page.py +0 -150
- jvcli/client/pages/graph_page.py +0 -20
- jvcli/commands/clean.py +0 -29
- jvcli/commands/studio.py +0 -258
- jvcli/studio/assets/index-DDV79SDu.js +0 -213
- jvcli/studio/assets/index-DdMMONxd.css +0 -1
- jvcli/studio/index.html +0 -15
- jvcli/studio/jac_logo.png +0 -0
- jvcli/studio/tauri.svg +0 -6
- jvcli/studio/vite.svg +0 -1
- jvcli/studio-auth/assets/index-Bh6lyeXA.js +0 -218
- jvcli/studio-auth/assets/index-DdMMONxd.css +0 -1
- jvcli/studio-auth/index.html +0 -15
- jvcli/studio-auth/jac_logo.png +0 -0
- jvcli/studio-auth/tauri.svg +0 -6
- jvcli/studio-auth/vite.svg +0 -1
- jvcli/templates/2.0.0/project/main.jac +0 -2
- jvcli-2.0.31.dist-info/RECORD +0 -61
- /jvcli/templates/{2.0.0 → 2.1.1}/project/gitignore.example +0 -0
- /jvcli/templates/{2.0.0 → 2.1.1}/project/globals.jac +0 -0
- /jvcli/templates/{2.0.0 → 2.1.1}/project/tests/README.md +0 -0
- /jvcli/templates/{CHANGELOG.md → 2.1.1/sourcefiles/CHANGELOG.md} +0 -0
- /jvcli/templates/{README.md → 2.1.1/sourcefiles/README.md} +0 -0
- /jvcli/templates/{2.0.0 → 2.1.1/sourcefiles}/agent_info.yaml +0 -0
- /jvcli/templates/{2.0.0 → 2.1.1/sourcefiles}/agent_knowledge.yaml +0 -0
- /jvcli/templates/{2.0.0 → 2.1.1/sourcefiles}/agent_memory.yaml +0 -0
- {jvcli-2.0.31.dist-info → jvcli-2.1.1.dist-info}/WHEEL +0 -0
- {jvcli-2.0.31.dist-info → jvcli-2.1.1.dist-info}/entry_points.txt +0 -0
- {jvcli-2.0.31.dist-info → jvcli-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {jvcli-2.0.31.dist-info → jvcli-2.1.1.dist-info}/top_level.txt +0 -0
jvcli/client/lib/utils.py
DELETED
@@ -1,312 +0,0 @@
|
|
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_BASE_URL = os.environ.get("JIVAS_BASE_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_api(
|
54
|
-
endpoint: str,
|
55
|
-
method: str = "POST",
|
56
|
-
headers: Optional[Dict] = None,
|
57
|
-
json_data: Optional[Dict] = None,
|
58
|
-
files: Optional[List] = None,
|
59
|
-
data: Optional[Dict] = None,
|
60
|
-
timeout: int = 10,
|
61
|
-
) -> Optional[requests.Response]:
|
62
|
-
"""Generic function to call an API endpoint."""
|
63
|
-
|
64
|
-
if not endpoint.startswith("http"):
|
65
|
-
endpoint = f"{JIVAS_BASE_URL}/{endpoint}"
|
66
|
-
|
67
|
-
ctx = get_user_info() # Assumes a function that fetches user info
|
68
|
-
|
69
|
-
if ctx.get("token"):
|
70
|
-
try:
|
71
|
-
headers = headers or {}
|
72
|
-
headers["Authorization"] = f"Bearer {ctx['token']}"
|
73
|
-
|
74
|
-
response = requests.request(
|
75
|
-
method=method,
|
76
|
-
url=endpoint,
|
77
|
-
headers=headers,
|
78
|
-
json=json_data,
|
79
|
-
files=files,
|
80
|
-
data=data,
|
81
|
-
timeout=timeout,
|
82
|
-
)
|
83
|
-
|
84
|
-
if response.status_code == 401:
|
85
|
-
st.session_state.EXPIRATION = ""
|
86
|
-
return None
|
87
|
-
|
88
|
-
return response
|
89
|
-
|
90
|
-
except Exception as e:
|
91
|
-
st.session_state.EXPIRATION = ""
|
92
|
-
st.write(e)
|
93
|
-
|
94
|
-
return None
|
95
|
-
|
96
|
-
|
97
|
-
def call_action_walker_exec(
|
98
|
-
agent_id: str,
|
99
|
-
module_root: str,
|
100
|
-
walker: str,
|
101
|
-
args: Optional[Dict] = None,
|
102
|
-
files: Optional[List] = None,
|
103
|
-
headers: Optional[Dict] = None,
|
104
|
-
) -> list:
|
105
|
-
"""Call the API to execute a walker action for a given agent."""
|
106
|
-
|
107
|
-
endpoint = f"{JIVAS_BASE_URL}/action/walker"
|
108
|
-
|
109
|
-
# Create form data
|
110
|
-
data = {"agent_id": agent_id, "module_root": module_root, "walker": walker}
|
111
|
-
|
112
|
-
if args:
|
113
|
-
data["args"] = json.dumps(args)
|
114
|
-
|
115
|
-
file_list = []
|
116
|
-
|
117
|
-
if files:
|
118
|
-
for file in files:
|
119
|
-
file_list.append(("attachments", (file[0], file[1], file[2])))
|
120
|
-
|
121
|
-
response = call_api(endpoint, headers=headers, data=data, files=file_list)
|
122
|
-
|
123
|
-
if response is not None and response.status_code == 200:
|
124
|
-
result = response.json()
|
125
|
-
return result if result else []
|
126
|
-
|
127
|
-
return []
|
128
|
-
|
129
|
-
|
130
|
-
def call_healthcheck(agent_id: str, headers: Optional[Dict] = None) -> Optional[dict]:
|
131
|
-
"""Call the API to check the health of an agent."""
|
132
|
-
endpoint = "walker/healthcheck"
|
133
|
-
json_data = {"agent_id": agent_id}
|
134
|
-
response = call_api(endpoint, headers=headers, json_data=json_data)
|
135
|
-
|
136
|
-
if response is not None and response.status_code in [200, 501, 503]:
|
137
|
-
result = response.json()
|
138
|
-
reports = result.get("reports", [])
|
139
|
-
return reports[0] if reports else {}
|
140
|
-
|
141
|
-
return {}
|
142
|
-
|
143
|
-
|
144
|
-
def call_list_agents(headers: Optional[Dict] = None) -> list:
|
145
|
-
"""Call the API to list agents."""
|
146
|
-
endpoint = "walker/list_agents"
|
147
|
-
json_data = {"reporting": True}
|
148
|
-
response = call_api(endpoint, headers=headers, json_data=json_data)
|
149
|
-
|
150
|
-
if response is not None and response.status_code == 200:
|
151
|
-
result = response.json()
|
152
|
-
reports = result.get("reports", [])
|
153
|
-
return [
|
154
|
-
{"id": agent.get("id", ""), "label": agent.get("name", "")}
|
155
|
-
for agent in reports
|
156
|
-
]
|
157
|
-
|
158
|
-
return []
|
159
|
-
|
160
|
-
|
161
|
-
def call_get_agent(agent_id: str, headers: Optional[Dict] = None) -> dict:
|
162
|
-
"""Call the API to get details of a specific agent."""
|
163
|
-
endpoint = "walker/get_agent"
|
164
|
-
json_data = {"agent_id": agent_id}
|
165
|
-
response = call_api(endpoint, headers=headers, json_data=json_data)
|
166
|
-
|
167
|
-
if response is not None and response.status_code == 200:
|
168
|
-
result = response.json()
|
169
|
-
reports = result.get("reports", [])
|
170
|
-
return reports[0] if reports else {}
|
171
|
-
|
172
|
-
return {}
|
173
|
-
|
174
|
-
|
175
|
-
def call_list_actions(agent_id: str, headers: Optional[Dict] = None) -> list:
|
176
|
-
"""Call the API to list actions for a given agent."""
|
177
|
-
endpoint = "walker/list_actions"
|
178
|
-
json_data = {"agent_id": agent_id}
|
179
|
-
response = call_api(endpoint, headers=headers, json_data=json_data)
|
180
|
-
|
181
|
-
if response is not None and response.status_code == 200:
|
182
|
-
result = response.json()
|
183
|
-
reports = result.get("reports", [])
|
184
|
-
return reports[0] if reports else []
|
185
|
-
|
186
|
-
return []
|
187
|
-
|
188
|
-
|
189
|
-
def call_get_action(
|
190
|
-
agent_id: str, action_id: str, headers: Optional[Dict] = None
|
191
|
-
) -> dict:
|
192
|
-
"""Call the API to get a specific action for a given agent."""
|
193
|
-
endpoint = "walker/get_action"
|
194
|
-
json_data = {"agent_id": agent_id, "action_id": action_id}
|
195
|
-
response = call_api(endpoint, headers=headers, json_data=json_data)
|
196
|
-
|
197
|
-
if response is not None and response.status_code == 200:
|
198
|
-
result = response.json()
|
199
|
-
reports = result.get("reports", [])
|
200
|
-
return reports[0] if reports else {}
|
201
|
-
|
202
|
-
return {}
|
203
|
-
|
204
|
-
|
205
|
-
def call_update_action(
|
206
|
-
agent_id: str, action_id: str, action_data: dict, headers: Optional[Dict] = None
|
207
|
-
) -> dict:
|
208
|
-
"""Call the API to update a specific action for a given agent."""
|
209
|
-
endpoint = "walker/update_action"
|
210
|
-
json_data = {
|
211
|
-
"agent_id": agent_id,
|
212
|
-
"action_id": action_id,
|
213
|
-
"action_data": action_data,
|
214
|
-
}
|
215
|
-
response = call_api(endpoint, headers=headers, json_data=json_data)
|
216
|
-
|
217
|
-
if response is not None and response.status_code == 200:
|
218
|
-
result = response.json()
|
219
|
-
reports = result.get("reports", [])
|
220
|
-
return reports[0] if reports else {}
|
221
|
-
|
222
|
-
return {}
|
223
|
-
|
224
|
-
|
225
|
-
def call_update_agent(
|
226
|
-
agent_id: str, agent_data: dict, headers: Optional[Dict] = None
|
227
|
-
) -> dict:
|
228
|
-
"""Call the API to update a specific agent."""
|
229
|
-
endpoint = "walker/update_agent"
|
230
|
-
json_data = {"agent_id": agent_id, "agent_data": agent_data}
|
231
|
-
response = call_api(endpoint, headers=headers, json_data=json_data)
|
232
|
-
|
233
|
-
if response is not None and response.status_code == 200:
|
234
|
-
result = response.json()
|
235
|
-
reports = result.get("reports", [])
|
236
|
-
return reports[0] if reports else {}
|
237
|
-
|
238
|
-
return {}
|
239
|
-
|
240
|
-
|
241
|
-
def call_import_agent(descriptor: str, headers: Optional[Dict] = None) -> list:
|
242
|
-
"""Call the API to import an agent."""
|
243
|
-
endpoint = "walker/import_agent"
|
244
|
-
json_data = {"descriptor": descriptor}
|
245
|
-
response = call_api(endpoint, headers=headers, json_data=json_data)
|
246
|
-
|
247
|
-
if response is not None and response.status_code == 200:
|
248
|
-
result = response.json()
|
249
|
-
reports = result.get("reports", [])
|
250
|
-
return reports[0] if reports else []
|
251
|
-
|
252
|
-
return []
|
253
|
-
|
254
|
-
|
255
|
-
def get_user_info() -> dict:
|
256
|
-
"""Get user information from the session state."""
|
257
|
-
return {
|
258
|
-
"root_id": st.session_state.get("ROOT_ID", ""),
|
259
|
-
"token": st.session_state.get("TOKEN", ""),
|
260
|
-
"expiration": st.session_state.get("EXPIRATION", ""),
|
261
|
-
}
|
262
|
-
|
263
|
-
|
264
|
-
def decode_base64_image(base64_string: str) -> Image:
|
265
|
-
"""Decode a base64 string into an image."""
|
266
|
-
# Decode the base64 string
|
267
|
-
image_data = base64.b64decode(base64_string)
|
268
|
-
|
269
|
-
# Create a bytes buffer from the decoded bytes
|
270
|
-
image_buffer = BytesIO(image_data)
|
271
|
-
|
272
|
-
# Open the image using PIL
|
273
|
-
return Image.open(image_buffer)
|
274
|
-
|
275
|
-
|
276
|
-
class LongStringDumper(yaml.SafeDumper):
|
277
|
-
"""Custom YAML dumper to handle long strings."""
|
278
|
-
|
279
|
-
def represent_scalar(
|
280
|
-
self, tag: str, value: str, style: Optional[str] = None
|
281
|
-
) -> yaml.ScalarNode:
|
282
|
-
"""Represent scalar values, using block style for long strings."""
|
283
|
-
# Replace any escape sequences to format the output as desired
|
284
|
-
if (
|
285
|
-
len(value) > 150 or "\n" in value
|
286
|
-
): # Adjust the threshold for long strings as needed
|
287
|
-
style = "|"
|
288
|
-
# converts all newline escapes to actual representations
|
289
|
-
value = "\n".join([line.rstrip() for line in value.split("\n")])
|
290
|
-
else:
|
291
|
-
# converts all newline escapes to actual representations
|
292
|
-
value = "\n".join([line.rstrip() for line in value.split("\n")]).rstrip()
|
293
|
-
|
294
|
-
return super().represent_scalar(tag, value, style)
|
295
|
-
|
296
|
-
|
297
|
-
def jac_yaml_dumper(
|
298
|
-
data: Any,
|
299
|
-
indent: int = 2,
|
300
|
-
default_flow_style: bool = False,
|
301
|
-
allow_unicode: bool = True,
|
302
|
-
sort_keys: bool = False,
|
303
|
-
) -> str:
|
304
|
-
"""Dumps YAML data using LongStringDumper with customizable options."""
|
305
|
-
return yaml.dump(
|
306
|
-
data,
|
307
|
-
Dumper=LongStringDumper,
|
308
|
-
indent=indent,
|
309
|
-
default_flow_style=default_flow_style,
|
310
|
-
allow_unicode=allow_unicode,
|
311
|
-
sort_keys=sort_keys,
|
312
|
-
)
|
jvcli/client/lib/widgets.py
DELETED
@@ -1,295 +0,0 @@
|
|
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
|
-
def update_action() -> None:
|
39
|
-
st.session_state[model_key]
|
40
|
-
call_update_action(
|
41
|
-
agent_id=agent_id,
|
42
|
-
action_id=action_id,
|
43
|
-
action_data=st.session_state[model_key],
|
44
|
-
)
|
45
|
-
|
46
|
-
current_state = st.session_state[model_key]["enabled"]
|
47
|
-
new_state = st.checkbox(
|
48
|
-
"Enabled",
|
49
|
-
key="enabled",
|
50
|
-
value=current_state,
|
51
|
-
)
|
52
|
-
|
53
|
-
if new_state != current_state:
|
54
|
-
st.session_state[model_key]["enabled"] = new_state
|
55
|
-
update_action()
|
56
|
-
st.rerun()
|
57
|
-
|
58
|
-
return model_key, module_root
|
59
|
-
|
60
|
-
|
61
|
-
def snake_to_title(snake_str: str) -> str:
|
62
|
-
"""Convert a snake_case string to Title Case."""
|
63
|
-
return snake_str.replace("_", " ").title()
|
64
|
-
|
65
|
-
|
66
|
-
def app_controls(
|
67
|
-
agent_id: str,
|
68
|
-
action_id: str,
|
69
|
-
hidden: Optional[list] = None,
|
70
|
-
masked: Optional[list] = None,
|
71
|
-
) -> None:
|
72
|
-
"""Render the app controls for a given agent and action."""
|
73
|
-
if hidden is None:
|
74
|
-
hidden = []
|
75
|
-
if masked is None:
|
76
|
-
masked = []
|
77
|
-
|
78
|
-
# Generate a dynamic key for the session state using the action_id
|
79
|
-
model_key = f"model_{agent_id}_{action_id}"
|
80
|
-
|
81
|
-
# Combine default masked keys with additional keys specified in 'masked'
|
82
|
-
default_masked_keys = [
|
83
|
-
"password",
|
84
|
-
"token",
|
85
|
-
"api_key",
|
86
|
-
"key",
|
87
|
-
"secret",
|
88
|
-
"secret_key",
|
89
|
-
]
|
90
|
-
all_masked_keys = set(default_masked_keys + masked)
|
91
|
-
|
92
|
-
# Recursive function to handle nested dictionaries
|
93
|
-
def render_fields(item_key: str, value: Any, parent_key: str = "") -> None:
|
94
|
-
"""Render fields based on their type."""
|
95
|
-
|
96
|
-
# Skip rendering if the field is in the hidden list
|
97
|
-
if item_key in hidden:
|
98
|
-
return
|
99
|
-
|
100
|
-
field_type = type(value)
|
101
|
-
label = snake_to_title(item_key) # Convert item_key to Title Case
|
102
|
-
|
103
|
-
# Special case for masked fields to render as a password field
|
104
|
-
if item_key.lower() in all_masked_keys:
|
105
|
-
st.session_state[model_key][item_key] = st.text_input(
|
106
|
-
label, value=value, type="password", key=item_key
|
107
|
-
)
|
108
|
-
|
109
|
-
elif field_type == int:
|
110
|
-
st.session_state[model_key][item_key] = st.number_input(
|
111
|
-
label, value=value, step=1, key=item_key
|
112
|
-
)
|
113
|
-
|
114
|
-
elif field_type == float:
|
115
|
-
st.session_state[model_key][item_key] = st.number_input(
|
116
|
-
label, value=value, step=0.01, key=item_key
|
117
|
-
)
|
118
|
-
|
119
|
-
elif field_type == bool:
|
120
|
-
st.session_state[model_key][item_key] = st.checkbox(
|
121
|
-
label, value=value, key=item_key
|
122
|
-
)
|
123
|
-
|
124
|
-
elif field_type == list:
|
125
|
-
yaml_str = st.text_area(
|
126
|
-
label + " (YAML format)",
|
127
|
-
value=yaml.dump(value, sort_keys=False),
|
128
|
-
key=item_key,
|
129
|
-
)
|
130
|
-
try:
|
131
|
-
# Update the list with the user-defined YAML
|
132
|
-
loaded_value = yaml.safe_load(yaml_str)
|
133
|
-
if not isinstance(loaded_value, list):
|
134
|
-
raise ValueError("The provided YAML does not produce a list.")
|
135
|
-
st.session_state[model_key][item_key] = loaded_value
|
136
|
-
except (yaml.YAMLError, ValueError) as e:
|
137
|
-
st.error(f"Error parsing YAML for {item_key}: {e}")
|
138
|
-
|
139
|
-
elif field_type == str:
|
140
|
-
if len(value) > 100:
|
141
|
-
st.session_state[model_key][item_key] = st.text_area(
|
142
|
-
label, value=value, key=item_key
|
143
|
-
)
|
144
|
-
else:
|
145
|
-
st.session_state[model_key][item_key] = st.text_input(
|
146
|
-
label, value=value, key=item_key
|
147
|
-
)
|
148
|
-
|
149
|
-
elif field_type == dict:
|
150
|
-
yaml_str = st.text_area(
|
151
|
-
label + " (YAML format)",
|
152
|
-
value=yaml.dump(value, sort_keys=False),
|
153
|
-
key=item_key,
|
154
|
-
)
|
155
|
-
try:
|
156
|
-
# Update the dictionary with the user-defined YAML
|
157
|
-
st.session_state[model_key][item_key] = yaml.safe_load(yaml_str) or {}
|
158
|
-
except yaml.YAMLError as e:
|
159
|
-
st.error(f"Error parsing YAML for {item_key}: {e}")
|
160
|
-
|
161
|
-
else:
|
162
|
-
st.write(f"Unsupported type for {item_key}: {field_type}")
|
163
|
-
|
164
|
-
# Iterate over keys of context except specific keys
|
165
|
-
keys_to_iterate = [
|
166
|
-
key
|
167
|
-
for key in (st.session_state[model_key]).keys()
|
168
|
-
if key not in ["id", "version", "label", "description", "enabled", "_package"]
|
169
|
-
]
|
170
|
-
|
171
|
-
for item_key in keys_to_iterate:
|
172
|
-
render_fields(item_key, st.session_state[model_key][item_key])
|
173
|
-
|
174
|
-
|
175
|
-
def app_update_action(agent_id: str, action_id: str) -> None:
|
176
|
-
"""Add a standard update button to apply changes."""
|
177
|
-
|
178
|
-
model_key = f"model_{agent_id}_{action_id}"
|
179
|
-
|
180
|
-
st.divider()
|
181
|
-
|
182
|
-
if st.button("Update"):
|
183
|
-
result = call_update_action(
|
184
|
-
agent_id=agent_id,
|
185
|
-
action_id=action_id,
|
186
|
-
action_data=st.session_state[model_key],
|
187
|
-
)
|
188
|
-
if result and result.get("id", "") == action_id:
|
189
|
-
st.success("Changes saved")
|
190
|
-
else:
|
191
|
-
st.error("Unable to save changes")
|
192
|
-
|
193
|
-
|
194
|
-
def dynamic_form(
|
195
|
-
field_definitions: list,
|
196
|
-
initial_data: Optional[list] = None,
|
197
|
-
session_key: str = "dynamic_form",
|
198
|
-
) -> list:
|
199
|
-
"""
|
200
|
-
Create a dynamic form widget with add/remove functionality.
|
201
|
-
|
202
|
-
Parameters:
|
203
|
-
- field_definitions: A list of dictionaries where each dictionary defines a field
|
204
|
-
with 'name', 'type', and any specific 'options' if needed.
|
205
|
-
- initial_data: A list of dictionaries to initialize the form with predefined values.
|
206
|
-
- session_key: A unique key to store and manage session state of the form.
|
207
|
-
|
208
|
-
Returns:
|
209
|
-
- list: The current value of the dynamic form.
|
210
|
-
"""
|
211
|
-
if session_key not in st.session_state:
|
212
|
-
if initial_data is not None:
|
213
|
-
st.session_state[session_key] = []
|
214
|
-
for idx, row_data in enumerate(initial_data):
|
215
|
-
fields = {
|
216
|
-
field["name"]: row_data.get(field["name"], "")
|
217
|
-
for field in field_definitions
|
218
|
-
}
|
219
|
-
st.session_state[session_key].append({"id": idx, "fields": fields})
|
220
|
-
else:
|
221
|
-
st.session_state[session_key] = [
|
222
|
-
{"id": 0, "fields": {field["name"]: "" for field in field_definitions}}
|
223
|
-
]
|
224
|
-
|
225
|
-
def add_row() -> None: # pragma: no cover, don't know how to test this yet 😅
|
226
|
-
"""Add a new row to the dynamic form."""
|
227
|
-
new_id = (
|
228
|
-
max((item["id"] for item in st.session_state[session_key]), default=-1) + 1
|
229
|
-
)
|
230
|
-
new_row = {
|
231
|
-
"id": new_id,
|
232
|
-
"fields": {field["name"]: "" for field in field_definitions},
|
233
|
-
}
|
234
|
-
st.session_state[session_key].append(new_row)
|
235
|
-
|
236
|
-
def remove_row(
|
237
|
-
id_to_remove: int,
|
238
|
-
) -> None: # pragma: no cover, don't know how to test this yet 😅
|
239
|
-
"""Remove a row from the dynamic form."""
|
240
|
-
st.session_state[session_key] = [
|
241
|
-
item for item in st.session_state[session_key] if item["id"] != id_to_remove
|
242
|
-
]
|
243
|
-
|
244
|
-
for item in st.session_state[session_key]:
|
245
|
-
# Display fields in a row
|
246
|
-
row_cols = st.columns(len(field_definitions))
|
247
|
-
for i, field in enumerate(field_definitions):
|
248
|
-
field_name = field["name"]
|
249
|
-
field_type = field.get("type", "text")
|
250
|
-
options = field.get("options", [])
|
251
|
-
|
252
|
-
if field_type == "text":
|
253
|
-
item["fields"][field_name] = row_cols[i].text_input(
|
254
|
-
field_name,
|
255
|
-
value=item["fields"][field_name],
|
256
|
-
key=f"{session_key}_{item['id']}_{field_name}",
|
257
|
-
)
|
258
|
-
elif field_type == "number":
|
259
|
-
field_value = item["fields"][field_name]
|
260
|
-
if field_value == "":
|
261
|
-
field_value = 0
|
262
|
-
item["fields"][field_name] = row_cols[i].number_input(
|
263
|
-
field_name,
|
264
|
-
value=int(field_value),
|
265
|
-
key=f"{session_key}_{item['id']}_{field_name}",
|
266
|
-
)
|
267
|
-
elif field_type == "select":
|
268
|
-
item["fields"][field_name] = row_cols[i].selectbox(
|
269
|
-
field_name,
|
270
|
-
options,
|
271
|
-
index=(
|
272
|
-
options.index(item["fields"][field_name])
|
273
|
-
if item["fields"][field_name] in options
|
274
|
-
else 0
|
275
|
-
),
|
276
|
-
key=f"{session_key}_{item['id']}_{field_name}",
|
277
|
-
)
|
278
|
-
|
279
|
-
# Add a remove button in a new row beneath the fields, aligned to the left
|
280
|
-
with st.container():
|
281
|
-
if st.button(
|
282
|
-
"Remove",
|
283
|
-
key=f"remove_{item['id']}",
|
284
|
-
on_click=lambda id=item["id"]: remove_row(id),
|
285
|
-
):
|
286
|
-
pass
|
287
|
-
|
288
|
-
# Add a divider above the "Add Row" button
|
289
|
-
st.divider()
|
290
|
-
|
291
|
-
# Button to add a new row
|
292
|
-
st.button("Add Row", on_click=add_row)
|
293
|
-
|
294
|
-
# Return the current value of the dynamic form
|
295
|
-
return [item["fields"] for item in st.session_state[session_key]]
|
jvcli/client/pages/__init__.py
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
"""Streamlit pages for the JVCLI client module."""
|