Skip to content

Commit

Permalink
Detect JSON in output and use code block for it and make long outputs…
Browse files Browse the repository at this point in the history
… scrollable (#306)

* wip

* Detect JSON is outputs and make long outputs scrollable

* fix tests
  • Loading branch information
davorrunje authored Oct 3, 2024
1 parent 638bc02 commit 18c5bb7
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 21 deletions.
3 changes: 2 additions & 1 deletion docs/docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ search:
- [FastAgencyWSGINotImplementedError](api/fastagency/exceptions/FastAgencyWSGINotImplementedError.md)
- helpers
- [check_imports](api/fastagency/helpers/check_imports.md)
- [extract_json_objects](api/fastagency/helpers/extract_json_objects.md)
- [jsonify_string](api/fastagency/helpers/jsonify_string.md)
- logging
- [get_logger](api/fastagency/logging/get_logger.md)
- runtime
Expand Down Expand Up @@ -143,7 +145,6 @@ search:
- message
- [MesopGUIMessageVisitor](api/fastagency/ui/mesop/message/MesopGUIMessageVisitor.md)
- [consume_responses](api/fastagency/ui/mesop/message/consume_responses.md)
- [dict_to_markdown](api/fastagency/ui/mesop/message/dict_to_markdown.md)
- [handle_message](api/fastagency/ui/mesop/message/handle_message.md)
- [message_box](api/fastagency/ui/mesop/message/message_box.md)
- send_prompt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ search:
boost: 0.5
---

::: fastagency.ui.mesop.message.dict_to_markdown
::: fastagency.helpers.extract_json_objects
11 changes: 11 additions & 0 deletions docs/docs/en/api/fastagency/helpers/jsonify_string.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
# 0.5 - API
# 2 - Release
# 3 - Contributing
# 5 - Template Page
# 10 - Default
search:
boost: 0.5
---

::: fastagency.helpers.jsonify_string
41 changes: 41 additions & 0 deletions fastagency/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import importlib
import json
from collections.abc import Iterator
from json import JSONDecoder
from typing import Optional

__all__ = ["check_imports"]

Expand All @@ -13,3 +17,40 @@ def check_imports(package_names: list[str], target_name: str) -> None:
raise ImportError(
f"Package(s) {', '.join(not_importable)} not found. Please install it with:\n\npip install \"fastagency[{target_name}]\"\n"
)


# based on https://stackoverflow.com/questions/61380028/how-to-detect-and-indent-json-substrings-inside-longer-non-json-text/61384796#61384796
def extract_json_objects(
text: str, decoder: Optional[JSONDecoder] = None
) -> Iterator[str]:
decoder = decoder or JSONDecoder()
pos = 0
while True:
# print(f"matching: {text[pos:]}")
match = text.find("{", pos)
if match == -1:
yield text[pos:] # return the remaining text
break
yield text[pos:match].rstrip(" ") # modification for the non-JSON parts
try:
result, index = decoder.raw_decode(text[match:])
yield result
pos = match + index
# move past space characters if needed
while pos < len(text) and text[pos] == " ":
pos += 1
except ValueError:
yield text[match]
pos = match + 1


def jsonify_string(line: str) -> str:
line_parts: list[str] = []
for result in extract_json_objects(line):
if isinstance(result, dict): # got a JSON obj
line_parts.append(f"\n```\n{json.dumps(result, indent=4)}\n```\n")
else: # got text/non-JSON-obj
line_parts.append(result)
# (don't make that a list comprehension, quite un-readable)

return "".join(line_parts)
1 change: 0 additions & 1 deletion fastagency/ui/mesop/data_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ class ConversationMessage:
conversation_id: str = ""
feedback: list[str] = field(default_factory=list)
feedback_completed: bool = False
collapsed: Optional[bool] = None


@dataclass
Expand Down
30 changes: 16 additions & 14 deletions fastagency/ui/mesop/message.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import json
from collections.abc import Iterable, Iterator
from typing import Any, Callable, Optional
from typing import Callable, Optional
from uuid import uuid4

import mesop as me

from fastagency.helpers import jsonify_string

from ...base import (
AskingMessage,
Error,
Expand All @@ -28,10 +30,6 @@
logger = get_logger(__name__)


def dict_to_markdown(d: dict[str, Any], *, indent: int = 2) -> str:
return "\n```\n" + json.dumps(d, indent=2) + "\n```\n"


def consume_responses(responses: Iterable[MesopMessage]) -> Iterator[None]:
for message in responses:
state = me.state(State)
Expand Down Expand Up @@ -138,8 +136,9 @@ def message_content_to_markdown(self, msg: IOMessage) -> str:

return "\n".join([f"**{k}**: {v} <br>" for k, v in d["content"].items()])

def _render_content(self, content: str, style: me.Style) -> None:
me.markdown(content, style=style)
def _render_content(self, content: str, msg_md_style: me.Style) -> None:
content = jsonify_string(content)
me.markdown(content, style=msg_md_style)

def visit_default(
self,
Expand All @@ -165,7 +164,10 @@ def visit_default(
content = content or self.message_content_to_markdown(message)

# me.markdown(content, style=style.md or self._styles.message.default.md)
self._render_content(content, style.md or self._styles.message.default.md)
self._render_content(
content,
style.md or self._styles.message.default.md,
)

if inner_callback:
inner_callback()
Expand All @@ -184,7 +186,6 @@ def visit_error(self, message: Error) -> None:
message,
content=f"### {message.short}\n{message.long}",
style=self._styles.message.error,
# error=True,
)

def visit_system_message(self, message: SystemMessage) -> None:
Expand All @@ -194,7 +195,7 @@ def visit_system_message(self, message: SystemMessage) -> None:
{message.message['body']}
"""
if "heading" in message.message and "body" in message.message
else None
else json.dumps(message.message, indent=2)
)

self.visit_default(
Expand All @@ -207,9 +208,9 @@ def visit_suggested_function_call(self, message: SuggestedFunctionCall) -> None:
content = f"""
**function_name**: `{message.function_name}`<br>
**call_id**: `{message.call_id}`<br>
**arguments**:
{dict_to_markdown(message.arguments)}
**arguments**: {json.dumps(message.arguments)}
"""
logger.warning(f"visit_suggested_function_call: {content=}")
self.visit_default(
message,
content=content,
Expand Down Expand Up @@ -372,11 +373,12 @@ def render_error_message(

content = (
"Failed to render message:"
+ dict_to_markdown(message.model_dump())
+ json.dumps(message.model_dump(), indent=2)
+ f"<br>Error: {e}"
)

logger.info(f"render_error_message: {content=}")
logger.warning(f"render_error_message: {content=}")
logger.warning(e, exc_info=True)
# me.markdown(content, style=style.md or self._styles.message.default.md)
self._render_content(content, style.md or self._styles.message.default.md)

Expand Down
30 changes: 30 additions & 0 deletions fastagency/ui/mesop/styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,43 @@
padding=me.Padding.all(16),
)

MSG_DEFAULT_BUTTON_STYLE = me.Style(
margin=me.Margin.symmetric(horizontal=8),
padding=me.Padding.all(16),
border_radius=8,
background="#1976d2",
)

MSG_DEFAULT_SELECTED_BUTTON_STYLE = me.Style(
margin=me.Margin.symmetric(horizontal=8),
padding=me.Padding.all(16),
border_radius=8,
background="#1976d2",
color="#fff",
)

MSG_DEFAULT_DISABLED_BUTTON_STYLE = me.Style(
margin=me.Margin.symmetric(horizontal=8),
padding=me.Padding.all(16),
border_radius=8,
background="#64b5f6",
color="#fff",
)


@dataclass
class MesopMessageStyles:
box: me.Style = field(default_factory=lambda: MSG_DEFAULT_BOX_STYLE)
md: me.Style = field(default_factory=lambda: MSG_DEFAULT_MD_STYLE)
header_box: me.Style = field(default_factory=lambda: MSG_DEFAULT_HEADER_BOX_STYLE)
header_md: me.Style = field(default_factory=lambda: MSG_DEFAULT_HEADER_MD_STYLE)
button: me.Style = field(default_factory=lambda: MSG_DEFAULT_BUTTON_STYLE)
disabled_button: me.Style = field(
default_factory=lambda: MSG_DEFAULT_DISABLED_BUTTON_STYLE
)
selected_button: me.Style = field(
default_factory=lambda: MSG_DEFAULT_SELECTED_BUTTON_STYLE
)


TEXT_INPUT_INNER_BOX_STYLE = me.Style(
Expand Down
36 changes: 36 additions & 0 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from fastagency.helpers import jsonify_string


def test_hello() -> None:
demo_text = """Hello, {"a": {"b": "c"}} is some json data, but also {"c": [1,2,3]} is too"""
expected = """Hello,
```
{
"a": {
"b": "c"
}
}
```
is some json data, but also
```
{
"c": [
1,
2,
3
]
}
```
is too"""
actual = jsonify_string(demo_text)
assert actual == expected, actual


def test_bad_suggested_function_call() -> None:
content = "**function_name**: `search_gifs`<br>\n**call_id**: `call_T70PONfhtAqCu6FdRWp1frAS`<br>\n**arguments**: {'q': 'cats', 'limit': 5}\n"
expected = """**function_name**: `search_gifs`<br>
**call_id**: `call_T70PONfhtAqCu6FdRWp1frAS`<br>
**arguments**:{'q': 'cats', 'limit': 5}
"""
actual = jsonify_string(content)
assert actual == expected, actual
7 changes: 3 additions & 4 deletions tests/ui/mesop/test_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def test_system_message(self, monkeypatch: pytest.MonkeyPatch) -> None:
assert_called_with_one_of(
me.markdown,
"**System message: sender -> recipient**",
"**message**: {'type': 'test', 'data': 'this is a test message'} <br>",
'\n```\n{\n "type": "test",\n "data": "this is a test message"\n}\n```\n',
)

def test_suggested_function_call(self, monkeypatch: pytest.MonkeyPatch) -> None:
Expand Down Expand Up @@ -122,11 +122,10 @@ def test_suggested_function_call(self, monkeypatch: pytest.MonkeyPatch) -> None:
**function_name**: `function_name`<br>
**call_id**: `my_call_id`<br>
**arguments**:
```
{
"arg1": "value1",
"arg2": "value2"
"arg1": "value1",
"arg2": "value2"
}
```
Expand Down

0 comments on commit 18c5bb7

Please sign in to comment.