Skip to content

Commit dc4c047

Browse files
committed
Execute app_runs
1 parent bdba159 commit dc4c047

File tree

4 files changed

+164
-23
lines changed

4 files changed

+164
-23
lines changed

llmstack/sheets/apis.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from llmstack.sheets.models import (
1111
PromptlySheet,
1212
PromptlySheetCell,
13+
PromptlySheetColumn,
1314
PromptlySheetRunEntry,
1415
)
1516
from llmstack.sheets.serializers import PromptlySheetSerializer
@@ -98,7 +99,9 @@ def patch(self, request, sheet_uuid=None):
9899

99100
if "data" in request.data:
100101
if "columns" in request.data["data"]:
101-
sheet.data["columns"] = request.data["data"]["columns"]
102+
sheet.data["columns"] = [
103+
PromptlySheetColumn(**column_data).model_dump() for column_data in request.data["data"]["columns"]
104+
]
102105

103106
if "total_rows" in request.data["data"]:
104107
sheet.data["total_rows"] = request.data["data"]["total_rows"]
@@ -138,7 +141,7 @@ def run_async(self, request, sheet_uuid=None):
138141

139142
job = PromptlySheetAppExecuteJob.create(
140143
func="llmstack.sheets.tasks.run_sheet",
141-
args=[sheet, run_entry],
144+
args=[sheet, run_entry, request.user],
142145
).add_to_queue()
143146

144147
return DRFResponse({"job_id": job.id, "run_id": run_entry.uuid}, status=202)

llmstack/sheets/consumers.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
from channels.db import database_sync_to_async
55
from channels.generic.websocket import AsyncWebsocketConsumer
66

7-
from llmstack.sheets.models import PromptlySheet
8-
97
logger = logging.getLogger(__name__)
108

119

1210
@database_sync_to_async
1311
def update_sheet_channel_name(sheet_id, channel_name):
12+
from llmstack.sheets.models import PromptlySheet
13+
1414
# Update the channel name for the sheet
1515
sheet = PromptlySheet.objects.get(uuid=sheet_id)
1616
sheet_extra_data = sheet.extra_data or {}
@@ -22,18 +22,21 @@ def update_sheet_channel_name(sheet_id, channel_name):
2222
class SheetAppConsumer(AsyncWebsocketConsumer):
2323
async def connect(self):
2424
self.sheet_id = self.scope["url_route"]["kwargs"]["sheet_id"]
25+
self.run_id = self.scope["url_route"]["kwargs"]["run_id"]
2526
self._user = self.scope.get("user", None)
2627
# Update the channel name for the sheet
27-
await update_sheet_channel_name(self.sheet_id, self.channel_name)
28+
await self.channel_layer.group_add(self.run_id, self.channel_name)
2829
await super().connect()
2930

3031
async def disconnect(self, close_code):
31-
await update_sheet_channel_name(self.sheet_id, None)
32+
await self.channel_layer.group_discard(self.run_id, self.channel_name)
3233

3334
async def close(self, code=None, reason=None):
3435
await update_sheet_channel_name(self.sheet_id, None)
3536
await super().close(code, reason)
3637

37-
# Called when a message is received on the channel
38-
async def send_message(self, event):
38+
async def cell_update(self, event):
39+
await self.send(text_data=json.dumps(event))
40+
41+
async def cell_updating(self, event):
3942
await self.send(text_data=json.dumps(event))

llmstack/sheets/models.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import json
33
import logging
44
import uuid
5-
from typing import List
5+
from enum import Enum
6+
from typing import List, Optional
67

78
from django.db import models
89
from django.db.models.signals import post_delete
@@ -14,20 +15,51 @@
1415
logger = logging.getLogger(__name__)
1516

1617

18+
class PromptlySheetColumnType(str, Enum):
19+
TEXT = "text"
20+
NUMBER = "number"
21+
BOOLEAN = "boolean"
22+
DATE = "date"
23+
TIME = "time"
24+
DATETIME = "datetime"
25+
DURATION = "duration"
26+
PERCENTAGE = "percentage"
27+
CURRENCY = "currency"
28+
URL = "url"
29+
EMAIL = "email"
30+
PHONE = "phone"
31+
IMAGE = "image"
32+
FILE = "file"
33+
FORMULA = "formula"
34+
APP_RUN = "app_run"
35+
PROCESSOR_RUN = "processor_run"
36+
37+
def __str__(self):
38+
return self.value
39+
40+
41+
class PromptlySheetColumn(BaseModel):
42+
title: str
43+
kind: PromptlySheetColumnType = PromptlySheetColumnType.TEXT
44+
data: dict = {}
45+
col: int
46+
width: Optional[int] = None
47+
48+
1749
class PromptlySheetCell(BaseModel):
1850
row: int
1951
col: int
2052
data: str = ""
2153
formula: str = ""
22-
kind: str = "string"
54+
kind: PromptlySheetColumnType = PromptlySheetColumnType.TEXT
2355

2456
@property
2557
def is_formula(self):
2658
return bool(self.formula)
2759

2860
@property
2961
def cell_id(self):
30-
return f"{self.col}-{self.row}"
62+
return f"{self.row}-{self.col}"
3163

3264
def model_dump(
3365
self,
@@ -132,7 +164,10 @@ def get_sheet_cells(objref):
132164
(
133165
dict(
134166
map(
135-
lambda cell_entry: (int(cell_entry[0]), PromptlySheetCell(**cell_entry[1])),
167+
lambda cell_entry: (
168+
int(cell_entry[0]),
169+
PromptlySheetCell(**cell_entry[1]),
170+
),
136171
row_entry[1].items(),
137172
)
138173
)
@@ -185,12 +220,12 @@ def cells(self):
185220
def rows(self):
186221
for objref in self.data.get("cells", []):
187222
cells = get_sheet_cells(objref)
188-
for cell_row in cells:
189-
yield (cell_row, cells[cell_row])
223+
for row, row_cells in cells.items():
224+
yield (row, row_cells)
190225

191226
@property
192227
def columns(self):
193-
return self.data.get("columns", [])
228+
return [PromptlySheetColumn(**column) for column in self.data.get("columns", [])]
194229

195230
def save(self, *args, **kwargs):
196231
if "cells" in kwargs:

llmstack/sheets/tasks.py

Lines changed: 108 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,117 @@
1-
from llmstack.sheets.apis import PromptlySheetViewSet
1+
import logging
2+
import uuid
23

4+
from asgiref.sync import async_to_sync
5+
from channels.layers import get_channel_layer
6+
from django.contrib.auth.models import User
7+
from django.test import RequestFactory
8+
from rest_framework.response import Response as DRFResponse
39

4-
def process_sheet_execute_request(user_email, sheet_id):
5-
from django.contrib.auth.models import User
6-
from django.test import RequestFactory
10+
from llmstack.common.utils.utils import hydrate_input
11+
from llmstack.sheets.models import PromptlySheet, PromptlySheetCell, PromptlySheetColumn
12+
from llmstack.sheets.serializers import PromptlySheetSerializer
713

8-
user = User.objects.get(email=user_email)
14+
try:
15+
from promptly.promptly_app_store.apis import AppStoreAppViewSet
16+
except ImportError:
17+
from llmstack.app_store.apis import AppStoreAppViewSet
18+
19+
20+
logger = logging.getLogger(__name__)
21+
channel_layer = get_channel_layer()
22+
23+
24+
def number_to_letters(num):
25+
letters = ""
26+
while num >= 0:
27+
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[num % 26] + letters
28+
num = num // 26 - 1
29+
return letters
30+
31+
32+
def _execute_cell(
33+
cell: PromptlySheetCell,
34+
column: PromptlySheetColumn,
35+
row: dict[str, PromptlySheetCell],
36+
sheet: PromptlySheet,
37+
run_id: str,
38+
user: User,
39+
) -> PromptlySheetCell:
40+
if column.kind != "app_run":
41+
return cell
42+
43+
async_to_sync(channel_layer.group_send)(run_id, {"type": "cell.updating", "cell": {"id": cell.cell_id}})
44+
45+
app_slug = column.data["app_slug"]
46+
input_values = {number_to_letters(col): cell.data for col, cell in row.items()}
47+
input = hydrate_input(column.data["input"], input_values)
948

1049
request = RequestFactory().post(
11-
f"/api/sheets/{sheet_id}/execute",
50+
f"/api/store/apps/{app_slug}",
1251
format="json",
1352
)
53+
request.data = {
54+
"stream": False,
55+
"input": input,
56+
}
1457
request.user = user
15-
response = PromptlySheetViewSet().execute(request, sheet_uuid=sheet_id)
1658

17-
return {"status_code": response.status_code, "data": response}
59+
# Execute the app
60+
response = async_to_sync(AppStoreAppViewSet().run_app_internal_async)(
61+
slug=app_slug,
62+
session_id=None,
63+
request_uuid=str(uuid.uuid4()),
64+
request=request,
65+
)
66+
67+
output = response.get("output", "")
68+
async_to_sync(channel_layer.group_send)(
69+
run_id, {"type": "cell.update", "cell": {"id": cell.cell_id, "data": response.get("output", "")}}
70+
)
71+
cell.data = output
72+
73+
return cell
74+
75+
76+
def run_sheet(sheet, run_entry, user):
77+
try:
78+
processed_cells = []
79+
existing_rows = list(sheet.rows)
80+
existing_cols = sheet.columns
81+
82+
for row_number in range(sheet.data.get("total_rows", 0)):
83+
# Find existing row and the cell
84+
existing_row = next(filter(lambda row: row[0] == row_number, existing_rows), None)
85+
existing_row_cells = existing_row[1] if existing_row else {}
86+
87+
for column in existing_cols:
88+
if column.kind != "app_run":
89+
if existing_row:
90+
existing_cell = next(
91+
filter(lambda cell: cell.col == column.col, existing_row_cells.values()), None
92+
)
93+
if existing_cell:
94+
processed_cells.append(existing_cell)
95+
continue
96+
97+
# Create a new cell
98+
cell_to_execute = PromptlySheetCell(
99+
row=row_number,
100+
col=column.col,
101+
kind=column.kind,
102+
)
103+
processed_cells.append(
104+
_execute_cell(cell_to_execute, column, existing_row_cells, sheet, str(run_entry.uuid), user)
105+
)
106+
107+
if processed_cells:
108+
sheet.save(cells=processed_cells, update_fields=["updated_at"])
109+
# Store the processed data in sheet runs table
110+
run_entry.save(cells=processed_cells)
111+
112+
except Exception:
113+
logger.exception("Error executing sheet")
114+
115+
sheet.is_locked = False
116+
sheet.save(update_fields=["is_locked"])
117+
return DRFResponse(PromptlySheetSerializer(instance=sheet).data)

0 commit comments

Comments
 (0)