Added Application HolidayViewer

This commit is contained in:
Kodjo Sossouvi
2025-06-27 07:26:58 +02:00
parent 66ea45f501
commit 9f4b8ab4d0
87 changed files with 3756 additions and 212 deletions

View File

@@ -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)

View File

@@ -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 */

View File

@@ -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}"}}',

View File

@@ -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,

View File

@@ -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):

View File

@@ -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"