Skip to content

Commit

Permalink
feat: New eval no state
Browse files Browse the repository at this point in the history
  • Loading branch information
ramedina86 committed Dec 9, 2024
1 parent 1efc789 commit 6e37f35
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 12 deletions.
5 changes: 4 additions & 1 deletion src/ui/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function generateCore() {
const components: Ref<ComponentMap> = ref({});
const userFunctions: Ref<UserFunction[]> = ref([]);
const userState: Ref<Record<string, any>> = ref({});
const evaluatedExpressions: Ref<Record<string, any>> = ref({});
let webSocket: WebSocket;
const syncHealth: Ref<"idle" | "connected" | "offline" | "suspended"> =
ref("idle");
Expand Down Expand Up @@ -146,7 +147,7 @@ export function generateCore() {
*/

const mutationFlag = key.charAt(0);
const accessor = parseAccessor(key.substring(1));
const accessor = parseAccessor(key.substring(0));
const lastElementIndex = accessor.length - 1;
let stateRef = userState.value;

Expand Down Expand Up @@ -217,6 +218,7 @@ export function generateCore() {
message.messageType == "eventResponse" ||
message.messageType == "stateEnquiryResponse"
) {
evaluatedExpressions.value = message.payload?.evaluatedExpressions;
ingestMutations(message.payload?.mutations);
collateMail(message.payload?.mail);
ingestComponents(message.payload?.components);
Expand Down Expand Up @@ -567,6 +569,7 @@ export function generateCore() {
webSocket,
syncHealth,
frontendMessageMap: readonly(frontendMessageMap),
evaluatedExpressions: readonly(evaluatedExpressions),
mode: readonly(mode),
userFunctions: readonly(userFunctions),
addMailSubscription,
Expand Down
23 changes: 22 additions & 1 deletion src/ui/src/renderer/useEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Component, Core, FieldType, InstancePath } from "@/writerTypes";
export function useEvaluator(wf: Core) {
const templateRegex = /[\\]?@{([^}]*)}/g;

const expressionsTemplateRegex = /\{\{(.*?)\}\}/g;

/**
* Returns the expression as an array of static accessors.
* For example, turns a.b.c into ["a", "b", "c"].
Expand Down Expand Up @@ -119,7 +121,7 @@ export function useEvaluator(wf: Core) {
instancePath: InstancePath,
): string {
if (template === undefined || template === null) return "";
const evaluatedTemplate = template.replace(
let evaluatedTemplate = template.replace(
templateRegex,
(match, captured) => {
if (match.charAt(0) == "\\") return match.substring(1); // Escaped @, don't evaluate, return without \
Expand All @@ -139,6 +141,25 @@ export function useEvaluator(wf: Core) {
},
);

evaluatedTemplate = evaluatedTemplate.replaceAll(expressionsTemplateRegex, (match, captured) => {
if (match.charAt(0) == "\\") return match.substring(1);


const expr = captured.trim();
if (!expr) return "";

const exprValue = wf.evaluatedExpressions.value[expr];

if (typeof exprValue == "undefined") {
return "";
} else if (typeof exprValue == "object") {
return JSON.stringify(exprValue);
}

return exprValue.toString();

});

return evaluatedTemplate;
}

Expand Down
14 changes: 12 additions & 2 deletions src/writer/app_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,13 @@ def _handle_session_init(self, payload: InitSessionRequestPayload) -> InitSessio
session.session_state.add_log_entry(
"error", "Serialisation error", tb.format_exc())

self._execute_user_app_code(session.globals)

ui_component_tree = core_ui.export_component_tree(
session.session_component_tree, mode=writer.Config.mode)

res_payload = InitSessionResponsePayload(
userState=user_state,
userState=session.get_serialized_globals(),
sessionId=session.session_id,
mail=session.session_state.mail,
components=ui_component_tree,
Expand All @@ -176,6 +178,7 @@ def _handle_event(self, session: WriterSession, event: WriterEvent) -> EventResp
result = session.event_handler.handle(event)

mutations = {}
session.session_state.user_state.apply_mutation_marker(recursive=True)

try:
mutations = session.session_state.user_state.get_mutations_as_dict()
Expand All @@ -192,7 +195,8 @@ def _handle_event(self, session: WriterSession, event: WriterEvent) -> EventResp

res_payload = EventResponsePayload(
result=result,
mutations=mutations,
mutations=session.get_serialized_globals(),
evaluatedExpressions=session.get_serialized_evaluated_expressions(),
components=ui_component_tree,
mail=mail
)
Expand Down Expand Up @@ -315,6 +319,12 @@ def _handle_message(self, session_id: str, request: AppProcessServerRequest) ->

raise MessageHandlingException("Invalid event.")

def _execute_user_app_code(self, session_globals) -> Dict:
code_path = os.path.join(self.app_path, "main.py")
code = compile(self.run_code, code_path, "exec")
exec(code, session_globals)
return session_globals

def _execute_user_code(self) -> None:
"""
Executes the user code and captures standard output.
Expand Down
39 changes: 31 additions & 8 deletions src/writer/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,33 @@ def __init__(self, session_id: str, cookies: Optional[Dict[str, str]], headers:
self.headers = headers
self.last_active_timestamp: int = int(time.time())
new_state = WriterState.get_new()
new_state.user_state.mutated = set()
self.session_state = new_state
self.session_component_tree = core_ui.build_session_component_tree(base_component_tree)
self.globals = {}
self.event_handler = EventHandler(self)
self.userinfo: Optional[dict] = None

def update_last_active_timestamp(self) -> None:
self.last_active_timestamp = int(time.time())

def get_serialized_evaluated_expressions(self):
serializer = StateSerialiser()
evaluated_expressions = {}
expressions = self.session_component_tree.scan_expressions()
for expression in (expressions or {}):
print(f"evaluating {expression}")
try:
evaluated_expressions[expression] = serializer.serialise(eval(expression, self.globals))
except Exception as e:
evaluated_expressions[expression] = None
return evaluated_expressions

def get_serialized_globals(self):
serializer = StateSerialiser()
globals_excluding_builtins = {k: v for k, v in self.globals.items() if k != "__builtins__"}

return serializer.serialise(globals_excluding_builtins)


@dataclasses.dataclass
class MutationSubscription:
Expand Down Expand Up @@ -338,8 +356,10 @@ def serialise(self, v: Any) -> Union[Dict, List, str, bool, int, float, None]:
# Covers Altair charts, Plotly graphs
return self._serialise_dict_recursively(v.to_dict())

raise StateSerialiserException(
f"Object of type { type(v) } (MRO: {v_mro}) cannot be serialised.")
return f"Object of type { type(v) } (MRO: {v_mro}) cannot be serialised."

# raise StateSerialiserException(
# f"Object of type { type(v) } (MRO: {v_mro}) cannot be serialised.")

def _serialise_dict_recursively(self, d: Dict) -> Dict:
return {str(k): self.serialise(v) for k, v in d.items()}
Expand Down Expand Up @@ -1619,9 +1639,11 @@ def _get_handler_callable(self, handler: str) -> Optional[Callable]:
workflow_id = handler[17:]
return self._get_workflow_callable(workflow_id=workflow_id)

current_app_process = get_app_process()
handler_registry = current_app_process.handler_registry
callable_handler = handler_registry.find_handler_callable(handler)
callable_handler = self.session.globals[handler]

This comment has been minimized.

Copy link
@mmikita95

mmikita95 Dec 9, 2024

Contributor

Wondering if this change is compatible with handlers registered from outside main.py? I understand that, if imported directly, they're available as globals too, but we've also supported "chained" registrations - like module3 -> module2 (registers module3) -> module1 (registers module2) -> main (registers module1). This might require that main explicitly imports all modules?

This comment has been minimized.

Copy link
@ramedina86

ramedina86 via email Dec 9, 2024

Author Collaborator

# current_app_process = get_app_process()
# handler_registry = current_app_process.handler_registry
# callable_handler = handler_registry.find_handler_callable(handler)
return callable_handler

def _get_calling_arguments(self, ev: WriterEvent, instance_path: Optional[InstancePath] = None):
Expand All @@ -1645,8 +1667,9 @@ def _call_handler_callable(
captured_stdout = None
with core_ui.use_component_tree(self.session.session_component_tree), \
contextlib.redirect_stdout(io.StringIO()) as f:
middlewares_executors = current_app_process.middleware_registry.executors()
result = EventHandlerExecutor.invoke_with_middlewares(middlewares_executors, handler_callable, calling_arguments)
handler_callable()
# middlewares_executors = current_app_process.middleware_registry.executors()
# result = EventHandlerExecutor.invoke_with_middlewares(middlewares_executors, handler_callable, calling_arguments)
captured_stdout = f.getvalue()

if captured_stdout:
Expand Down
15 changes: 15 additions & 0 deletions src/writer/core_ui.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import contextlib
import copy
import logging
import re
import uuid
from contextvars import ContextVar
from enum import Enum
Expand Down Expand Up @@ -251,6 +252,20 @@ def to_dict(self) -> Dict:

return components

def scan_expressions(self) -> List:
pattern = re.compile(r"\{\{(.*?)\}\}")
trees = reversed(self.tree_branches)
expressions = []
for tree in trees:
all_components = tree.components.values()
for component in all_components:
for field_value in component.content.values():
matches = pattern.findall(field_value)
for match in matches:
expressions.append(match.strip())
return expressions


def next_page_id(self) -> str:
return f"page-{self.page_counter}"

Expand Down
1 change: 1 addition & 0 deletions src/writer/ss_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ class InitSessionResponse(AppProcessServerResponse):
class EventResponsePayload(BaseModel):
result: Any
mutations: Dict[str, Any]
evaluatedExpressions: Dict[str, Any]
mail: List
components: Optional[Dict] = None

Expand Down

0 comments on commit 6e37f35

Please sign in to comment.