Skip to content

Commit 898944f

Browse files
authored
Fix DataSourceSelector and add history download (#66)
1 parent 8afe85f commit 898944f

File tree

6 files changed

+127
-34
lines changed

6 files changed

+127
-34
lines changed

llmstack/base/flags.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ def get_flags(self):
5555
'CAN_ADD_TWILIO_INTERGRATION': [
5656
Condition('can_add_twilio_integration', True),
5757
],
58+
'CAN_EXPORT_HISTORY' : [
59+
Condition('can_export_history', False),
60+
]
5861
}
5962

6063
return flags
@@ -209,4 +212,8 @@ def has_exceeded_app_create_quota(value, request=None, **kwargs):
209212

210213
@conditions.register('can_add_twilio_integration')
211214
def can_add_twilio_integration(value, request=None, **kwargs):
212-
return True
215+
return True
216+
217+
@conditions.register('can_export_history')
218+
def can_export_history(value, request=None, **kwargs):
219+
return False

llmstack/client/src/components/apps/AppRunHistoryTimeline.jsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { ReactComponent as DiscordIcon } from "../../assets/images/icons/discord
3333
import { ReactComponent as SlackIcon } from "../../assets/images/icons/slack.svg";
3434
import "ace-builds/src-noconflict/mode-sh";
3535
import "ace-builds/src-noconflict/theme-chrome";
36+
import { profileFlagsState } from "../../data/atoms";
3637

3738
const browserAndOSFromUACache = {};
3839

@@ -300,6 +301,7 @@ const FilterBar = ({ apps, sessions, users, onFilter }) => {
300301
};
301302

302303
export function AppRunHistoryTimeline(props) {
304+
const profileFlags = useRecoilValue(profileFlagsState);
303305
const { filter, filteredColumns, showFilterBar } = props;
304306
const apps = useRecoilValue(appsState);
305307
const [rows, setRows] = useState([]);
@@ -452,6 +454,42 @@ export function AppRunHistoryTimeline(props) {
452454

453455
return (
454456
<Grid container spacing={1}>
457+
<Box sx={{ display: "flex", width: "100%", justifyContent: "end" }}>
458+
<Button
459+
disabled={profileFlags?.CAN_EXPORT_HISTORY !== true}
460+
onClick={() => {
461+
axios()
462+
.post(
463+
`/api/history/download`,
464+
{
465+
...filters,
466+
},
467+
{
468+
responseType: "blob",
469+
},
470+
)
471+
.then((response) => {
472+
const url = window.URL.createObjectURL(
473+
new Blob([response.data]),
474+
);
475+
const link = document.createElement("a");
476+
const pageNumber = filters.page || 1;
477+
link.href = url;
478+
link.setAttribute(
479+
"download",
480+
`history-${moment().format(
481+
"YYYY-MM-DD HH:MM",
482+
)}-${pageNumber}.csv`,
483+
);
484+
document.body.appendChild(link);
485+
link.click();
486+
});
487+
}}
488+
>
489+
Download CSV
490+
</Button>
491+
</Box>
492+
455493
<TableContainer sx={{ padding: "10px 20px" }}>
456494
{showFilterBar && (
457495
<Box>

llmstack/client/src/components/datasource/DataSourceSelector.jsx

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ import { useRecoilValue } from "recoil";
33
import { dataSourcesState, orgDataSourcesState } from "../../data/atoms";
44
import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
55
import { AddDataSourceModal } from "./AddDataSourceModal";
6-
import { Chip } from "@mui/material";
6+
import { Autocomplete, TextField } from "@mui/material";
77
import FormControl from "@mui/material/FormControl";
8-
import Select from "@mui/material/Select";
9-
import MenuItem from "@mui/material/MenuItem";
10-
import Input from "@mui/material/Input";
118

129
export function DataSourceSelector(props) {
1310
const dataSources = useRecoilValue(dataSourcesState);
@@ -24,43 +21,42 @@ export function DataSourceSelector(props) {
2421

2522
return (
2623
<FormControl fullWidth>
27-
<Select
28-
sx={{ border: "1px solid #ddd", borderRadius: 1 }}
29-
labelId="multiple-chip-label"
30-
id="multiple-chip"
24+
<Autocomplete
3125
multiple
26+
id="datasource-selector"
27+
options={[
28+
...uniqueDataSources,
29+
]}
30+
getOptionLabel={(option) => {
31+
const dataSource = uniqueDataSources.find(
32+
(uniqueDataSource) => uniqueDataSource.uuid === option)
33+
34+
return dataSource ? dataSource.name : option.name ? option.name : option;
35+
}}
36+
isOptionEqualToValue={(option, value) => {
37+
return option.uuid === value.uuid || option.uuid === value;
38+
}}
3239
value={
3340
props.value
3441
? typeof props.value === "string"
3542
? [props.value]
3643
: props.value
3744
: []
3845
}
39-
onChange={(event) => props.onChange(event.target.value)}
40-
input={<Input id="select-multiple-chip" />}
41-
renderValue={(selected) => (
42-
<div>
43-
{(typeof selected === "string" ? [selected] : selected).map(
44-
(value) => (
45-
<Chip
46-
key={value}
47-
label={
48-
uniqueDataSources.find((ds) => ds.uuid === value)?.name ||
49-
value
50-
}
51-
style={{ margin: 2, borderRadius: 5 }}
52-
/>
53-
),
54-
)}
55-
</div>
46+
renderInput={(params) => (
47+
<TextField
48+
{...params}
49+
variant="standard"
50+
label="Data Sources"
51+
placeholder="Data Sources"
52+
/>
5653
)}
57-
>
58-
{uniqueDataSources.map((dataSource) => (
59-
<MenuItem key={dataSource.uuid} value={dataSource.uuid}>
60-
{dataSource.name}
61-
</MenuItem>
62-
))}
63-
</Select>
54+
onChange={(event, value) => {
55+
props.onChange(
56+
value.map((dataSource) => dataSource?.uuid || dataSource),
57+
);
58+
}}
59+
/>
6460
<button
6561
onClick={() => setShowAddDataSourceModal(true)}
6662
style={{ backgroundColor: "#6287ac", color: "#fed766" }}

llmstack/processors/apis.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import asyncio
2+
from datetime import datetime
23
import json
34
import logging
45
import uuid
56
from collections import namedtuple
67
from django.conf import settings
8+
from django.core.paginator import Paginator, EmptyPage
9+
from flags.state import flag_enabled
710

811
from django.contrib.auth import authenticate
912
from django.contrib.auth import login
@@ -389,6 +392,52 @@ def list(self, request):
389392
)
390393

391394
return DRFResponse(response.data)
395+
396+
def get_csv(self, queryset):
397+
yield ','.join([
398+
'Request UUID', 'App UUID', 'Session Key', 'Request User Email', 'Request IP', 'Request Location', 'Request User Agent', 'Request Content Type', 'Request Body', 'Response Body', 'Created At',
399+
]) + '\n'
400+
for entry in queryset:
401+
logger.info(entry)
402+
yield ','.join([
403+
entry.request_uuid, entry.app_uuid, entry.session_key if entry.session_key else '', entry.request_user_email, entry.request_ip, entry.request_location, entry.request_user_agent, entry.request_content_type, json.dumps(entry.request_body), json.dumps(entry.response_body), entry.created_at.strftime('%Y-%m-%d %H:%M:%S'),
404+
]) + '\n'
405+
406+
def download(self, request):
407+
if not flag_enabled('CAN_EXPORT_HISTORY', request=request):
408+
return HttpResponseForbidden('You do not have permission to download history')
409+
410+
app_uuid = request.data.get('app_uuid', None)
411+
session_key = request.data.get('session_key', None)
412+
request_user_email = request.data.get('request_user_email', None)
413+
endpoint_uuid = request.data.get('endpoint_uuid', None)
414+
page_number = request.data.get('page', 1)
415+
416+
filters = {
417+
'owner': request.user,
418+
}
419+
if app_uuid and app_uuid != 'null':
420+
filters['app_uuid'] = app_uuid
421+
if session_key and session_key != 'null':
422+
filters['session_key'] = session_key
423+
if request_user_email and request_user_email != 'null':
424+
filters['request_user_email'] = request_user_email
425+
if endpoint_uuid and endpoint_uuid != 'null':
426+
filters['endpoint_uuid'] = endpoint_uuid
427+
428+
queryset = RunEntry.objects.all().filter(**filters).order_by('-created_at')
429+
paginator = Paginator(queryset, self.paginate_by)
430+
try:
431+
page = paginator.page(page_number)
432+
except EmptyPage:
433+
page = paginator.page(paginator.num_pages)
434+
435+
response = StreamingHttpResponse(
436+
streaming_content=self.get_csv(page), content_type='text/csv',
437+
)
438+
time_now = datetime.now().strftime('%Y-%m-%d')
439+
response['Content-Disposition'] = f'attachment; filename="promptly_{time_now}_{page_number}.csv"'
440+
return response
392441

393442
def list_sessions(self, request):
394443
app_uuid = request.GET.get('app_uuid', None)

llmstack/processors/providers/twilio/create_message.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,4 @@ def on_error(self, error: Any) -> None:
9292
return super().on_error(error)
9393

9494
def get_bookkeeping_data(self) -> BookKeepingData:
95-
return BookKeepingData(input=self._input, timestamp=time.time(), run_data={'twilio': {'requestor' : self._input._request.From}})
95+
return BookKeepingData(input=self._input, timestamp=time.time(), run_data={'twilio': {'requestor' : self._input.to}})

llmstack/processors/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141

4242
# History
4343
path('api/history', apis.HistoryViewSet.as_view({'get': 'list'})),
44+
45+
path('api/history/download', apis.HistoryViewSet.as_view({'post': 'download'})),
46+
4447
path(
4548
'api/history/sessions',
4649
apis.HistoryViewSet.as_view({'get': 'list_sessions'}),

0 commit comments

Comments
 (0)