Repository: airtai/fastagency

Issue:
Feature: add support for Together AI
Add support for Together AI

Discussion:

Patch:
diff --git a/docs/docs/en/user-guide/ui/mesop/basics.md b/docs/docs/en/user-guide/ui/mesop/basics.md
index 114b3fe5..baa97671 100644
--- a/docs/docs/en/user-guide/ui/mesop/basics.md
+++ b/docs/docs/en/user-guide/ui/mesop/basics.md
@@ -2,16 +2,68 @@
 
 **[MesopUI](../../../../api/fastagency/ui/mesop/MesopUI/)** in FastAgency offers a web-based interface for interacting with multi-agent workflows. Unlike the **ConsoleUI**, which is text-based and runs in the command line, MesopUI provides a user-friendly browser interface, making it ideal for applications that need a more engaging, graphical interaction. MesopUI is perfect for building interactive web applications and enabling users to interact with agents in a more intuitive way.
 
+## Installation
+
 To install **FastAgency** with MesopUI support, use the following command:
 
 ```bash
 pip install "fastagency[autogen,mesop]"
 ```
 
-This command ensures that the required dependencies for both **AutoGen** and **MesopUI** are installed.
+This command ensures that the required dependencies for both **AutoGen** and **Mesop** are installed.
 
 Below, we’ll demonstrate how to set up a basic student-teacher conversation using **[MesopUI](../../../../api/fastagency/ui/mesop/MesopUI/)**.
 
+## Usage
+
+You can simply create Mesop based UI by importing and instantiating the `MesopUI` class with no parameters:
+
+```python
+{! docs_src/user_guide/ui/mesop/main_mesop.py [ln:9] !}
+
+ui = MesopUI()
+```
+
+However, you might want to add some customisation to the look-and-feel of the user interface or change some security settings as follows:
+
+### Security
+
+You can pass a custom [SecurityPolicy](https://google.github.io/mesop/api/page/#mesop.security.security_policy.SecurityPolicy){target="_blank"} object and specify things such as:
+
+- a list of allowed iframe parents,
+
+- a list of sites you can connect to,
+
+- a list of sites you load scripts from, and
+
+- a flag to disable trusted types.
+
+```python
+{! docs_src/user_guide/ui/mesop/main_mesop.py [ln:3] !}
+
+{! docs_src/user_guide/ui/mesop/main_mesop.py [ln:9] !}
+
+{! docs_src/user_guide/ui/mesop/main_mesop.py [ln:54] !}
+
+ui = MesopUI(security_policy=security_policy)
+```
+
+Please see the [Mesop documentation](https://google.github.io/mesop/api/page/#mesop.security.security_policy.SecurityPolicy){target="_blank"} for details.
+
+### Modifying styles
+
+All [Styles](https://google.github.io/mesop/api/style/){target="_blank"} used in styling of Mesop components can be passed to the [`MesopUI`](../../../../api/fastagency/ui/mesop/MesopUI/)constructor and change the default behavior. They are specified in top-level styling class [`MesopHomePageStyles`](../../../../api/fastagency/ui/mesop/styles/MesopHomePageStyles/).
+
+```python
+{! docs_src/user_guide/ui/mesop/main_mesop.py [ln:3] !}
+
+{! docs_src/user_guide/ui/mesop/main_mesop.py [ln:9] !}
+
+{! docs_src/user_guide/ui/mesop/main_mesop.py [ln:56-79] !}
+
+ui = MesopUI(style=style)
+```
+
 ## Example: Student and Teacher Learning Chat
 
 This example shows how to create a simple learning chat where a student agent interacts with a teacher agent. The student asks questions, and the teacher provides responses, simulating a learning environment. The conversation is facilitated through the web interface using **[MesopUI](../../../../api/fastagency/ui/mesop/MesopUI/)**.
@@ -22,7 +74,7 @@ This example shows how to create a simple learning chat where a student agent in
 We begin by importing the necessary modules from **FastAgency** and **AutoGen**. These imports provide the essential building blocks for creating agents, workflows, and integrating MesopUI.
 
 ```python
-{! docs_src/getting_started/main_mesop.py [ln:1-7] !}
+{! docs_src/user_guide/ui/mesop/main_mesop.py [ln:1-14] !}
 ```
 
 - **ConversableAgent**: This class allows the creation of agents that can engage in conversational tasks.
@@ -34,7 +86,7 @@ We begin by importing the necessary modules from **FastAgency** and **AutoGen**.
 Next, we configure the language model that powers the agents. In this case, we're using **GPT-4o**, and the API key is retrieved from the environment.
 
 ```python
-{! docs_src/getting_started/main_mesop.py [ln:9-19] !}
+{! docs_src/user_guide/ui/mesop/main_mesop.py [ln:16-24] !}
 ```
 
 - **Explanation**: The configuration specifies the LLM model and API key used for powering the conversation between agents. The temperature is set to `0.0` to ensure deterministic responses from the agents, making interactions consistent and reliable.
@@ -43,7 +95,7 @@ Next, we configure the language model that powers the agents. In this case, we'r
 Here, we define a simple workflow where the **Student Agent** interacts with the **Teacher Agent**. The student asks questions, and the teacher responds as a math teacher. The workflow is registered using **AutoGenWorkflows**.
 
 ```python
-{! docs_src/getting_started/main_mesop.py [ln:22-44] !}
+{! docs_src/user_guide/ui/mesop/main_mesop.py [ln:26-51] !}
 ```
 
 - **Agent Overview**: The **Student Agent** is configured with a system message, "You are a student willing to learn," and will initiate questions during the interaction. The **Teacher Agent**, on the other hand, is set up as a math teacher and will respond to the student's questions.
@@ -53,8 +105,7 @@ Here, we define a simple workflow where the **Student Agent** interacts with the
 Finally, we instantiate **[MesopUI](../../../../api/fastagency/ui/mesop/MesopUI/)** to link the workflow to a web-based interface. This allows the user to interact with the agents through a web browser.
 
 ```python
-from fastagency.ui.mesop import MesopUI
-app = FastAgency(wf=wf, ui=MesopUI())
+{! docs_src/user_guide/ui/mesop/main_mesop.py [ln:54-83] !}
 ```
 
 - **Explanation**: Here, we set up the **MesopUI** as the user interface for the workflow, which will allow the entire agent interaction to take place through a web-based platform.
diff --git a/docs/docs_src/user_guide/ui/__init__.py b/docs/docs_src/user_guide/ui/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/docs/docs_src/user_guide/ui/mesop/__init__.py b/docs/docs_src/user_guide/ui/mesop/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/docs/docs_src/user_guide/ui/mesop/main_mesop.py b/docs/docs_src/user_guide/ui/mesop/main_mesop.py
new file mode 100644
index 00000000..4b21dcea
--- /dev/null
+++ b/docs/docs_src/user_guide/ui/mesop/main_mesop.py
@@ -0,0 +1,83 @@
+import os
+
+import mesop as me
+
+from autogen.agentchat import ConversableAgent
+
+from fastagency import UI, FastAgency, Workflows
+from fastagency.runtime.autogen import AutoGenWorkflows
+from fastagency.ui.mesop import MesopUI
+from fastagency.ui.mesop.styles import (
+    MesopHomePageStyles,
+    MesopMessagesStyles,
+    MesopSingleChoiceInnerStyles,
+)
+
+llm_config = {
+    "config_list": [
+        {
+            "model": "gpt-4o-mini",
+            "api_key": os.getenv("OPENAI_API_KEY"),
+        }
+    ],
+    "temperature": 0.0,
+}
+
+wf = AutoGenWorkflows()
+
+
+@wf.register(name="simple_learning", description="Student and teacher learning chat")
+def simple_workflow(
+    wf: Workflows, ui: UI, initial_message: str, session_id: str
+) -> str:
+    student_agent = ConversableAgent(
+        name="Student_Agent",
+        system_message="You are a student willing to learn.",
+        llm_config=llm_config,
+    )
+    teacher_agent = ConversableAgent(
+        name="Teacher_Agent",
+        system_message="You are a math teacher.",
+        llm_config=llm_config,
+    )
+
+    chat_result = student_agent.initiate_chat(
+        teacher_agent,
+        message=initial_message,
+        summary_method="reflection_with_llm",
+        max_turns=5,
+    )
+
+    return chat_result.summary  # type: ignore[no-any-return]
+
+
+security_policy=me.SecurityPolicy(allowed_iframe_parents=["https://acme.com"])
+
+styles=MesopHomePageStyles(
+    stylesheets=[
+        "https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap"
+    ],
+    root=me.Style(
+        background="#e7f2ff",
+        height="100%",
+        font_family="Inter",
+        display="flex",
+        flex_direction="row",
+    ),
+    message=MesopMessagesStyles(
+        single_choice_inner=MesopSingleChoiceInnerStyles(
+            disabled_button=me.Style(
+                margin=me.Margin.symmetric(horizontal=8),
+                padding=me.Padding.all(16),
+                border_radius=8,
+                background="#64b5f6",
+                color="#fff",
+                font_size=16,
+            ),
+        )
+    ),
+)
+
+ui = MesopUI(security_policy=security_policy, styles=styles)
+
+app = FastAgency(wf=wf, ui=MesopUI(), title="Learning Chat")
diff --git a/fastagency/ui/mesop/base.py b/fastagency/ui/mesop/base.py
index 2e229a3f..fc36ac48 100644
--- a/fastagency/ui/mesop/base.py
+++ b/fastagency/ui/mesop/base.py
@@ -8,6 +8,7 @@ from tempfile import TemporaryDirectory
 from typing import Any, Callable, ClassVar, Optional
 from uuid import uuid4
 
+import mesop as me
 from mesop.bin.bin import FLAGS as MESOP_FLAGS
 from mesop.bin.bin import main as mesop_main
 
@@ -23,6 +24,7 @@ from ...base import (
     Workflows,
 )
 from ...logging import get_logger
+from .styles import MesopHomePageStyles
 
 logger = get_logger(__name__)
 
@@ -42,11 +44,19 @@ class MesopUI(IOMessageVisitor):  # UI
     _app: Optional[Runnable] = None
     _me: Optional[Callable[..., Any]] = None
 
-    def __init__(self, super_conversation: "Optional[MesopUI]" = None) -> None:
+    def __init__(
+        self,
+        super_conversation: "Optional[MesopUI]" = None,
+        *,
+        security_policy: Optional[me.SecurityPolicy] = None,
+        styles: Optional[MesopHomePageStyles] = None,
+    ) -> None:
         """Initialize the console UI object.
 
         Args:
             super_conversation (Optional[MesopUI], optional): The super conversation. Defaults to None.
+            security_policy (Optional[me.SecurityPolicy], optional): The security policy. Defaults to None.
+            styles (Optional[MesopHomePageStyles], optional): The styles. Defaults to None.
         """
         logger.info(f"Initializing MesopUI: {self}")
         try:
@@ -64,7 +74,7 @@ class MesopUI(IOMessageVisitor):  # UI
             if MesopUI._me is None:
                 from .main import create_home_page, me
 
-                create_home_page(self)
+                create_home_page(self, security_policy=security_policy, styles=styles)
                 MesopUI._me = me
 
         except Exception as e:
diff --git a/fastagency/ui/mesop/main.py b/fastagency/ui/mesop/main.py
index 91ca7128..342f4912 100644
--- a/fastagency/ui/mesop/main.py
+++ b/fastagency/ui/mesop/main.py
@@ -32,7 +32,7 @@ def create_home_page(
     styles: Optional[MesopHomePageStyles] = None,
     security_policy: Optional[me.SecurityPolicy] = None,
 ) -> Callable[[], None]:
-    mhp = MesopHomePage(ui, security_policy=security_policy)
+    mhp = MesopHomePage(ui, styles=styles, security_policy=security_policy)
 
     return mhp.build()
 
diff --git a/fastagency/ui/mesop/message.py b/fastagency/ui/mesop/message.py
index 2b7cd958..70014201 100644
--- a/fastagency/ui/mesop/message.py
+++ b/fastagency/ui/mesop/message.py
@@ -128,14 +128,6 @@ class MesopGUIMessageVisitor(IOMessageVisitor):
         responses = send_user_feedback_to_autogen(feedback)
         yield from consume_responses(responses)
 
-    def message_content_to_markdown(self, msg: IOMessage) -> str:
-        d = msg.model_dump()
-        d.pop("sender")
-        d.pop("recipient")
-        d.pop("type")
-
-        return "\n".join([f"**{k}**: {v} <br>" for k, v in d["content"].items()])
-
     def _render_content(self, content: str, msg_md_style: me.Style) -> None:
         content = jsonify_string(content)
         me.markdown(content, style=msg_md_style)
@@ -148,8 +140,9 @@ class MesopGUIMessageVisitor(IOMessageVisitor):
         style: Optional[MesopMessageStyles] = None,
         error: Optional[bool] = False,
         inner_callback: Optional[Callable[..., None]] = None,
+        scrollable: Optional[bool] = False,
     ) -> None:
-        logger.info(f"visit_default: {message=}")
+        # logger.info(f"visit_default: {message=}")
         style = style or self._styles.message.default
         title = message.type.replace("_", " ").capitalize()
         title = "[Error] " + title if error else title
@@ -161,12 +154,14 @@ class MesopGUIMessageVisitor(IOMessageVisitor):
                 md_style=style.header_md,
             )
 
-            content = content or self.message_content_to_markdown(message)
+            content = content or json.dumps(message.model_dump()["content"])
 
-            # me.markdown(content, style=style.md or self._styles.message.default.md)
             self._render_content(
                 content,
-                style.md or self._styles.message.default.md,
+                msg_md_style=style.scrollable_md
+                or self._styles.message.default.scrollable_md
+                if scrollable
+                else style.md or self._styles.message.default.md,
             )
 
             if inner_callback:
@@ -186,6 +181,7 @@ class MesopGUIMessageVisitor(IOMessageVisitor):
             message,
             content=f"### {message.short}\n{message.long}",
             style=self._styles.message.error,
+            scrollable=True,
         )
 
     def visit_system_message(self, message: SystemMessage) -> None:
@@ -195,32 +191,36 @@ class MesopGUIMessageVisitor(IOMessageVisitor):
 {message.message['body']}
 """
             if "heading" in message.message and "body" in message.message
-            else json.dumps(message.message, indent=2)
+            else json.dumps(message.message)
         )
 
         self.visit_default(
             message,
             content=content,
             style=self._styles.message.system,
+            scrollable=True,
         )
 
     def visit_suggested_function_call(self, message: SuggestedFunctionCall) -> None:
-        content = f"""
-**function_name**: `{message.function_name}`<br>
+        content = f"""**function_name**: `{message.function_name}`<br>
 **call_id**: `{message.call_id}`<br>
-**arguments**: {json.dumps(message.arguments)}
-"""
-        logger.warning(f"visit_suggested_function_call: {content=}")
+**arguments**: {json.dumps(message.arguments)}"""
         self.visit_default(
             message,
             content=content,
             style=self._styles.message.suggested_function_call,
+            scrollable=True,
         )
 
     def visit_function_call_execution(self, message: FunctionCallExecution) -> None:
+        content = f"""**function_name**: `{message.function_name}`<br>
+**call_id**: `{message.call_id}`<br>
+**retval**: {message.retval}"""
         return self.visit_default(
             message,
+            content=content,
             style=self._styles.message.function_call_execution,
+            scrollable=True,
         )
 
     def visit_text_input(self, message: TextInput) -> str:
diff --git a/fastagency/ui/mesop/styles.py b/fastagency/ui/mesop/styles.py
index d347bc99..05388f62 100644
--- a/fastagency/ui/mesop/styles.py
+++ b/fastagency/ui/mesop/styles.py
@@ -105,6 +105,12 @@ MSG_DEFAULT_BOX_STYLE = me.Style(
 )
 
 MSG_DEFAULT_MD_STYLE = me.Style(padding=me.Padding.all(16))
+MSG_DEFAULT_MD_SCROLABLE_STYLE = me.Style(
+    padding=me.Padding(top=16, left=16, right=16),
+    overflow_y="auto",
+    max_height="300px",
+    margin=me.Margin(bottom=16),
+)
 
 MSG_SYSTEM_HEADER_BOX_STYLE = me.Style(
     background="#b3e5fc",
@@ -174,6 +180,9 @@ MSG_DEFAULT_DISABLED_BUTTON_STYLE = me.Style(
 class MesopMessageStyles:
     box: me.Style = field(default_factory=lambda: MSG_DEFAULT_BOX_STYLE)
     md: me.Style = field(default_factory=lambda: MSG_DEFAULT_MD_STYLE)
+    scrollable_md: me.Style = field(
+        default_factory=lambda: MSG_DEFAULT_MD_SCROLABLE_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)


Test patch:
diff --git a/tests/ui/mesop/test_message.py b/tests/ui/mesop/test_message.py
index 7623475a..ac58867a 100644
--- a/tests/ui/mesop/test_message.py
+++ b/tests/ui/mesop/test_message.py
@@ -118,8 +118,7 @@ class TestMessageBox:
         assert_called_with_one_of(
             me.markdown,
             "**Suggested function call: sender -> recipient**",
-            """
-**function_name**: `function_name`<br>
+            """**function_name**: `function_name`<br>
 **call_id**: `my_call_id`<br>
 **arguments**:
 ```
@@ -128,7 +127,6 @@ class TestMessageBox:
     "arg2": "value2"
 }
 ```
-
 """,
         )
 
@@ -154,9 +152,9 @@ class TestMessageBox:
         assert_called_with_one_of(
             me.markdown,
             "**Function call execution: sender -> recipient**",
-            """**function_name**: function_name <br>
-**call_id**: my_call_id <br>
-**retval**: return_value <br>""",
+            """**function_name**: `function_name`<br>
+**call_id**: `my_call_id`<br>
+**retval**: return_value""",
         )
 
     def test_text_input(self, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -259,5 +257,11 @@ class TestMessageBox:
         assert_called_with_one_of(
             me.markdown,
             "**Workflow completed: sender -> recipient**",
-            "**result**: success <br>",
+            """
+```
+{
+    "result": "success"
+}
+```
+""",
         )