Added Application HolidayViewer
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from fasthtml.fastapp import fast_app
|
||||
@@ -14,7 +15,28 @@ logger = logging.getLogger("Debugger")
|
||||
def post(session, _id: str, user_id: str, digest: str = None):
|
||||
logger.debug(f"Entering {Routes.DbEngineData} with args {debug_session(session)}, {_id=}, {user_id=}, {digest=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.add_tab(user_id, digest)
|
||||
return instance.db_engine_headers(user_id, digest)
|
||||
|
||||
|
||||
@rt(Routes.DbEngineDigest)
|
||||
def post(session, _id: str, user_id: str, digest: str):
|
||||
logger.debug(f"Entering {Routes.DbEngineDigest} with args {debug_session(session)}, {_id=}, {user_id=}, {digest=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.open_digest(user_id, digest)
|
||||
|
||||
|
||||
@rt(Routes.IaBuddyRequests)
|
||||
def post(session, _id: str, boundaries: str = None):
|
||||
logger.debug(f"Entering {Routes.IaBuddyRequests} with args {debug_session(session)}, {_id=}, {boundaries=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.ia_requests(json.loads(boundaries) if boundaries else None)
|
||||
|
||||
|
||||
@rt(Routes.IaBuddyRequest)
|
||||
def post(session, _id: str, request: str, boundaries: str = None):
|
||||
logger.debug(f"Entering {Routes.IaBuddyRequest} with args {debug_session(session)}, {_id=}, {request=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.ia_request(request, json.loads(boundaries) if boundaries else None)
|
||||
|
||||
|
||||
@rt(Routes.JsonViewerFold)
|
||||
@@ -23,10 +45,3 @@ def post(session, _id: str, node_id: str, folding: str):
|
||||
instance = InstanceManager.get(session, _id)
|
||||
instance.set_node_folding(node_id, folding)
|
||||
return instance.render_node(node_id)
|
||||
|
||||
@rt(Routes.JsonOpenDigest)
|
||||
def post(session, _id: str, user_id: str, digest: str):
|
||||
logger.debug(f"Entering {Routes.JsonOpenDigest} with args {debug_session(session)}, {_id=}, {user_id=}, {digest=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.open_digest(user_id, digest)
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
.mmt-jsonviewer {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
max-height: 100%
|
||||
}
|
||||
|
||||
/* Use inherited CSS variables for your custom theme */
|
||||
|
||||
@@ -11,7 +11,7 @@ class Commands:
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.DbEngineData}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'{{"_id": "{self._id}", "user_id": "{user_id}"}}',
|
||||
"hx-vals": f'js:{{"_id": "{self._id}", "user_id": "{user_id}", "boundaries": getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
|
||||
}
|
||||
|
||||
def db_engine_refs(self, ref_id: str | None):
|
||||
@@ -19,7 +19,23 @@ class Commands:
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.DbEngineRefs}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'{{"_id": "{self._id}", "ref_id": "{ref_id}"}}',
|
||||
"hx-vals": f'js:{{"_id": "{self._id}", "ref_id": "{ref_id}", "boundaries": getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
|
||||
}
|
||||
|
||||
def ia_buddy_requests(self):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.IaBuddyRequests}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}", "boundaries": getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
|
||||
}
|
||||
|
||||
def ia_buddy_request(self, request_id: str | None):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.IaBuddyRequest}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}", "request": "{request_id}", "boundaries": getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +54,7 @@ class JsonViewerCommands:
|
||||
|
||||
def open_digest(self, user_id, digest):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.JsonOpenDigest}",
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.DbEngineDigest}",
|
||||
"hx-target": f"#{self._owner.get_owner().tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'{{"_id": "{self._id}", "user_id": "{user_id}", "digest": "{digest}"}}',
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from fasthtml.components import *
|
||||
|
||||
from ai.debug_lmm import DebugConversation
|
||||
from components.BaseComponent import BaseComponent
|
||||
from components.aibuddy.assets.icons import icon_brain_ok
|
||||
from components.aibuddy.components.AIBuddy import AIBuddy
|
||||
from components.debugger.assets.icons import icon_dbengine
|
||||
from components.debugger.commands import Commands
|
||||
from components.debugger.components.JsonViewer import JsonViewer
|
||||
from components.debugger.constants import DBENGINE_DEBUGGER_INSTANCE_ID
|
||||
from components_helpers import mk_ellipsis, mk_icon
|
||||
from components_helpers import mk_ellipsis, mk_accordion_section
|
||||
from core.instance_manager import InstanceManager
|
||||
from core.utils import get_unique_id
|
||||
|
||||
@@ -22,43 +26,83 @@ class Debugger(BaseComponent):
|
||||
self.tabs_manager = tabs_manager
|
||||
self.commands = Commands(self)
|
||||
|
||||
def add_tab(self, user_id, digest):
|
||||
content = self.mk_db_engine_object(user_id, digest)
|
||||
def ia_requests(self, boundaries: dict):
|
||||
def _mk_span(conversation, helper):
|
||||
return Span(
|
||||
f"{datetime.fromtimestamp(conversation.start_time).strftime('%Y-%m-%d %H:%M:%S')} - {conversation.initial_prompt}",
|
||||
cls=helper.class_string,
|
||||
**self.commands.ia_buddy_request(conversation.id))
|
||||
|
||||
ia_buddy = InstanceManager.get(self._session, AIBuddy.create_component_id(self._session))
|
||||
conversations = ia_buddy.get_conversations()
|
||||
|
||||
logger.debug(f"mk_ia_requests_object: {conversations}")
|
||||
|
||||
hook = (lambda key, node, helper: isinstance(node.value, DebugConversation),
|
||||
lambda key, node, helper: _mk_span(node.value, helper))
|
||||
|
||||
jsonviewer = InstanceManager.get(self._session,
|
||||
JsonViewer.create_component_id(self._session, prefix=self._id),
|
||||
JsonViewer,
|
||||
owner=self,
|
||||
user_id=None,
|
||||
data=ia_buddy.get_conversations(),
|
||||
hooks=[hook],
|
||||
boundaries=boundaries)
|
||||
return self._add_tab(f"debugger-iabuddy-requests", "AI requests", jsonviewer)
|
||||
|
||||
def ia_request(self, request_id, boundaries: dict):
|
||||
def _mk_text_area(value, helper):
|
||||
return Textarea(value, cls="textarea textarea-sm w-full p-2", disabled=True)
|
||||
|
||||
ia_buddy = InstanceManager.get(self._session, AIBuddy.create_component_id(self._session))
|
||||
requests = ia_buddy.get_conversations()
|
||||
request = next((req for req in requests if req.id == request_id), None)
|
||||
|
||||
logger.debug(f"request: {request}")
|
||||
if request is None:
|
||||
return None
|
||||
|
||||
hook = (lambda key, node, helper: key in ("extended_prompt", "response"),
|
||||
lambda key, node, helper: _mk_text_area(node.value, helper))
|
||||
|
||||
jsonviewer = InstanceManager.get(self._session,
|
||||
JsonViewer.create_component_id(self._session, prefix=self._id),
|
||||
JsonViewer,
|
||||
owner=self,
|
||||
user_id=None,
|
||||
data=request.to_dict(),
|
||||
hooks=[hook],
|
||||
boundaries=boundaries)
|
||||
return self._add_tab(f"debugger-iabuddy-{request_id}", f"Request {request.initial_prompt}", jsonviewer)
|
||||
|
||||
def db_engine_headers(self, user_id, digest):
|
||||
data = self.db_engine.debug_load(user_id, digest) if digest else self.db_engine.debug_head(user_id)
|
||||
logger.debug(f"mk_db_engine: {data}")
|
||||
|
||||
tab_key = f"debugger-dbengine-{digest}"
|
||||
title = f"DBEngine-{digest if digest else 'head'}"
|
||||
self.tabs_manager.add_tab(title, content, key=tab_key)
|
||||
return self.tabs_manager.render()
|
||||
|
||||
def mk_db_engine_object(self, user_id, digest):
|
||||
data = self.db_engine.debug_load(user_id, digest) if digest else self.db_engine.debug_head(user_id)
|
||||
|
||||
logger.debug(f"mk_db_engine: {data}")
|
||||
return InstanceManager.get(self._session,
|
||||
JsonViewer.create_component_id(self._session, prefix=self._id),
|
||||
JsonViewer,
|
||||
owner=self,
|
||||
user_id=user_id,
|
||||
data=data)
|
||||
jsonviewer = InstanceManager.get(self._session,
|
||||
JsonViewer.create_component_id(self._session, prefix=self._id),
|
||||
JsonViewer,
|
||||
owner=self,
|
||||
user_id=user_id,
|
||||
data=data,
|
||||
key=tab_key)
|
||||
return self._add_tab(tab_key, title, jsonviewer)
|
||||
|
||||
def mk_db_engine(self, selected):
|
||||
return Div(
|
||||
Input(type="radio",
|
||||
name=f"dbengine-accordion-{self._id}",
|
||||
checked="checked" if selected else None,
|
||||
cls="p-0! min-h-0!",
|
||||
),
|
||||
Div(
|
||||
mk_icon(icon_dbengine, can_select=False), mk_ellipsis("DbEngine", cls="text-sm"),
|
||||
cls="collapse-title p-0 min-h-0 flex truncate",
|
||||
),
|
||||
Div(
|
||||
*[Div(user_id, **self.commands.db_engine_data(user_id)) for user_id in self.db_engine.debug_users()],
|
||||
Div("refs", **self.commands.db_engine_refs(None)),
|
||||
cls="collapse-content pr-0! truncate",
|
||||
),
|
||||
cls="collapse mb-2",
|
||||
id=f"db_engine_{self._id}",
|
||||
)
|
||||
content = [Div(user_id, **self.commands.db_engine_data(user_id)) for user_id in self.db_engine.debug_users()]
|
||||
content.append(Div("refs", **self.commands.db_engine_refs(None)))
|
||||
return mk_accordion_section(self._id, "DBEngine", icon_dbengine, content, selected)
|
||||
|
||||
def mk_ia(self, selected):
|
||||
ia_request = Div("requests", **self.commands.ia_buddy_requests())
|
||||
return mk_accordion_section(self._id, "IABuddy", icon_brain_ok, [ia_request], selected)
|
||||
|
||||
def _add_tab(self, tab_key, title, content):
|
||||
self.tabs_manager.add_tab(title, content, key=tab_key)
|
||||
return self.tabs_manager.render()
|
||||
|
||||
def __ft__(self):
|
||||
return Div(
|
||||
@@ -66,7 +110,7 @@ class Debugger(BaseComponent):
|
||||
mk_ellipsis("Debugger", cls="text-sm font-medium mb-1"),
|
||||
Div(
|
||||
self.mk_db_engine(True),
|
||||
cls="flex truncate",
|
||||
self.mk_ia(False),
|
||||
),
|
||||
|
||||
id=self._id,
|
||||
|
||||
@@ -9,6 +9,7 @@ from components.datagrid_new.components.DataGrid import DataGrid
|
||||
from components.debugger.assets.icons import icon_expanded, icon_collapsed, icon_class
|
||||
from components.debugger.commands import JsonViewerCommands
|
||||
from components.debugger.constants import INDENT_SIZE, MAX_TEXT_LENGTH, NODE_OBJECT, NODES_KEYS_TO_NOT_EXPAND
|
||||
from components_helpers import set_boundaries
|
||||
from core.serializer import TAG_OBJECT
|
||||
from core.utils import get_unique_id
|
||||
|
||||
@@ -42,13 +43,31 @@ class DictNode(Node):
|
||||
children: dict[str, Node] = dataclasses.field(default_factory=dict)
|
||||
|
||||
|
||||
class JsonViewerHelper:
|
||||
class_string = f"mmt-jsonviewer-string"
|
||||
class_bool = f"mmt-jsonviewer-bool"
|
||||
class_number = f"mmt-jsonviewer-number"
|
||||
class_null = f"mmt-jsonviewer-null"
|
||||
class_digest = f"mmt-jsonviewer-digest"
|
||||
class_object = f"mmt-jsonviewer-object"
|
||||
class_dataframe = f"mmt-jsonviewer-dataframe"
|
||||
|
||||
@staticmethod
|
||||
def is_sha256(_value):
|
||||
return (isinstance(_value, str) and
|
||||
len(_value) == 64 and
|
||||
all(c in '0123456789abcdefABCDEF' for c in _value))
|
||||
|
||||
|
||||
class JsonViewer(BaseComponent):
|
||||
def __init__(self, session, _id, owner, user_id, data):
|
||||
def __init__(self, session, _id, owner, user_id, data, hooks=None, key=None, boundaries=None):
|
||||
super().__init__(session, _id)
|
||||
self._key = key
|
||||
self._owner = owner # debugger component
|
||||
self.user_id = user_id
|
||||
self.data = data
|
||||
self._node_id = -1
|
||||
self._boundaries = boundaries if boundaries else {"height": "600"}
|
||||
self._commands = JsonViewerCommands(self)
|
||||
|
||||
# A little explanation on how the folding / unfolding work
|
||||
@@ -62,6 +81,12 @@ class JsonViewer(BaseComponent):
|
||||
self._nodes_by_id = {}
|
||||
|
||||
self.node = self._create_node(None, data)
|
||||
|
||||
# hooks are used to define specific rendering
|
||||
# They are tuple (Predicate, Element to render (eg Div))
|
||||
self.hooks = hooks or []
|
||||
|
||||
self._helper = JsonViewerHelper()
|
||||
|
||||
def set_node_folding(self, node_id, folding):
|
||||
if folding == self._folding_mode:
|
||||
@@ -84,7 +109,7 @@ class JsonViewer(BaseComponent):
|
||||
return self._owner
|
||||
|
||||
def open_digest(self, user_id: str, digest: str):
|
||||
return self._owner.add_tab(user_id, digest)
|
||||
return self._owner.db_engine_headers(user_id, digest)
|
||||
|
||||
def _create_node(self, key, data, level=0):
|
||||
if isinstance(data, list):
|
||||
@@ -115,17 +140,18 @@ class JsonViewer(BaseComponent):
|
||||
return node
|
||||
|
||||
def _must_expand(self, node):
|
||||
if not isinstance(node, (ListNode, DictNode)):
|
||||
return None
|
||||
|
||||
if self._folding_mode == FoldingMode.COLLAPSE:
|
||||
return node.node_id in self._nodes_to_track
|
||||
else:
|
||||
return node.node_id not in self._nodes_to_track
|
||||
|
||||
def _mk_folding(self, node: Node):
|
||||
if not isinstance(node, (ListNode, DictNode)):
|
||||
def _mk_folding(self, node: Node, must_expand: bool | None):
|
||||
if must_expand is None:
|
||||
return None
|
||||
|
||||
must_expand = self._must_expand(node)
|
||||
|
||||
return Span(icon_expanded if must_expand else icon_collapsed,
|
||||
cls="icon-16-inline mmt-jsonviewer-folding",
|
||||
style=f"margin-left: -{INDENT_SIZE}px;",
|
||||
@@ -136,15 +162,20 @@ class JsonViewer(BaseComponent):
|
||||
self._node_id += 1
|
||||
return f"{self._id}-{self._node_id}"
|
||||
|
||||
def _render_value(self, node):
|
||||
def _is_sha256(_value):
|
||||
return isinstance(_value, str) and len(_value) == 64 and all(
|
||||
c in '0123456789abcdefABCDEF' for c in _value)
|
||||
def _render_value(self, key, node, must_expand):
|
||||
if must_expand is False:
|
||||
return Span("[...]" if isinstance(node, ListNode) else "{...}",
|
||||
id=node.node_id,
|
||||
**self._commands.fold(node.node_id, FoldingMode.EXPAND))
|
||||
|
||||
for predicate, renderer in self.hooks:
|
||||
if predicate(key, node, self._helper):
|
||||
return renderer(key, node, self._helper)
|
||||
|
||||
if isinstance(node, DictNode):
|
||||
return self._render_dict(node)
|
||||
return self._render_dict(key, node)
|
||||
elif isinstance(node, ListNode):
|
||||
return self._render_list(node)
|
||||
return self._render_list(key, node)
|
||||
else:
|
||||
data_tooltip = None
|
||||
htmx_params = {}
|
||||
@@ -159,7 +190,7 @@ class JsonViewer(BaseComponent):
|
||||
elif node.value is None:
|
||||
str_value = "null"
|
||||
data_class = "null"
|
||||
elif _is_sha256(node.value):
|
||||
elif self._helper.is_sha256(node.value):
|
||||
str_value = str(node.value)
|
||||
data_class = "digest"
|
||||
htmx_params = self._commands.open_digest(self.user_id, node.value)
|
||||
@@ -194,24 +225,22 @@ class JsonViewer(BaseComponent):
|
||||
|
||||
return Span(str_value, cls=cls, data_tooltip=data_tooltip, **htmx_params)
|
||||
|
||||
def _render_dict(self, node: DictNode):
|
||||
if self._must_expand(node):
|
||||
return Span("{",
|
||||
*[
|
||||
self._render_node(key, value)
|
||||
for key, value in node.children.items()
|
||||
],
|
||||
Div("}"),
|
||||
id=node.node_id)
|
||||
else:
|
||||
return Span("{...}", id=node.node_id)
|
||||
def _render_dict(self, key, node: DictNode):
|
||||
return Span("{",
|
||||
*[
|
||||
self._render_node(child_key, value)
|
||||
for child_key, value in node.children.items()
|
||||
],
|
||||
Div("}"),
|
||||
id=node.node_id)
|
||||
|
||||
def _render_list(self, node: ListNode):
|
||||
def _all_the_same(_node):
|
||||
def _render_list(self, key, node: ListNode):
|
||||
def _all_the_same(_key, _node):
|
||||
if len(_node.children) == 0:
|
||||
return False
|
||||
|
||||
sample_value = _node.children[0].value
|
||||
sample_node = _node.children[0]
|
||||
sample_value = sample_node.value
|
||||
|
||||
if sample_value is None:
|
||||
return False
|
||||
@@ -220,6 +249,11 @@ class JsonViewer(BaseComponent):
|
||||
if type_ in (int, float, str, bool, list, dict, ValueNode):
|
||||
return False
|
||||
|
||||
# a specific rendering is specified
|
||||
for predicate, renderer in self.hooks:
|
||||
if predicate(_key, sample_node, self._helper):
|
||||
return False
|
||||
|
||||
return all(type(item.value) == type_ for item in _node.children)
|
||||
|
||||
def _render_as_grid(_node):
|
||||
@@ -244,27 +278,40 @@ class JsonViewer(BaseComponent):
|
||||
Div("]"),
|
||||
)
|
||||
|
||||
if self._must_expand(node):
|
||||
if _all_the_same(node):
|
||||
return _render_as_grid(node)
|
||||
return _render_as_list(node)
|
||||
else:
|
||||
return Span("[...]", id=node.node_id)
|
||||
return _render_as_grid(node) if _all_the_same(key, node) else _render_as_list(node)
|
||||
|
||||
def _render_node(self, key, node):
|
||||
must_expand = self._must_expand(node) # to be able to update the folding when the node is updated
|
||||
return Div(
|
||||
self._mk_folding(node),
|
||||
|
||||
self._mk_folding(node, must_expand),
|
||||
Span(f'{key} : ') if key is not None else None,
|
||||
self._render_value(node),
|
||||
self._render_value(key, node, must_expand),
|
||||
|
||||
style=f"margin-left: {INDENT_SIZE}px;",
|
||||
id=node.node_id if hasattr(node, "node_id") else None,
|
||||
)
|
||||
|
||||
def __ft__(self):
|
||||
return Div(
|
||||
Div(self._render_node(None, self.node), id=f"{self._id}-root"),
|
||||
Div(self._render_node(None, self.node),
|
||||
id=f"{self._id}-root",
|
||||
style="margin-left: 0px;"),
|
||||
cls="mmt-jsonviewer",
|
||||
id=f"{self._id}")
|
||||
id=f"{self._id}",
|
||||
**set_boundaries(self._boundaries),
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(other) is type(self):
|
||||
return self._key is not None and self._key == other._key
|
||||
else:
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._key) if self._key is not None else super().__hash__()
|
||||
|
||||
|
||||
|
||||
@staticmethod
|
||||
def add_quotes(value: str):
|
||||
|
||||
@@ -10,5 +10,7 @@ NODES_KEYS_TO_NOT_EXPAND = ["Dataframe", "__parent__"]
|
||||
class Routes:
|
||||
DbEngineData = "/dbengine-data"
|
||||
DbEngineRefs = "/dbengine-refs"
|
||||
DbEngineDigest = "/dbengine-digest"
|
||||
IaBuddyRequests = "/iabuddy-requests"
|
||||
IaBuddyRequest = "/iabuddy-request"
|
||||
JsonViewerFold = "/jsonviewer-fold"
|
||||
JsonOpenDigest = "/jsonviewer-open-digest"
|
||||
|
||||
Reference in New Issue
Block a user