Repository: microsoft/autogen
Pull request: Autogenstudio Updates [CSV support, Workflow Export, Skill Editing, Windows Testing ]
<!-- Thank you for your contribution! Please review https://microsoft.github.io/autogen/docs/Contribute before opening a pull request. -->

<!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. -->

## Why are these changes needed?

This PR adds improvements to AutoGen Studio 

- Fix a bug with workflow specification #1428 . Thanks @junkei-okinawa 
- Add Support for viewing CSV files in AutoGen Studio #1355. Thanks @pretherford 
- Fix Install Guide for Windows Users #1470. Thanks @kripper 
- Other Features
   - Add Models as high level object with support for testing models in UI. #1404
   - Support for downloading workflows (sanitized to remove api keys)  #1287
   - Add Support for editing skills with a full fledged code editor  . Fixes #1442 
   - Add speaker selection to Group Chat UI #1373 
   - Add temperate slider for models and default_auto_reply in UI #1521 

<!-- Please give a short summary of the change and the problem this solves. -->

## Related issue number 

#1373  #1442 

<!-- For example: "Closes #1234" -->

## Checks

- [ ] I've included any doc changes needed for https://microsoft.github.io/autogen/. See https://microsoft.github.io/autogen/docs/Contribute#documentation to build and test documentation locally.
- [ ] I've added tests (if relevant) corresponding to the changes introduced in this PR.
- [ ] I've made sure all auto checks have passed.


Problem statement:
[Issue]: Editing a Skill
### Describe the issue

In Studio, to change a skill, I need to delete and add it again with the new code.
Is that by design or did I mess up the installation?
If it is the current state of things, are there plans to add the capability to change the code of a skill through the studio interface?
Is there a work around?

### Steps to reproduce

_No response_

### Screenshots and logs

_No response_

### Additional Information

_No response_

Hints from issue discussion:
I think it would be good to make it clear in the documentation. I lost a lot of time thinking that the problem was with me and installing and uninstalling the product and searching for missing libraries and researching tutotial in the web.

Also, as temporary a work around would be good to explain in the documentation that all the entities can be found in the sqlite db created by the studio and that we can edit the entities directly using a SqLite editor.

For those looking for the same as I the sqlite db can be found at:

"~/miniconda3/envs/[your-env-name]/lib/python[your-python-version]/site-packages/autogenstudio/web/database.sqlite"
I am editing it using vscode SQLite3 Editor extension.

see the screen below for an example:
![image](https://github.com/microsoft/autogen/assets/255868/fcfe476e-2c6d-4875-9a3d-273fedbbd3b6)



Skill Editing is now implemented, with a full-featured code editor interface.
@madtank
@gagb 



https://github.com/microsoft/autogen/assets/1547007/58a32ece-792f-4eba-9195-953b78494158

Fantastic!!!!

I've tested it and it is very good with syntax highlighting and collapsing blocks.
Congratulations.

Commit message: format update

Patch to review:
diff --git a/samples/apps/autogen-studio/README.md b/samples/apps/autogen-studio/README.md
index 0f007731d1fa..1ec69842f2a8 100644
--- a/samples/apps/autogen-studio/README.md
+++ b/samples/apps/autogen-studio/README.md
@@ -1,4 +1,5 @@
 # AutoGen Studio
+
 [![PyPI version](https://badge.fury.io/py/autogenstudio.svg)](https://badge.fury.io/py/autogenstudio)
 [![Downloads](https://static.pepy.tech/badge/autogenstudio/week)](https://pepy.tech/project/autogenstudio)
 
@@ -10,6 +11,9 @@ Code for AutoGen Studio is on GitHub at [microsoft/autogen](https://github.com/m
 
 > **Note**: AutoGen Studio is meant to help you rapidly prototype multi-agent workflows and demonstrate an example of end user interfaces built with AutoGen. It is not meant to be a production-ready app.
 
+> [!WARNING]
+> AutoGen Studio is currently under active development and we are iterating quickly. Kindly consider that we may introduce breaking changes in the releases during the upcoming weeks, and also the `README` might be outdated. We'll update the `README` as soon as we stabilize the API.
+
 ### Capabilities / Roadmap
 
 Some of the capabilities supported by the app frontend include the following:
@@ -30,6 +34,8 @@ Project Structure:
 
 There are two ways to install AutoGen Studio - from PyPi or from source. We **recommend installing from PyPi** unless you plan to modify the source code.
 
+There are two ways to install AutoGen Studio - from PyPi or from source. We **recommend installing from PyPi** unless you plan to modify the source code.
+
 1.  **Install from PyPi**
 
     We recommend using a virtual environment (e.g., conda) to avoid conflicts with existing Python packages. With Python 3.10 or newer active in your virtual environment, use pip to install AutoGen Studio:
@@ -64,7 +70,7 @@ For Windows users, to build the frontend, you may need alternative commands to b
 
 ```bash
 
-  gatsby clean && rmdir /s /q ..\\autogenstudio\\web\\ui && (set \"PREFIX_PATH_VALUE=\" || ver>nul) && gatsby build --prefix-paths && xcopy /E /I /Y public ..\\autogenstudio\\web\\ui
+  gatsby clean && rmdir /s /q ..\\autogenstudio\\web\\ui 2>nul & (set \"PREFIX_PATH_VALUE=\" || ver>nul) && gatsby build --prefix-paths && xcopy /E /I /Y public ..\\autogenstudio\\web\\ui
 
 ```
 
@@ -77,6 +83,7 @@ autogenstudio ui --port 8081
 ```
 
 This will start the application on the specified port. Open your web browser and go to `http://localhost:8081/` to begin using AutoGen Studio.
+AutoGen Studio also takes a `--host <host>` argument to specify the host address. By default, it is set to `localhost`.
 
 Now that you have AutoGen Studio installed and running, you are ready to explore its capabilities, including defining and modifying agent workflows, interacting with agents and sessions, and expanding agent skills.
 
@@ -137,6 +144,13 @@ A: Yes, you can view the generated messages in the debug console of the web UI,
 Yes. AutoGen standardizes on the openai model api format, and you can use any api server that offers an openai compliant endpoint. In the AutoGen Studio UI, each agent has an `llm_config` field where you can input your model endpoint details including `model name`, `api key`, `base url`, `model type` and `api version`. For Azure OpenAI models, you can find these details in the Azure portal. Note that for Azure OpenAI, the `model name` is the deployment id or engine, and the `model type` is "azure".
 For other OSS models, we recommend using a server such as vllm to instantiate an openai compliant endpoint.
 
+**Q: The Server Starts But I Can't Access the UI**
+A: If you are running the server on a remote machine (or a local machine that fails to resolve localhost correstly), you may need to specify the host address. By default, the host address is set to `localhost`. You can specify the host address using the `--host <host>` argument. For example, to start the server on port 8081 and local address such that it is accessible from other machines on the network, you can run the following command:
+
+```bash
+autogenstudio ui --port 8081 --host 0.0.0.0
+```
+
 ## Acknowledgements
 
 AutoGen Studio is Based on the [AutoGen](https://microsoft.github.io/autogen) project. It was adapted from a research prototype built in October 2023 (original credits: Gagan Bansal, Adam Fourney, Victor Dibia, Piali Choudhury, Saleema Amershi, Ahmed Awadallah, Chi Wang).
diff --git a/samples/apps/autogen-studio/autogenstudio/__init__.py b/samples/apps/autogen-studio/autogenstudio/__init__.py
index 784328a73bd5..611b1c5203ce 100644
--- a/samples/apps/autogen-studio/autogenstudio/__init__.py
+++ b/samples/apps/autogen-studio/autogenstudio/__init__.py
@@ -1,3 +1,4 @@
 from .chatmanager import *
 from .workflowmanager import *
 from .datamodel import *
+from .version import __version__
diff --git a/samples/apps/autogen-studio/autogenstudio/chatmanager.py b/samples/apps/autogen-studio/autogenstudio/chatmanager.py
index 034a139e9542..8bfb132f38d9 100644
--- a/samples/apps/autogen-studio/autogenstudio/chatmanager.py
+++ b/samples/apps/autogen-studio/autogenstudio/chatmanager.py
@@ -43,6 +43,7 @@ def chat(self, message: Message, history: List, flow_config: AgentWorkFlowConfig
             output = ""
 
         metadata["code"] = ""
+        metadata["summary_method"] = flow_config.summary_method
         end_time = time.time()
         metadata["time"] = end_time - start_time
         modified_files = get_modified_files(start_time, end_time, scratch_dir, dest_dir=work_dir)
diff --git a/samples/apps/autogen-studio/autogenstudio/datamodel.py b/samples/apps/autogen-studio/autogenstudio/datamodel.py
index 283cd2610d59..593c0646bb69 100644
--- a/samples/apps/autogen-studio/autogenstudio/datamodel.py
+++ b/samples/apps/autogen-studio/autogenstudio/datamodel.py
@@ -33,8 +33,8 @@ def dict(self):
 @dataclass
 class Skill(object):
     title: str
-    file_name: str
     content: str
+    file_name: Optional[str] = None
     id: Optional[str] = None
     description: Optional[str] = None
     timestamp: Optional[str] = None
@@ -172,6 +172,7 @@ class GroupChatFlowSpec:
     timestamp: Optional[str] = None
     user_id: Optional[str] = None
     description: Optional[str] = None
+    skills: Optional[Union[None, List[Skill]]] = None
 
     def __post_init__(self):
         if self.timestamp is None:
@@ -237,6 +238,8 @@ class Session(object):
     id: Optional[str] = None
     timestamp: Optional[str] = None
     flow_config: AgentWorkFlowConfig = None
+    name: Optional[str] = None
+    description: Optional[str] = None
 
     def __post_init__(self):
         if self.timestamp is None:
diff --git a/samples/apps/autogen-studio/autogenstudio/utils/dbdefaults.json b/samples/apps/autogen-studio/autogenstudio/utils/dbdefaults.json
index 4f7c3ff81504..5883ce6fb76b 100644
--- a/samples/apps/autogen-studio/autogenstudio/utils/dbdefaults.json
+++ b/samples/apps/autogen-studio/autogenstudio/utils/dbdefaults.json
@@ -15,8 +15,8 @@
     {
       "model": "TheBloke/zephyr-7B-alpha-AWQ",
       "api_key": "EMPTY",
-      "base_url": "Your Model Endpoint",
-      "description": "Zephyr (local model configuration)"
+      "base_url": "http://localhost:8000/v1",
+      "description": "Local model example with vLLM server endpoint"
     }
   ],
   "agents": [
@@ -43,13 +43,12 @@
           "title": "find_papers_arxiv",
           "description": "This skill finds relevant papers on arXiv given a query.",
           "content": "import os\nimport re\nimport json\nimport hashlib\n\n\ndef search_arxiv(query, max_results=10):\n    \"\"\"\n    Searches arXiv for the given query using the arXiv API, then returns the search results. This is a helper function. In most cases, callers will want to use 'find_relevant_papers( query, max_results )' instead.\n\n    Args:\n        query (str): The search query.\n        max_results (int, optional): The maximum number of search results to return. Defaults to 10.\n\n    Returns:\n        jresults (list): A list of dictionaries. Each dictionary contains fields such as 'title', 'authors', 'summary', and 'pdf_url'\n\n    Example:\n        >>> results = search_arxiv(\"attention is all you need\")\n        >>> print(results)\n    \"\"\"\n\n    import arxiv\n\n    key = hashlib.md5((\"search_arxiv(\" + str(max_results) + \")\" + query).encode(\"utf-8\")).hexdigest()\n    # Create the cache if it doesn't exist\n    cache_dir = \".cache\"\n    if not os.path.isdir(cache_dir):\n        os.mkdir(cache_dir)\n\n    fname = os.path.join(cache_dir, key + \".cache\")\n\n    # Cache hit\n    if os.path.isfile(fname):\n        fh = open(fname, \"r\", encoding=\"utf-8\")\n        data = json.loads(fh.read())\n        fh.close()\n        return data\n\n    # Normalize the query, removing operator keywords\n    query = re.sub(r\"[^\\s\\w]\", \" \", query.lower())\n    query = re.sub(r\"\\s(and|or|not)\\s\", \" \", \" \" + query + \" \")\n    query = re.sub(r\"[^\\s\\w]\", \" \", query.lower())\n    query = re.sub(r\"\\s+\", \" \", query).strip()\n\n    search = arxiv.Search(query=query, max_results=max_results, sort_by=arxiv.SortCriterion.Relevance)\n\n    jresults = list()\n    for result in search.results():\n        r = dict()\n        r[\"entry_id\"] = result.entry_id\n        r[\"updated\"] = str(result.updated)\n        r[\"published\"] = str(result.published)\n        r[\"title\"] = result.title\n        r[\"authors\"] = [str(a) for a in result.authors]\n        r[\"summary\"] = result.summary\n        r[\"comment\"] = result.comment\n        r[\"journal_ref\"] = result.journal_ref\n        r[\"doi\"] = result.doi\n        r[\"primary_category\"] = result.primary_category\n        r[\"categories\"] = result.categories\n        r[\"links\"] = [str(link) for link in result.links]\n        r[\"pdf_url\"] = result.pdf_url\n        jresults.append(r)\n\n    if len(jresults) > max_results:\n        jresults = jresults[0:max_results]\n\n    # Save to cache\n    fh = open(fname, \"w\")\n    fh.write(json.dumps(jresults))\n    fh.close()\n    return jresults\n",
-          "file_name": "find_papers_arxiv.py"
+          "file_name": "find_papers_arxiv"
         },
         {
           "title": "generate_images",
           "description": "This skill generates images from a given query using OpenAI's DALL-E model and saves them to disk.",
-          "content": "from typing import List\nimport uuid\nimport requests  # to perform HTTP requests\nfrom pathlib import Path\n\nfrom openai import OpenAI\n\n\ndef generate_and_save_images(query: str, image_size: str = \"1024x1024\") -> List[str]:\n    \"\"\"\n    Function to paint, draw or illustrate images based on the users query or request. Generates images from a given query using OpenAI's DALL-E model and saves them to disk.  Use the code below anytime there is a request to create an image.\n\n    :param query: A natural language description of the image to be generated.\n    :param image_size: The size of the image to be generated. (default is \"1024x1024\")\n    :return: A list of filenames for the saved images.\n    \"\"\"\n\n    client = OpenAI()  # Initialize the OpenAI client\n    response = client.images.generate(model=\"dall-e-3\", prompt=query, n=1, size=image_size)  # Generate images\n\n    # List to store the file names of saved images\n    saved_files = []\n\n    # Check if the response is successful\n    if response.data:\n        for image_data in response.data:\n            # Generate a random UUID as the file name\n            file_name = str(uuid.uuid4()) + \".png\"  # Assuming the image is a PNG\n            file_path = Path(file_name)\n\n            img_url = image_data.url\n            img_response = requests.get(img_url)\n            if img_response.status_code == 200:\n                # Write the binary content to a file\n                with open(file_path, \"wb\") as img_file:\n                    img_file.write(img_response.content)\n                    print(f\"Image saved to {file_path}\")\n                    saved_files.append(str(file_path))\n            else:\n                print(f\"Failed to download the image from {img_url}\")\n    else:\n        print(\"No image data found in the response!\")\n\n    # Return the list of saved files\n    return saved_files\n\n\n# Example usage of the function:\n# generate_and_save_images(\"A cute baby sea otter\")\n",
-          "file_name": "generate_images.py"
+          "content": "from typing import List\nimport uuid\nimport requests  # to perform HTTP requests\nfrom pathlib import Path\n\nfrom openai import OpenAI\n\n\ndef generate_and_save_images(query: str, image_size: str = \"1024x1024\") -> List[str]:\n    \"\"\"\n    Function to paint, draw or illustrate images based on the users query or request. Generates images from a given query using OpenAI's DALL-E model and saves them to disk.  Use the code below anytime there is a request to create an image.\n\n    :param query: A natural language description of the image to be generated.\n    :param image_size: The size of the image to be generated. (default is \"1024x1024\")\n    :return: A list of filenames for the saved images.\n    \"\"\"\n\n    client = OpenAI()  # Initialize the OpenAI client\n    response = client.images.generate(model=\"dall-e-3\", prompt=query, n=1, size=image_size)  # Generate images\n\n    # List to store the file names of saved images\n    saved_files = []\n\n    # Check if the response is successful\n    if response.data:\n        for image_data in response.data:\n            # Generate a random UUID as the file name\n            file_name = str(uuid.uuid4()) + \".png\"  # Assuming the image is a PNG\n            file_path = Path(file_name)\n\n            img_url = image_data.url\n            img_response = requests.get(img_url)\n            if img_response.status_code == 200:\n                # Write the binary content to a file\n                with open(file_path, \"wb\") as img_file:\n                    img_file.write(img_response.content)\n                    print(f\"Image saved to {file_path}\")\n                    saved_files.append(str(file_path))\n            else:\n                print(f\"Failed to download the image from {img_url}\")\n    else:\n        print(\"No image data found in the response!\")\n\n    # Return the list of saved files\n    return saved_files\n\n\n# Example usage of the function:\n# generate_and_save_images(\"A cute baby sea otter\")\n"
         }
       ],
       "config": {
@@ -58,13 +57,6 @@
           "config_list": [
             {
               "model": "gpt-4-1106-preview"
-            },
-            {
-              "model": "gpt-3.5-turbo-16k"
-            },
-            {
-              "model": "TheBloke/zephyr-7B-alpha-AWQ",
-              "base_url": "http://localhost:8000/v1"
             }
           ],
           "temperature": 0.1,
@@ -81,20 +73,17 @@
     {
       "title": "find_papers_arxiv",
       "description": "This skill finds relevant papers on arXiv given a query.",
-      "content": "import os\nimport re\nimport json\nimport hashlib\n\n\ndef search_arxiv(query, max_results=10):\n    \"\"\"\n    Searches arXiv for the given query using the arXiv API, then returns the search results. This is a helper function. In most cases, callers will want to use 'find_relevant_papers( query, max_results )' instead.\n\n    Args:\n        query (str): The search query.\n        max_results (int, optional): The maximum number of search results to return. Defaults to 10.\n\n    Returns:\n        jresults (list): A list of dictionaries. Each dictionary contains fields such as 'title', 'authors', 'summary', and 'pdf_url'\n\n    Example:\n        >>> results = search_arxiv(\"attention is all you need\")\n        >>> print(results)\n    \"\"\"\n\n    import arxiv\n\n    key = hashlib.md5((\"search_arxiv(\" + str(max_results) + \")\" + query).encode(\"utf-8\")).hexdigest()\n    # Create the cache if it doesn't exist\n    cache_dir = \".cache\"\n    if not os.path.isdir(cache_dir):\n        os.mkdir(cache_dir)\n\n    fname = os.path.join(cache_dir, key + \".cache\")\n\n    # Cache hit\n    if os.path.isfile(fname):\n        fh = open(fname, \"r\", encoding=\"utf-8\")\n        data = json.loads(fh.read())\n        fh.close()\n        return data\n\n    # Normalize the query, removing operator keywords\n    query = re.sub(r\"[^\\s\\w]\", \" \", query.lower())\n    query = re.sub(r\"\\s(and|or|not)\\s\", \" \", \" \" + query + \" \")\n    query = re.sub(r\"[^\\s\\w]\", \" \", query.lower())\n    query = re.sub(r\"\\s+\", \" \", query).strip()\n\n    search = arxiv.Search(query=query, max_results=max_results, sort_by=arxiv.SortCriterion.Relevance)\n\n    jresults = list()\n    for result in search.results():\n        r = dict()\n        r[\"entry_id\"] = result.entry_id\n        r[\"updated\"] = str(result.updated)\n        r[\"published\"] = str(result.published)\n        r[\"title\"] = result.title\n        r[\"authors\"] = [str(a) for a in result.authors]\n        r[\"summary\"] = result.summary\n        r[\"comment\"] = result.comment\n        r[\"journal_ref\"] = result.journal_ref\n        r[\"doi\"] = result.doi\n        r[\"primary_category\"] = result.primary_category\n        r[\"categories\"] = result.categories\n        r[\"links\"] = [str(link) for link in result.links]\n        r[\"pdf_url\"] = result.pdf_url\n        jresults.append(r)\n\n    if len(jresults) > max_results:\n        jresults = jresults[0:max_results]\n\n    # Save to cache\n    fh = open(fname, \"w\")\n    fh.write(json.dumps(jresults))\n    fh.close()\n    return jresults\n",
-      "file_name": "find_papers_arxiv.py"
+      "content": "import os\nimport re\nimport json\nimport hashlib\n\n\ndef search_arxiv(query, max_results=10):\n    \"\"\"\n    Searches arXiv for the given query using the arXiv API, then returns the search results. This is a helper function. In most cases, callers will want to use 'find_relevant_papers( query, max_results )' instead.\n\n    Args:\n        query (str): The search query.\n        max_results (int, optional): The maximum number of search results to return. Defaults to 10.\n\n    Returns:\n        jresults (list): A list of dictionaries. Each dictionary contains fields such as 'title', 'authors', 'summary', and 'pdf_url'\n\n    Example:\n        >>> results = search_arxiv(\"attention is all you need\")\n        >>> print(results)\n    \"\"\"\n\n    import arxiv\n\n    key = hashlib.md5((\"search_arxiv(\" + str(max_results) + \")\" + query).encode(\"utf-8\")).hexdigest()\n    # Create the cache if it doesn't exist\n    cache_dir = \".cache\"\n    if not os.path.isdir(cache_dir):\n        os.mkdir(cache_dir)\n\n    fname = os.path.join(cache_dir, key + \".cache\")\n\n    # Cache hit\n    if os.path.isfile(fname):\n        fh = open(fname, \"r\", encoding=\"utf-8\")\n        data = json.loads(fh.read())\n        fh.close()\n        return data\n\n    # Normalize the query, removing operator keywords\n    query = re.sub(r\"[^\\s\\w]\", \" \", query.lower())\n    query = re.sub(r\"\\s(and|or|not)\\s\", \" \", \" \" + query + \" \")\n    query = re.sub(r\"[^\\s\\w]\", \" \", query.lower())\n    query = re.sub(r\"\\s+\", \" \", query).strip()\n\n    search = arxiv.Search(query=query, max_results=max_results, sort_by=arxiv.SortCriterion.Relevance)\n\n    jresults = list()\n    for result in search.results():\n        r = dict()\n        r[\"entry_id\"] = result.entry_id\n        r[\"updated\"] = str(result.updated)\n        r[\"published\"] = str(result.published)\n        r[\"title\"] = result.title\n        r[\"authors\"] = [str(a) for a in result.authors]\n        r[\"summary\"] = result.summary\n        r[\"comment\"] = result.comment\n        r[\"journal_ref\"] = result.journal_ref\n        r[\"doi\"] = result.doi\n        r[\"primary_category\"] = result.primary_category\n        r[\"categories\"] = result.categories\n        r[\"links\"] = [str(link) for link in result.links]\n        r[\"pdf_url\"] = result.pdf_url\n        jresults.append(r)\n\n    if len(jresults) > max_results:\n        jresults = jresults[0:max_results]\n\n    # Save to cache\n    fh = open(fname, \"w\")\n    fh.write(json.dumps(jresults))\n    fh.close()\n    return jresults\n"
     },
     {
       "title": "fetch_profile",
       "description": "This skill fetches the text content from a personal website.",
-      "content": "from typing import Optional\nimport requests\nfrom bs4 import BeautifulSoup\n\n\ndef fetch_user_profile(url: str) -> Optional[str]:\n    \"\"\"\n    Fetches the text content from a personal website.\n\n    Given a URL of a person's personal website, this function scrapes\n    the content of the page and returns the text found within the <body>.\n\n    Args:\n        url (str): The URL of the person's personal website.\n\n    Returns:\n        Optional[str]: The text content of the website's body, or None if any error occurs.\n    \"\"\"\n    try:\n        # Send a GET request to the URL\n        response = requests.get(url)\n        # Check for successful access to the webpage\n        if response.status_code == 200:\n            # Parse the HTML content of the page using BeautifulSoup\n            soup = BeautifulSoup(response.text, \"html.parser\")\n            # Extract the content of the <body> tag\n            body_content = soup.find(\"body\")\n            # Return all the text in the body tag, stripping leading/trailing whitespaces\n            return \" \".join(body_content.stripped_strings) if body_content else None\n        else:\n            # Return None if the status code isn't 200 (success)\n            return None\n    except requests.RequestException:\n        # Return None if any request-related exception is caught\n        return None\n",
-      "file_name": "fetch_profile.py"
+      "content": "from typing import Optional\nimport requests\nfrom bs4 import BeautifulSoup\n\n\ndef fetch_user_profile(url: str) -> Optional[str]:\n    \"\"\"\n    Fetches the text content from a personal website.\n\n    Given a URL of a person's personal website, this function scrapes\n    the content of the page and returns the text found within the <body>.\n\n    Args:\n        url (str): The URL of the person's personal website.\n\n    Returns:\n        Optional[str]: The text content of the website's body, or None if any error occurs.\n    \"\"\"\n    try:\n        # Send a GET request to the URL\n        response = requests.get(url)\n        # Check for successful access to the webpage\n        if response.status_code == 200:\n            # Parse the HTML content of the page using BeautifulSoup\n            soup = BeautifulSoup(response.text, \"html.parser\")\n            # Extract the content of the <body> tag\n            body_content = soup.find(\"body\")\n            # Return all the text in the body tag, stripping leading/trailing whitespaces\n            return \" \".join(body_content.stripped_strings) if body_content else None\n        else:\n            # Return None if the status code isn't 200 (success)\n            return None\n    except requests.RequestException:\n        # Return None if any request-related exception is caught\n        return None\n"
     },
     {
       "title": "generate_images",
       "description": "This skill generates images from a given query using OpenAI's DALL-E model and saves them to disk.",
-      "content": "from typing import List\nimport uuid\nimport requests  # to perform HTTP requests\nfrom pathlib import Path\n\nfrom openai import OpenAI\n\n\ndef generate_and_save_images(query: str, image_size: str = \"1024x1024\") -> List[str]:\n    \"\"\"\n    Function to paint, draw or illustrate images based on the users query or request. Generates images from a given query using OpenAI's DALL-E model and saves them to disk.  Use the code below anytime there is a request to create an image.\n\n    :param query: A natural language description of the image to be generated.\n    :param image_size: The size of the image to be generated. (default is \"1024x1024\")\n    :return: A list of filenames for the saved images.\n    \"\"\"\n\n    client = OpenAI()  # Initialize the OpenAI client\n    response = client.images.generate(model=\"dall-e-3\", prompt=query, n=1, size=image_size)  # Generate images\n\n    # List to store the file names of saved images\n    saved_files = []\n\n    # Check if the response is successful\n    if response.data:\n        for image_data in response.data:\n            # Generate a random UUID as the file name\n            file_name = str(uuid.uuid4()) + \".png\"  # Assuming the image is a PNG\n            file_path = Path(file_name)\n\n            img_url = image_data.url\n            img_response = requests.get(img_url)\n            if img_response.status_code == 200:\n                # Write the binary content to a file\n                with open(file_path, \"wb\") as img_file:\n                    img_file.write(img_response.content)\n                    print(f\"Image saved to {file_path}\")\n                    saved_files.append(str(file_path))\n            else:\n                print(f\"Failed to download the image from {img_url}\")\n    else:\n        print(\"No image data found in the response!\")\n\n    # Return the list of saved files\n    return saved_files\n\n\n# Example usage of the function:\n# generate_and_save_images(\"A cute baby sea otter\")\n",
-      "file_name": "generate_images.py"
+      "content": "from typing import List\nimport uuid\nimport requests  # to perform HTTP requests\nfrom pathlib import Path\n\nfrom openai import OpenAI\n\n\ndef generate_and_save_images(query: str, image_size: str = \"1024x1024\") -> List[str]:\n    \"\"\"\n    Function to paint, draw or illustrate images based on the users query or request. Generates images from a given query using OpenAI's DALL-E model and saves them to disk.  Use the code below anytime there is a request to create an image.\n\n    :param query: A natural language description of the image to be generated.\n    :param image_size: The size of the image to be generated. (default is \"1024x1024\")\n    :return: A list of filenames for the saved images.\n    \"\"\"\n\n    client = OpenAI()  # Initialize the OpenAI client\n    response = client.images.generate(model=\"dall-e-3\", prompt=query, n=1, size=image_size)  # Generate images\n\n    # List to store the file names of saved images\n    saved_files = []\n\n    # Check if the response is successful\n    if response.data:\n        for image_data in response.data:\n            # Generate a random UUID as the file name\n            file_name = str(uuid.uuid4()) + \".png\"  # Assuming the image is a PNG\n            file_path = Path(file_name)\n\n            img_url = image_data.url\n            img_response = requests.get(img_url)\n            if img_response.status_code == 200:\n                # Write the binary content to a file\n                with open(file_path, \"wb\") as img_file:\n                    img_file.write(img_response.content)\n                    print(f\"Image saved to {file_path}\")\n                    saved_files.append(str(file_path))\n            else:\n                print(f\"Failed to download the image from {img_url}\")\n    else:\n        print(\"No image data found in the response!\")\n\n    # Return the list of saved files\n    return saved_files\n\n\n# Example usage of the function:\n# generate_and_save_images(\"A cute baby sea otter\")\n"
     }
   ],
   "workflows": [
@@ -125,13 +114,6 @@
             "config_list": [
               {
                 "model": "gpt-4-1106-preview"
-              },
-              {
-                "model": "gpt-3.5-turbo-16k"
-              },
-              {
-                "model": "TheBloke/zephyr-7B-alpha-AWQ",
-                "base_url": "http://localhost:8000/v1"
               }
             ],
             "temperature": 0.1,
@@ -202,7 +184,7 @@
                 },
                 "human_input_mode": "NEVER",
                 "max_consecutive_auto_reply": 8,
-                "system_message": "You are a helpful assistant that can suggest a travel itinerary for a user. You are the primary coordinator who will receive suggestions or advice from other agents (local_assistant, language_assistant). You must ensure that the finally plan integrates the suggestions from other agents or team members. YOUR FINAL RESPONSE MUST BE THE COMPLETE PLAN that ends with the word TERMINATE. "
+                "system_message": "You are a helpful assistant that can suggest a travel itinerary for a user. You are the primary cordinator who will receive suggestions or advice from other agents (local_assistant, language_assistant). You must ensure that the finally plan integrates the suggestions from other agents or team members. YOUR FINAL RESPONSE MUST BE THE COMPLETE PLAN that ends with the word TERMINATE. "
               }
             },
             {
@@ -272,14 +254,12 @@
           {
             "title": "find_papers_arxiv",
             "description": "This skill finds relevant papers on arXiv given a query.",
-            "content": "import os\nimport re\nimport json\nimport hashlib\n\n\ndef search_arxiv(query, max_results=10):\n    \"\"\"\n    Searches arXiv for the given query using the arXiv API, then returns the search results. This is a helper function. In most cases, callers will want to use 'find_relevant_papers( query, max_results )' instead.\n\n    Args:\n        query (str): The search query.\n        max_results (int, optional): The maximum number of search results to return. Defaults to 10.\n\n    Returns:\n        jresults (list): A list of dictionaries. Each dictionary contains fields such as 'title', 'authors', 'summary', and 'pdf_url'\n\n    Example:\n        >>> results = search_arxiv(\"attention is all you need\")\n        >>> print(results)\n    \"\"\"\n\n    import arxiv\n\n    key = hashlib.md5((\"search_arxiv(\" + str(max_results) + \")\" + query).encode(\"utf-8\")).hexdigest()\n    # Create the cache if it doesn't exist\n    cache_dir = \".cache\"\n    if not os.path.isdir(cache_dir):\n        os.mkdir(cache_dir)\n\n    fname = os.path.join(cache_dir, key + \".cache\")\n\n    # Cache hit\n    if os.path.isfile(fname):\n        fh = open(fname, \"r\", encoding=\"utf-8\")\n        data = json.loads(fh.read())\n        fh.close()\n        return data\n\n    # Normalize the query, removing operator keywords\n    query = re.sub(r\"[^\\s\\w]\", \" \", query.lower())\n    query = re.sub(r\"\\s(and|or|not)\\s\", \" \", \" \" + query + \" \")\n    query = re.sub(r\"[^\\s\\w]\", \" \", query.lower())\n    query = re.sub(r\"\\s+\", \" \", query).strip()\n\n    search = arxiv.Search(query=query, max_results=max_results, sort_by=arxiv.SortCriterion.Relevance)\n\n    jresults = list()\n    for result in search.results():\n        r = dict()\n        r[\"entry_id\"] = result.entry_id\n        r[\"updated\"] = str(result.updated)\n        r[\"published\"] = str(result.published)\n        r[\"title\"] = result.title\n        r[\"authors\"] = [str(a) for a in result.authors]\n        r[\"summary\"] = result.summary\n        r[\"comment\"] = result.comment\n        r[\"journal_ref\"] = result.journal_ref\n        r[\"doi\"] = result.doi\n        r[\"primary_category\"] = result.primary_category\n        r[\"categories\"] = result.categories\n        r[\"links\"] = [str(link) for link in result.links]\n        r[\"pdf_url\"] = result.pdf_url\n        jresults.append(r)\n\n    if len(jresults) > max_results:\n        jresults = jresults[0:max_results]\n\n    # Save to cache\n    fh = open(fname, \"w\")\n    fh.write(json.dumps(jresults))\n    fh.close()\n    return jresults\n",
-            "file_name": "find_papers_arxiv.py"
+            "content": "import os\nimport re\nimport json\nimport hashlib\n\n\ndef search_arxiv(query, max_results=10):\n    \"\"\"\n    Searches arXiv for the given query using the arXiv API, then returns the search results. This is a helper function. In most cases, callers will want to use 'find_relevant_papers( query, max_results )' instead.\n\n    Args:\n        query (str): The search query.\n        max_results (int, optional): The maximum number of search results to return. Defaults to 10.\n\n    Returns:\n        jresults (list): A list of dictionaries. Each dictionary contains fields such as 'title', 'authors', 'summary', and 'pdf_url'\n\n    Example:\n        >>> results = search_arxiv(\"attention is all you need\")\n        >>> print(results)\n    \"\"\"\n\n    import arxiv\n\n    key = hashlib.md5((\"search_arxiv(\" + str(max_results) + \")\" + query).encode(\"utf-8\")).hexdigest()\n    # Create the cache if it doesn't exist\n    cache_dir = \".cache\"\n    if not os.path.isdir(cache_dir):\n        os.mkdir(cache_dir)\n\n    fname = os.path.join(cache_dir, key + \".cache\")\n\n    # Cache hit\n    if os.path.isfile(fname):\n        fh = open(fname, \"r\", encoding=\"utf-8\")\n        data = json.loads(fh.read())\n        fh.close()\n        return data\n\n    # Normalize the query, removing operator keywords\n    query = re.sub(r\"[^\\s\\w]\", \" \", query.lower())\n    query = re.sub(r\"\\s(and|or|not)\\s\", \" \", \" \" + query + \" \")\n    query = re.sub(r\"[^\\s\\w]\", \" \", query.lower())\n    query = re.sub(r\"\\s+\", \" \", query).strip()\n\n    search = arxiv.Search(query=query, max_results=max_results, sort_by=arxiv.SortCriterion.Relevance)\n\n    jresults = list()\n    for result in search.results():\n        r = dict()\n        r[\"entry_id\"] = result.entry_id\n        r[\"updated\"] = str(result.updated)\n        r[\"published\"] = str(result.published)\n        r[\"title\"] = result.title\n        r[\"authors\"] = [str(a) for a in result.authors]\n        r[\"summary\"] = result.summary\n        r[\"comment\"] = result.comment\n        r[\"journal_ref\"] = result.journal_ref\n        r[\"doi\"] = result.doi\n        r[\"primary_category\"] = result.primary_category\n        r[\"categories\"] = result.categories\n        r[\"links\"] = [str(link) for link in result.links]\n        r[\"pdf_url\"] = result.pdf_url\n        jresults.append(r)\n\n    if len(jresults) > max_results:\n        jresults = jresults[0:max_results]\n\n    # Save to cache\n    fh = open(fname, \"w\")\n    fh.write(json.dumps(jresults))\n    fh.close()\n    return jresults\n"
           },
           {
             "title": "generate_images",
             "description": "This skill generates images from a given query using OpenAI's DALL-E model and saves them to disk.",
-            "content": "from typing import List\nimport uuid\nimport requests  # to perform HTTP requests\nfrom pathlib import Path\n\nfrom openai import OpenAI\n\n\ndef generate_and_save_images(query: str, image_size: str = \"1024x1024\") -> List[str]:\n    \"\"\"\n    Function to paint, draw or illustrate images based on the users query or request. Generates images from a given query using OpenAI's DALL-E model and saves them to disk.  Use the code below anytime there is a request to create an image.\n\n    :param query: A natural language description of the image to be generated.\n    :param image_size: The size of the image to be generated. (default is \"1024x1024\")\n    :return: A list of filenames for the saved images.\n    \"\"\"\n\n    client = OpenAI()  # Initialize the OpenAI client\n    response = client.images.generate(model=\"dall-e-3\", prompt=query, n=1, size=image_size)  # Generate images\n\n    # List to store the file names of saved images\n    saved_files = []\n\n    # Check if the response is successful\n    if response.data:\n        for image_data in response.data:\n            # Generate a random UUID as the file name\n            file_name = str(uuid.uuid4()) + \".png\"  # Assuming the image is a PNG\n            file_path = Path(file_name)\n\n            img_url = image_data.url\n            img_response = requests.get(img_url)\n            if img_response.status_code == 200:\n                # Write the binary content to a file\n                with open(file_path, \"wb\") as img_file:\n                    img_file.write(img_response.content)\n                    print(f\"Image saved to {file_path}\")\n                    saved_files.append(str(file_path))\n            else:\n                print(f\"Failed to download the image from {img_url}\")\n    else:\n        print(\"No image data found in the response!\")\n\n    # Return the list of saved files\n    return saved_files\n\n\n# Example usage of the function:\n# generate_and_save_images(\"A cute baby sea otter\")\n",
-            "file_name": "generate_images.py"
+            "content": "from typing import List\nimport uuid\nimport requests  # to perform HTTP requests\nfrom pathlib import Path\n\nfrom openai import OpenAI\n\n\ndef generate_and_save_images(query: str, image_size: str = \"1024x1024\") -> List[str]:\n    \"\"\"\n    Function to paint, draw or illustrate images based on the users query or request. Generates images from a given query using OpenAI's DALL-E model and saves them to disk.  Use the code below anytime there is a request to create an image.\n\n    :param query: A natural language description of the image to be generated.\n    :param image_size: The size of the image to be generated. (default is \"1024x1024\")\n    :return: A list of filenames for the saved images.\n    \"\"\"\n\n    client = OpenAI()  # Initialize the OpenAI client\n    response = client.images.generate(model=\"dall-e-3\", prompt=query, n=1, size=image_size)  # Generate images\n\n    # List to store the file names of saved images\n    saved_files = []\n\n    # Check if the response is successful\n    if response.data:\n        for image_data in response.data:\n            # Generate a random UUID as the file name\n            file_name = str(uuid.uuid4()) + \".png\"  # Assuming the image is a PNG\n            file_path = Path(file_name)\n\n            img_url = image_data.url\n            img_response = requests.get(img_url)\n            if img_response.status_code == 200:\n                # Write the binary content to a file\n                with open(file_path, \"wb\") as img_file:\n                    img_file.write(img_response.content)\n                    print(f\"Image saved to {file_path}\")\n                    saved_files.append(str(file_path))\n            else:\n                print(f\"Failed to download the image from {img_url}\")\n    else:\n        print(\"No image data found in the response!\")\n\n    # Return the list of saved files\n    return saved_files\n\n\n# Example usage of the function:\n# generate_and_save_images(\"A cute baby sea otter\")\n"
           }
         ],
         "config": {
@@ -288,13 +268,6 @@
             "config_list": [
               {
                 "model": "gpt-4-1106-preview"
-              },
-              {
-                "model": "gpt-3.5-turbo-16k"
-              },
-              {
-                "model": "TheBloke/zephyr-7B-alpha-AWQ",
-                "base_url": "http://localhost:8000/v1"
               }
             ],
             "temperature": 0.1,
diff --git a/samples/apps/autogen-studio/autogenstudio/utils/dbutils.py b/samples/apps/autogen-studio/autogenstudio/utils/dbutils.py
index d1891081b3c5..92893c01c3dc 100644
--- a/samples/apps/autogen-studio/autogenstudio/utils/dbutils.py
+++ b/samples/apps/autogen-studio/autogenstudio/utils/dbutils.py
@@ -5,8 +5,17 @@
 import os
 from typing import Any, List, Dict, Optional, Tuple
 from ..datamodel import AgentFlowSpec, AgentWorkFlowConfig, Gallery, Message, Model, Session, Skill
+from ..version import __version__ as __db_version__
 
 
+VERSION_TABLE_SQL = """
+            CREATE TABLE IF NOT EXISTS version (
+
+                version TEXT NOT NULL,
+                UNIQUE (version)
+            )
+            """
+
 MODELS_TABLE_SQL = """
             CREATE TABLE IF NOT EXISTS models (
                 id TEXT NOT NULL,
@@ -140,6 +149,13 @@ def reset_db(self):
             os.remove(self.path)
         self.init_db(path=self.path)
 
+    def run_migrations(self):
+        """
+        Run migrations to update the database schema.
+        """
+
+        pass
+
     def init_db(self, path: str = "database.sqlite", **kwargs: Any) -> None:
         """
         Initializes the database by creating necessary tables.
@@ -152,6 +168,10 @@ def init_db(self, path: str = "database.sqlite", **kwargs: Any) -> None:
         self.conn = sqlite3.connect(path, check_same_thread=False, **kwargs)
         self.cursor = self.conn.cursor()
 
+        # Create the version table
+        self.cursor.execute(VERSION_TABLE_SQL)
+        self.cursor.execute("INSERT INTO version (version) VALUES (?)", (__db_version__,))
+
         # Create the models table
         self.cursor.execute(MODELS_TABLE_SQL)
 
diff --git a/samples/apps/autogen-studio/autogenstudio/utils/utils.py b/samples/apps/autogen-studio/autogenstudio/utils/utils.py
index ce2778f09f8e..771039a93001 100644
--- a/samples/apps/autogen-studio/autogenstudio/utils/utils.py
+++ b/samples/apps/autogen-studio/autogenstudio/utils/utils.py
@@ -1,4 +1,3 @@
-import ast
 import base64
 import hashlib
 from typing import List, Dict, Tuple, Union
@@ -7,7 +6,8 @@
 from pathlib import Path
 import re
 import autogen
-from ..datamodel import AgentConfig, AgentFlowSpec, AgentWorkFlowConfig, LLMConfig, Skill
+from autogen.oai.client import OpenAIWrapper
+from ..datamodel import AgentConfig, AgentFlowSpec, AgentWorkFlowConfig, LLMConfig, Model, Skill
 
 
 def md5_hash(text: str) -> str:
@@ -27,6 +27,9 @@ def clear_folder(folder_path: str) -> None:
     :param folder_path: The path to the folder to clear.
     """
     # exit if the folder does not exist
+    if not os.path.exists(folder_path):
+        return
+    # exit if the folder does not exist
     if not os.path.exists(folder_path):
         return
     for file in os.listdir(folder_path):
@@ -86,6 +89,9 @@ def get_file_type(file_path: str) -> str:
         ".config",
     }
 
+    # Supported spreadsheet extensions
+    CSV_EXTENSIONS = {".csv", ".xlsx"}
+
     # Supported image extensions
     IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".svg", ".webp"}
     # Supported (web) video extensions
@@ -100,6 +106,8 @@ def get_file_type(file_path: str) -> str:
     # Determine the file type based on the extension
     if file_extension in CODE_EXTENSIONS:
         file_type = "code"
+    elif file_extension in CSV_EXTENSIONS:
+        file_type = "csv"
     elif file_extension in IMAGE_EXTENSIONS:
         file_type = "image"
     elif file_extension == PDF_EXTENSION:
@@ -211,19 +219,18 @@ def init_webserver_folders(root_file_path: str) -> Dict[str, str]:
     :param root_file_path: The root directory where webserver folders will be created
     :return: A dictionary with the path of each created folder
     """
+
+    if not os.path.exists(root_file_path):
+        os.makedirs(root_file_path, exist_ok=True)
     files_static_root = os.path.join(root_file_path, "files/")
     static_folder_root = os.path.join(root_file_path, "ui")
-    workdir_root = os.path.join(root_file_path, "workdir")
 
     os.makedirs(files_static_root, exist_ok=True)
     os.makedirs(os.path.join(files_static_root, "user"), exist_ok=True)
     os.makedirs(static_folder_root, exist_ok=True)
-    os.makedirs(workdir_root, exist_ok=True)
-
     folders = {
         "files_static_root": files_static_root,
         "static_folder_root": static_folder_root,
-        "workdir_root": workdir_root,
     }
     return folders
 
@@ -261,6 +268,9 @@ def get_skills_from_prompt(skills: List[Skill], work_dir: str) -> str:
         os.makedirs(work_dir)
 
     # overwrite skills.py in work_dir
+    with open(os.path.join(work_dir, "skills.py"), "w", encoding="utf-8") as f:
+        f.write(prompt)
+    # overwrite skills.py in work_dir
     with open(os.path.join(work_dir, "skills.py"), "w", encoding="utf-8") as f:
         f.write(prompt)
 
@@ -375,3 +385,22 @@ def extract_successful_code_blocks(messages: List[Dict[str, str]]) -> List[str]:
                 successful_code_blocks.extend(code_blocks)
 
     return successful_code_blocks
+
+
+def sanitize_model(model: Model):
+    """
+    Sanitize model dictionary to remove None values and empty strings and only keep valid keys.
+    """
+    if isinstance(model, Model):
+        model = model.dict()
+    valid_keys = ["model", "base_url", "api_key", "api_type", "api_version"]
+    # only add key if value is not None
+    sanitized_model = {k: v for k, v in model.items() if (v is not None and v != "") and k in valid_keys}
+    return sanitized_model
+
+
+def test_model(model: Model):
+    sanitized_model = sanitize_model(model)
+    client = OpenAIWrapper(config_list=[sanitized_model])
+    response = client.create(messages=[{"role": "user", "content": "2+2="}], cache_seed=None)
+    return response
diff --git a/samples/apps/autogen-studio/autogenstudio/version.py b/samples/apps/autogen-studio/autogenstudio/version.py
index 51feb3765672..19173e3684e3 100644
--- a/samples/apps/autogen-studio/autogenstudio/version.py
+++ b/samples/apps/autogen-studio/autogenstudio/version.py
@@ -1,4 +1,3 @@
-VERSION = "0.0.33a"
+VERSION = "0.0.42a"
 __version__ = VERSION
-__db_version__ = "0.0.1"
 APP_NAME = "autogenstudio"
diff --git a/samples/apps/autogen-studio/autogenstudio/web/app.py b/samples/apps/autogen-studio/autogenstudio/web/app.py
index 6934c7e8aaf0..c069623af3df 100644
--- a/samples/apps/autogen-studio/autogenstudio/web/app.py
+++ b/samples/apps/autogen-studio/autogenstudio/web/app.py
@@ -5,6 +5,7 @@
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.staticfiles import StaticFiles
 from fastapi import HTTPException
+from openai import OpenAIError
 from ..version import VERSION
 
 from ..datamodel import (
@@ -14,7 +15,7 @@
     Message,
     Session,
 )
-from ..utils import md5_hash, init_webserver_folders, DBManager, dbutils
+from ..utils import md5_hash, init_webserver_folders, DBManager, dbutils, test_model
 
 from ..chatmanager import AutoGenChatManager
 
@@ -250,7 +251,6 @@ async def get_user_skills(user_id: str):
 async def create_user_skills(req: DBWebRequestModel):
     try:
         skills = dbutils.upsert_skill(skill=req.skill, dbmanager=dbmanager)
-
         return {
             "status": True,
             "message": "Skills retrieved successfully",
@@ -385,6 +385,32 @@ async def create_user_models(req: DBWebRequestModel):
         }
 
 
+@api.post("/models/test")
+async def test_user_models(req: DBWebRequestModel):
+    """Test a model to verify it works"""
+
+    try:
+        response = test_model(model=req.model)
+        return {
+            "status": True,
+            "message": "Model tested successfully",
+            "data": response,
+        }
+
+    except OpenAIError as oai_error:
+        print(traceback.format_exc())
+        return {
+            "status": False,
+            "message": "Error occurred while testing model: " + str(oai_error),
+        }
+    except Exception as ex_error:
+        print(traceback.format_exc())
+        return {
+            "status": False,
+            "message": "Error occurred while testing model: " + str(ex_error),
+        }
+
+
 @api.delete("/models/delete")
 async def delete_user_model(req: DBWebRequestModel):
     """Delete a model for a user"""
@@ -427,7 +453,6 @@ async def get_user_workflows(user_id: str):
 @api.post("/workflows")
 async def create_user_workflow(req: DBWebRequestModel):
     """Create a new workflow for a user"""
-
     try:
         workflow = dbutils.upsert_workflow(workflow=req.workflow, dbmanager=dbmanager)
         return {
diff --git a/samples/apps/autogen-studio/autogenstudio/workflowmanager.py b/samples/apps/autogen-studio/autogenstudio/workflowmanager.py
index 62c3b977c1d6..480dcdb9901b 100644
--- a/samples/apps/autogen-studio/autogenstudio/workflowmanager.py
+++ b/samples/apps/autogen-studio/autogenstudio/workflowmanager.py
@@ -1,8 +1,8 @@
+import os
 from typing import List, Optional
-from dataclasses import asdict
 import autogen
-from .datamodel import AgentConfig, AgentFlowSpec, AgentWorkFlowConfig, GroupChatConfig, Message
-from .utils import get_skills_from_prompt, clear_folder
+from .datamodel import AgentConfig, AgentFlowSpec, AgentWorkFlowConfig, Message
+from .utils import get_skills_from_prompt, clear_folder, sanitize_model
 from datetime import datetime
 
 
@@ -113,24 +113,42 @@ def sanitize_agent_spec(self, agent_spec: AgentFlowSpec) -> AgentFlowSpec:
         agent_spec.config.is_termination_msg = agent_spec.config.is_termination_msg or (
             lambda x: "TERMINATE" in x.get("content", "").rstrip()[-20:]
         )
-        skills_prompt = ""
-        if agent_spec.skills:
-            # get skill prompt, also write skills to a file named skills.py
-            skills_prompt = get_skills_from_prompt(agent_spec.skills, self.work_dir)
 
-        if agent_spec.type == "userproxy":
+        def get_default_system_message(agent_type: str) -> str:
+            if agent_type == "assistant":
+                return autogen.AssistantAgent.DEFAULT_SYSTEM_MESSAGE
+            else:
+                return "You are a helpful AI Assistant."
+
+        # sanitize llm_config if present
+        if agent_spec.config.llm_config is not False:
+            config_list = []
+            for llm in agent_spec.config.llm_config.config_list:
+                # check if api_key is present either in llm or env variable
+                if "api_key" not in llm and "OPENAI_API_KEY" not in os.environ:
+                    error_message = f"api_key is not present in llm_config or OPENAI_API_KEY env variable for agent ** {agent_spec.config.name}**. Update your workflow to provide an api_key to use the LLM."
+                    raise ValueError(error_message)
+
+                # only add key if value is not None
+                sanitized_llm = sanitize_model(llm)
+                config_list.append(sanitized_llm)
+            agent_spec.config.llm_config.config_list = config_list
+        if agent_spec.config.code_execution_config is not False:
             code_execution_config = agent_spec.config.code_execution_config or {}
             code_execution_config["work_dir"] = self.work_dir
+            # tbd check if docker is installed
+            code_execution_config["use_docker"] = False
             agent_spec.config.code_execution_config = code_execution_config
 
-        if agent_spec.type == "assistant":
-            agent_spec.config.system_message = (
-                autogen.AssistantAgent.DEFAULT_SYSTEM_MESSAGE
-                + "\n\n"
-                + agent_spec.config.system_message
-                + "\n\n"
-                + skills_prompt
-            )
+        if agent_spec.skills:
+            # get skill prompt, also write skills to a file named skills.py
+            skills_prompt = ""
+            skills_prompt = get_skills_from_prompt(agent_spec.skills, self.work_dir)
+
+            if agent_spec.config.system_message:
+                agent_spec.config.system_message = agent_spec.config.system_message + "\n\n" + skills_prompt
+            else:
+                agent_spec.config.system_message = get_default_system_message(agent_spec.type) + "\n\n" + skills_prompt
 
         return agent_spec
 
@@ -144,7 +162,7 @@ def load(self, agent_spec: AgentFlowSpec) -> autogen.Agent:
         Returns:
             An instance of the loaded agent.
         """
-
+        agent_spec = self.sanitize_agent_spec(agent_spec)
         if agent_spec.type == "groupchat":
             agents = [
                 self.load(self.sanitize_agent_spec(agent_config)) for agent_config in agent_spec.groupchat_config.agents
@@ -156,7 +174,6 @@ def load(self, agent_spec: AgentFlowSpec) -> autogen.Agent:
             return manager
 
         else:
-            agent_spec = self.sanitize_agent_spec(agent_spec)
             agent = self.load_agent_config(agent_spec.config, agent_spec.type)
             return agent
 
diff --git a/samples/apps/autogen-studio/frontend/.env.default b/samples/apps/autogen-studio/frontend/.env.default
index da3ebffaa289..7f0839b275d2 100644
--- a/samples/apps/autogen-studio/frontend/.env.default
+++ b/samples/apps/autogen-studio/frontend/.env.default
@@ -1,5 +1 @@
- # use this for .env.development assuming your backend is running on port 8081
 GATSBY_API_URL=http://127.0.0.1:8081/api
-
-# use this .env.production assuming your backend is running on same port as frontend. Remember toremove these comments.
-GATSBY_API_URL=/api
diff --git a/samples/apps/autogen-studio/frontend/README.md b/samples/apps/autogen-studio/frontend/README.md
index 7af58ee311ec..b707495cf42a 100644
--- a/samples/apps/autogen-studio/frontend/README.md
+++ b/samples/apps/autogen-studio/frontend/README.md
@@ -25,6 +25,7 @@ the front end makes request to the backend api and expects it at /api on localho
 
 ## setting env variables for the UI
 
-- please look at env.default
-- make a copy of this file and name it `env.development`
+- please look at `.env.default`
+- make a copy of this file and name it `.env.development`
 - set the values for the variables in this file
+  - The main variable here is `GATSBY_API_URL` which should be set to `http://localhost:8081/api` for local development. This tells the UI where to make requests to the backend.
diff --git a/samples/apps/autogen-studio/frontend/gatsby-config.ts b/samples/apps/autogen-studio/frontend/gatsby-config.ts
index 923e531f5155..9644cfc03898 100644
--- a/samples/apps/autogen-studio/frontend/gatsby-config.ts
+++ b/samples/apps/autogen-studio/frontend/gatsby-config.ts
@@ -1,7 +1,16 @@
 import type { GatsbyConfig } from "gatsby";
+import fs from 'fs';
+
+const envFile = `.env.${process.env.NODE_ENV}`;
+
+fs.access(envFile, fs.constants.F_OK, (err) => {
+  if (err) {
+    console.warn(`File '${envFile}' is missing. Using default values.`);
+  }
+});
 
 require("dotenv").config({
-  path: `.env.${process.env.NODE_ENV}`,
+  path: envFile,
 });
 
 const config: GatsbyConfig = {
diff --git a/samples/apps/autogen-studio/frontend/package.json b/samples/apps/autogen-studio/frontend/package.json
index 2996613fb1f2..80975bab0aba 100644
--- a/samples/apps/autogen-studio/frontend/package.json
+++ b/samples/apps/autogen-studio/frontend/package.json
@@ -22,6 +22,7 @@
     "@heroicons/react": "^2.0.18",
     "@mdx-js/mdx": "^1.6.22",
     "@mdx-js/react": "^1.6.22",
+    "@monaco-editor/react": "^4.6.0",
     "@tailwindcss/line-clamp": "^0.4.0",
     "@tailwindcss/typography": "^0.5.9",
     "@types/lodash.debounce": "^4.0.9",
@@ -40,6 +41,7 @@
     "gatsby-transformer-sharp": "^4.14.0",
     "jszip": "^3.10.1",
     "lodash.debounce": "^4.0.8",
+    "papaparse": "^5.4.1",
     "postcss": "^8.4.13",
     "react": "^18.2.0",
     "react-contenteditable": "^3.3.6",
@@ -56,7 +58,8 @@
   },
   "devDependencies": {
     "@types/node": "^18.7.13",
-    "@types/react": "^18.2.37",
+    "@types/papaparse": "^5.3.14",
+    "@types/react": "^18.2.48",
     "@types/react-dom": "^18.2.15",
     "@types/react-inner-image-zoom": "^3.0.0",
     "@types/react-resizable": "^3.0.2",
diff --git a/samples/apps/autogen-studio/frontend/src/components/atoms.tsx b/samples/apps/autogen-studio/frontend/src/components/atoms.tsx
index dfdc3b4bc63d..a3de42ad31d5 100644
--- a/samples/apps/autogen-studio/frontend/src/components/atoms.tsx
+++ b/samples/apps/autogen-studio/frontend/src/components/atoms.tsx
@@ -5,30 +5,35 @@ import {
   XMarkIcon,
   ClipboardIcon,
   PlusIcon,
-  ArrowPathIcon,
-  ArrowDownRightIcon,
-  PencilIcon,
   UserGroupIcon,
   UsersIcon,
+  ExclamationTriangleIcon,
+  InformationCircleIcon,
 } from "@heroicons/react/24/outline";
 import React, { ReactNode, useEffect, useRef, useState } from "react";
 import Icon from "./icons";
 import {
   Button,
+  Divider,
   Dropdown,
   Input,
   MenuProps,
   Modal,
   Select,
   Slider,
+  Table,
+  Space,
   Tooltip,
   message,
+  theme,
 } from "antd";
+import Editor from "@monaco-editor/react";
+import Papa from "papaparse";
 import remarkGfm from "remark-gfm";
 import ReactMarkdown from "react-markdown";
 import { atomDark } from "react-syntax-highlighter/dist/esm/styles/prism";
 import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
-import { fetchJSON, getServerUrl, truncateText } from "./utils";
+import { fetchJSON, getServerUrl, obscureString, truncateText } from "./utils";
 import {
   IAgentFlowSpec,
   IFlowConfig,
@@ -37,12 +42,10 @@ import {
   ISkill,
   IStatus,
 } from "./types";
-import { ResizableBox } from "react-resizable";
-import debounce from "lodash.debounce";
 import TextArea from "antd/es/input/TextArea";
 import { appContext } from "../hooks/provider";
-import Item from "antd/es/list/Item";
 
+const { useToken } = theme;
 interface CodeProps {
   node?: any;
   inline?: any;
@@ -207,7 +210,7 @@ export const CollapseBox = ({
       </div>
 
       {isOpen && (
-        <div className={`${className} bg-light  rounded rounded-t-none`}>
+        <div className={`${className} bg-tertiary  rounded rounded-t-none`}>
           {children}
         </div>
       )}
@@ -298,7 +301,7 @@ export const GroupView = ({
   return (
     <div className={`rounded mt-4  border-secondary   ${className}`}>
       <div className="mt-4 p-2 rounded border relative">
-        <div className={`absolute  -top-5  p-2 inline-block ${className}`}>
+        <div className={`absolute  -top-3 inline-block ${className}`}>
           {title}
         </div>
         <div className="mt-2"> {children}</div>
@@ -371,7 +374,7 @@ export const LoadingOverlay = ({ children, loading }: IProps) => {
       {loading && (
         <>
           <div
-            className="absolute inset-0 bg-secondary flex"
+            className="absolute inset-0 bg-secondary flex  pointer-events-none"
             style={{ opacity: 0.5 }}
           >
             {/* Overlay background */}
@@ -593,12 +596,13 @@ export const ControlRowView = ({
         <span className="text-primary inline-block">{title} </span>
         <span className="text-xs ml-1 text-accent -mt-2 inline-block">
           {truncateText(value + "", 20)}
-        </span>
+        </span>{" "}
+        <Tooltip title={description}>
+          <InformationCircleIcon className="text-gray-400 inline-block w-4 h-4" />
+        </Tooltip>
       </div>
-      <div className="text-secondary text-xs"> {description} </div>
       {control}
-
-      <div className="border-b border-dashed mt-2 mx-2"></div>
+      <div className="bordper-b  border-secondary border-dashed pb-2 mxp-2"></div>
     </div>
   );
 };
@@ -627,14 +631,14 @@ export const ModelSelector = ({
   const { user } = React.useContext(appContext);
   const listModelsUrl = `${serverUrl}/models?user_id=${user?.email}`;
 
-  const sanitizeModelConfig = (config: IModelConfig) => {
-    const sanitizedConfig: IModelConfig = { model: config.model };
-    if (config.api_key) sanitizedConfig.api_key = config.api_key;
-    if (config.base_url) sanitizedConfig.base_url = config.base_url;
-    if (config.api_type) sanitizedConfig.api_type = config.api_type;
-    if (config.api_version) sanitizedConfig.api_version = config.api_version;
-    return sanitizedConfig;
-  };
+  // const sanitizeModelConfig = (config: IModelConfig) => {
+  //   const sanitizedConfig: IModelConfig = { model: config.model };
+  //   if (config.api_key) sanitizedConfig.api_key = config.api_key;
+  //   if (config.base_url) sanitizedConfig.base_url = config.base_url;
+  //   if (config.api_type) sanitizedConfig.api_type = config.api_type;
+  //   if (config.api_version) sanitizedConfig.api_version = config.api_version;
+  //   return sanitizedConfig;
+  // };
 
   const handleRemoveConfig = (index: number) => {
     const updatedConfigs = configs.filter((_, i) => i !== index);
@@ -684,13 +688,20 @@ export const ModelSelector = ({
     models.length > 0
       ? models.map((model: IModelConfig, index: number) => ({
           key: index,
-          label: model.model,
+          label: (
+            <>
+              <div>{model.model}</div>
+              <div className="text-xs text-accent">
+                {truncateText(model.description || "", 20)}
+              </div>
+            </>
+          ),
           value: index,
         }))
       : [
           {
             key: -1,
-            label: "No models found",
+            label: <>No models found</>,
             value: 0,
           },
         ];
@@ -698,17 +709,50 @@ export const ModelSelector = ({
   const modelOnClick: MenuProps["onClick"] = ({ key }) => {
     const selectedIndex = parseInt(key.toString());
     let selectedModel = models[selectedIndex];
-    selectedModel = sanitizeModelConfig(selectedModel);
+    selectedModel = selectedModel;
     const updatedConfigs = [...configs, selectedModel];
     setConfigs(updatedConfigs);
   };
 
+  const menuStyle: React.CSSProperties = {
+    boxShadow: "none",
+  };
+
+  const { token } = useToken();
+  const contentStyle: React.CSSProperties = {
+    backgroundColor: token.colorBgElevated,
+    borderRadius: token.borderRadiusLG,
+    boxShadow: token.boxShadowSecondary,
+  };
+
+  const addModelsMessage = (
+    <span className="text-xs">
+      {" "}
+      <ExclamationTriangleIcon className="w-4 h-4 inline-block mr-1" /> Please
+      create models in the Model tab
+    </span>
+  );
+
   const AddModelsDropDown = () => {
     return (
       <Dropdown
         menu={{ items: modelItems, onClick: modelOnClick }}
         placement="bottomRight"
         trigger={["click"]}
+        dropdownRender={(menu) => (
+          <div style={contentStyle}>
+            {React.cloneElement(menu as React.ReactElement, {
+              style: menuStyle,
+            })}
+            {models.length === 0 && (
+              <>
+                <Divider style={{ margin: 0 }} />
+                <Space style={{ padding: 8 }}></Space>
+                <div className="p-3">{addModelsMessage}</div>
+              </>
+            )}
+          </div>
+        )}
       >
         <div
           className="inline-flex mr-1 mb-1 p-1 px-2 rounded border hover:border-accent duration-300 hover:text-accent"
@@ -722,7 +766,7 @@ export const ModelSelector = ({
 
   const handleOk = () => {
     if (newModelConfig?.model.trim()) {
-      const sanitizedConfig = sanitizeModelConfig(newModelConfig);
+      const sanitizedConfig = newModelConfig;
 
       if (editIndex !== null) {
         // Edit existing model
@@ -756,15 +800,22 @@ export const ModelSelector = ({
   };
 
   const modelButtons = configs.map((config, i) => {
-    const tooltipText = `${config.model} \n ${config.base_url || ""} \n ${
-      config.api_type || ""
-    }`;
+    const tooltipText = (
+      <>
+        <div>{config.model}</div>
+        {config.base_url && <div>{config.base_url}</div>}
+        {config.api_key && <div>{obscureString(config.api_key, 3)}</div>}
+        <div className="text-xs text-accent">
+          {truncateText(config.description || "", 90)}
+        </div>
+      </>
+    );
     return (
       <div
         key={"modelrow_" + i}
-        role="button"
+        // role="button"
         className="mr-1 mb-1 p-1 px-2 rounded border"
-        onClick={() => showModal(config, i)}
+        // onClick={() => showModal(config, i)}
       >
         <div className="inline-flex">
           {" "}
@@ -889,6 +940,73 @@ export const ImageLoader = ({
   );
 };
 
+type DataRow = { [key: string]: any };
+export const CsvLoader = ({
+  csvUrl,
+  className,
+}: {
+  csvUrl: string;
+  className?: string;
+}) => {
+  const [data, setData] = useState<DataRow[]>([]);
+  const [columns, setColumns] = useState<any[]>([]);
+  const [isLoading, setIsLoading] = useState<boolean>(true);
+
+  useEffect(() => {
+    const fetchData = async () => {
+      try {
+        const response = await fetch(csvUrl);
+        const csvString = await response.text();
+        const parsedData = Papa.parse(csvString, {
+          header: true,
+          dynamicTyping: true,
+          skipEmptyLines: true,
+        });
+        setData(parsedData.data as DataRow[]);
+
+        // Use the keys of the first object for column headers
+        const firstRow = parsedData.data[0] as DataRow; // Type assertion
+        const columnHeaders: any[] = Object.keys(firstRow).map((key) => {
+          const val = {
+            title: key.charAt(0).toUpperCase() + key.slice(1), // Capitalize the key for the title
+            dataIndex: key,
+            key: key,
+          };
+          if (typeof firstRow[key] === "number") {
+            return {
+              ...val,
+              sorter: (a: DataRow, b: DataRow) => a[key] - b[key],
+            };
+          }
+          return val;
+        });
+        setColumns(columnHeaders);
+        setIsLoading(false);
+      } catch (error) {
+        console.error("Error fetching CSV data:", error);
+        setIsLoading(false);
+      }
+    };
+
+    fetchData();
+  }, [csvUrl]);
+
+  // calculate x scroll, based on number of columns
+  const scrollX = columns.length * 150;
+
+  return (
+    <div className={`CsvLoader ${className}`}>
+      <Table
+        dataSource={data}
+        columns={columns}
+        loading={isLoading}
+        pagination={{ pageSize: 50 }}
+        scroll={{ y: 450, x: scrollX }}
+      />
+    </div>
+  );
+};
+
 export const CodeLoader = ({
   url,
   className,
@@ -963,6 +1081,12 @@ export const AgentFlowSpecView = ({
   const [localFlowSpec, setLocalFlowSpec] =
     React.useState<IAgentFlowSpec>(flowSpec);
 
+  // Required to monitor localAgent updates that occur in GroupChatFlowSpecView and reflect updates.
+  useEffect(() => {
+    console.log("AgentFlowSpecView useEffect => flowSpec", flowSpec);
+    setLocalFlowSpec(flowSpec);
+  }, [flowSpec]);
+
   // Event handlers for updating local state and propagating changes
 
   const onControlChange = (value: any, key: string) => {
@@ -985,7 +1109,10 @@ export const AgentFlowSpecView = ({
   return (
     <>
       <div className="text-accent ">{title}</div>
-      <GroupView title={flowSpec.config.name} className="mb-4 bg-primary  ">
+      <GroupView
+        title=<div className="px-2">{flowSpec.config.name}</div>
+        className="mb-4 bg-primary  "
+      >
         <ControlRowView
           title="Agent Name"
           className="mt-4"
@@ -1032,7 +1159,7 @@ export const AgentFlowSpecView = ({
           value={flowSpec.config.max_consecutive_auto_reply}
           control={
             <Slider
-              min={2}
+              min={1}
               max={30}
               defaultValue={flowSpec.config.max_consecutive_auto_reply}
               step={1}
@@ -1450,7 +1577,6 @@ const GroupChatFlowSpecView = ({
           role="button"
           onClick={() => {
             setSelectedAgent(index);
-            // setShowAgentModal(true);
           }}
         >
           <div className="inline-flex">
@@ -1499,12 +1625,45 @@ const GroupChatFlowSpecView = ({
             }}
           />
         )}
-      <GroupView title="Group Chat Agents">
+      <GroupView title=<div className="px-2">Group Chat Agents</div>>
         <div className="flex flex-wrap mt-3">
           {agentsView}
           <AgentDropDown />
         </div>
       </GroupView>
+
+      <ControlRowView
+        title="Speaker Selection Method"
+        description="How the next speaker is selected"
+        className="mt-4"
+        value={flowSpec?.groupchat_config?.speaker_selection_method || "auto"}
+        control={
+          <Select
+            className="mt-2 w-full"
+            defaultValue={
+              flowSpec?.groupchat_config?.speaker_selection_method || "auto"
+            }
+            onChange={(value: any) => {
+              if (flowSpec?.groupchat_config) {
+                setFlowSpec({
+                  ...flowSpec,
+                  groupchat_config: {
+                    ...flowSpec?.groupchat_config,
+                    speaker_selection_method: value,
+                  },
+                });
+              }
+            }}
+            options={
+              [
+                { label: "Auto", value: "auto" },
+                { label: "Round Robin", value: "round_robin" },
+                { label: "Random", value: "random" },
+              ] as any
+            }
+          />
+        }
+      />
     </div>
   );
 };
@@ -1534,6 +1693,11 @@ const AgentModal = ({
     fetchAgents();
   }, []);
 
+  // Required to synchronize localAgent changes between GroupChatFlowSpecView and AgentFlowSpecView
+  useEffect(() => {
+    setLocalAgent(localAgent);
+  }, [localAgent]);
+
   const fetchAgents = () => {
     const onSuccess = (data: any) => {
       if (data && data.status) {
@@ -1576,6 +1740,10 @@ const AgentModal = ({
       onCancel={() => {
         setShowAgentModal(false);
       }}
+      afterClose={() => {
+        // If the modal is closed other than onOk, the agent is reset to before the update; if it is closed onOk, the agent is updated again with the localAgent passed to the handler.
+        setLocalAgent(agent);
+      }}
     >
       {agent && (
         <>
@@ -1585,8 +1753,6 @@ const AgentModal = ({
           </div>
           {localAgent && localAgent.type === "groupchat" && (
             <div>
-              {" "}
-              Group Chat
               <GroupChatFlowSpecView
                 flowSpec={localAgent as IGroupChatFlowSpec}
                 setFlowSpec={setLocalAgent}
@@ -1716,16 +1882,14 @@ export const FlowConfigViewer = ({
     }
   };
 
-  const updateFlowConfigName = (newName: string) => {
-    const updatedFlowConfig = { ...localFlowConfig, name: newName };
+  const updateFlowConfig = (key: string, value: string) => {
+    // When an updatedFlowConfig is created using localFlowConfig, if the contents of FlowConfigViewer Modal are changed after the Agent Specification Modal is updated, the updated contents of the Agent Specification Modal are not saved. Fixed to localFlowConfig->flowConfig. Fixed a bug.
+    const updatedFlowConfig = { ...flowConfig, [key]: value };
+    console.log("updatedFlowConfig: ", updatedFlowConfig);
     setLocalFlowConfig(updatedFlowConfig);
     setFlowConfig(updatedFlowConfig);
   };
 
-  // React.useEffect(() => {
-  //   setLocalFlowConfig(flowConfig);
-  // }, [flowConfig]);
-
   return (
     <>
       {/* <div className="mb-2">{flowConfig.name}</div> */}
@@ -1738,7 +1902,7 @@ export const FlowConfigViewer = ({
           <Input
             className="mt-2 w-full"
             value={localFlowConfig.name}
-            onChange={(e) => updateFlowConfigName(e.target.value)}
+            onChange={(e) => updateFlowConfig("name", e.target.value)}
           />
         }
       />
@@ -1752,14 +1916,7 @@ export const FlowConfigViewer = ({
           <Input
             className="mt-2 w-full"
             value={localFlowConfig.description}
-            onChange={(e) => {
-              const updatedConfig = {
-                ...localFlowConfig,
-                description: e.target.value,
-              };
-              setLocalFlowConfig(updatedConfig);
-              setFlowConfig(updatedConfig);
-            }}
+            onChange={(e) => updateFlowConfig("description", e.target.value)}
           />
         }
       />
@@ -1772,14 +1929,7 @@ export const FlowConfigViewer = ({
           <Select
             className="mt-2 w-full"
             defaultValue={localFlowConfig.summary_method || "last"}
-            onChange={(value: any) => {
-              const updatedConfig = {
-                ...localFlowConfig,
-                summary_method: value,
-              };
-              setLocalFlowConfig(updatedConfig);
-              setFlowConfig(updatedConfig);
-            }}
+            onChange={(value: any) => updateFlowConfig("summary_method", value)}
             options={
               [
                 { label: "last", value: "last" },
@@ -1809,3 +1959,49 @@ export const FlowConfigViewer = ({
     </>
   );
 };
+
+export const MonacoEditor = ({
+  value,
+  editorRef,
+  language,
+  onChange,
+  minimap = true,
+}: {
+  value: string;
+  onChange?: (value: string) => void;
+  editorRef: any;
+  language: string;
+  minimap?: boolean;
+}) => {
+  const [isEditorReady, setIsEditorReady] = useState(false);
+  const onEditorDidMount = (editor: any, monaco: any) => {
+    editorRef.current = editor;
+    setIsEditorReady(true);
+  };
+  return (
+    <div className="h-full rounded">
+      <Editor
+        height="100%"
+        className="h-full rounded"
+        defaultLanguage={language}
+        defaultValue={value}
+        value={value}
+        onChange={(value: string | undefined) => {
+          if (onChange && value) {
+            onChange(value);
+          }
+        }}
+        onMount={onEditorDidMount}
+        theme="vs-dark"
+        options={{
+          wordWrap: "on",
+          wrappingIndent: "indent",
+          wrappingStrategy: "advanced",
+          minimap: {
+            enabled: minimap,
+          },
+        }}
+      />
+    </div>
+  );
+};
diff --git a/samples/apps/autogen-studio/frontend/src/components/footer.tsx b/samples/apps/autogen-studio/frontend/src/components/footer.tsx
index 9514470dc2f1..104547b443d3 100644
--- a/samples/apps/autogen-studio/frontend/src/components/footer.tsx
+++ b/samples/apps/autogen-studio/frontend/src/components/footer.tsx
@@ -10,7 +10,9 @@ const Footer = () => {
   React.useEffect(() => {
     if (version === null) {
       fetchVersion().then((data) => {
-        setVersion(data.data.version);
+        if (data && data.data) {
+          setVersion(data.data.version);
+        }
       });
     }
   }, []);
diff --git a/samples/apps/autogen-studio/frontend/src/components/icons.tsx b/samples/apps/autogen-studio/frontend/src/components/icons.tsx
index 5d7fceab79ef..b2b8e3107800 100644
--- a/samples/apps/autogen-studio/frontend/src/components/icons.tsx
+++ b/samples/apps/autogen-studio/frontend/src/components/icons.tsx
@@ -74,6 +74,42 @@ const Icon = ({ icon = "app", size = 4, className = "" }: Props) => {
     );
   }
 
+  if (icon === "csv") {
+    return (
+      <svg
+        className={` ${sizeClass} inline-block  `}
+        aria-hidden="true"
+        fill="currentColor"
+        viewBox="0 0 50 63"
+      >
+        <path
+          d="M42.6967 62.1044H13.464C11.5281 62.1021 9.67207 61.3321 8.30315 59.9632C6.93422 58.5942 6.16417 56.7382 6.16193 54.8023V45.2659C6.16193 44.9442 6.28972 44.6357 6.5172 44.4082C6.74467 44.1807 7.0532 44.0529 7.3749 44.0529C7.6966 44.0529 8.00513 44.1807 8.2326 44.4082C8.46008 44.6357 8.58787 44.9442 8.58787 45.2659V54.8023C8.58948 56.095 9.10373 57.3344 10.0178 58.2485C10.9319 59.1626 12.1713 59.6768 13.464 59.6784H42.6967C43.9896 59.6768 45.229 59.1626 46.1433 58.2485C47.0576 57.3345 47.5721 56.0951 47.5741 54.8023V43.8746C47.5741 43.5529 47.7019 43.2444 47.9293 43.0169C48.1568 42.7894 48.4653 42.6616 48.787 42.6616C49.1087 42.6616 49.4173 42.7894 49.6447 43.0169C49.8722 43.2444 50 43.5529 50 43.8746V54.8023C49.9975 56.7383 49.2271 58.5944 47.858 59.9632C46.4889 61.3321 44.6328 62.1021 42.6967 62.1044Z"
+          fill="black"
+        />
+        <path
+          d="M48.7822 41.6183C48.4605 41.6183 48.152 41.4906 47.9245 41.2631C47.697 41.0356 47.5692 40.7271 47.5692 40.4054V12.5967C47.5681 12.2529 47.4946 11.9131 47.3533 11.5995C47.212 11.286 47.0062 11.0058 46.7492 10.7773L38.0522 3.04699C37.6064 2.64832 37.0297 2.42731 36.4317 2.42595H14.677C13.0616 2.42852 11.5131 3.07163 10.3712 4.21425C9.22922 5.35686 8.58704 6.90572 8.58543 8.52114V23.3037C8.58543 23.6254 8.45764 23.9339 8.23016 24.1614C8.00268 24.3888 7.69416 24.5166 7.37246 24.5166C7.05076 24.5166 6.74223 24.3888 6.51476 24.1614C6.28728 23.9339 6.15948 23.6254 6.15948 23.3037V8.52114C6.16173 6.26251 7.05971 4.09698 8.65646 2.49955C10.2532 0.902118 12.4184 0.0032109 14.677 9.10874e-08H36.4317C37.6233 -0.000230408 38.7736 0.437008 39.6643 1.22874L48.3613 8.96024C48.8752 9.41695 49.2866 9.97738 49.5682 10.6046C49.8498 11.2318 49.9953 11.9116 49.9952 12.5992V40.4054C49.9952 40.7271 49.8674 41.0356 49.6399 41.2631C49.4124 41.4906 49.1039 41.6183 48.7822 41.6183Z"
+          fill="black"
+        />
+        <path
+          d="M48.7203 13.1681H41.474C40.1838 13.1665 38.947 12.6533 38.0347 11.741C37.1224 10.8287 36.6091 9.59184 36.6075 8.30167V1.49325C36.6075 1.17155 36.7353 0.863022 36.9628 0.635545C37.1903 0.408069 37.4988 0.280273 37.8205 0.280273C38.1422 0.280273 38.4507 0.408069 38.6782 0.635545C38.9057 0.863022 39.0335 1.17155 39.0335 1.49325V8.30167C39.0341 8.94874 39.2915 9.56911 39.749 10.0267C40.2066 10.4842 40.8269 10.7415 41.474 10.7422H48.7203C49.042 10.7422 49.3505 10.87 49.578 11.0974C49.8055 11.3249 49.9333 11.6334 49.9333 11.9551C49.9333 12.2768 49.8055 12.5854 49.578 12.8129C49.3505 13.0403 49.042 13.1681 48.7203 13.1681Z"
+          fill="black"
+        />
+        <path
+          d="M6.10975 52.2791H4.24541C3.11946 52.2791 2.03962 51.8319 1.24345 51.0357C0.447283 50.2395 0 49.1597 0 48.0337C0 46.9078 0.447283 45.8279 1.24345 45.0318C2.03962 44.2356 3.11946 43.7883 4.24541 43.7883H9.75474C10.0764 43.7883 10.385 43.9161 10.6124 44.1436C10.8399 44.3711 10.9677 44.6796 10.9677 45.0013C10.9677 45.323 10.8399 45.6315 10.6124 45.859C10.385 46.0865 10.0764 46.2143 9.75474 46.2143H4.24541C3.76286 46.2143 3.30007 46.406 2.95886 46.7472C2.61764 47.0884 2.42595 47.5512 2.42595 48.0337C2.42595 48.5163 2.61764 48.9791 2.95886 49.3203C3.30007 49.6615 3.76286 49.8532 4.24541 49.8532H6.11339C6.43509 49.8532 6.74361 49.981 6.97109 50.2085C7.19857 50.4359 7.32636 50.7445 7.32636 51.0662C7.32636 51.3879 7.19857 51.6964 6.97109 51.9239C6.74361 52.1514 6.43509 52.2791 6.11339 52.2791H6.10975Z"
+          fill="black"
+        />
+        <path
+          d="M1.22872 48.8975C0.90702 48.8975 0.598496 48.7697 0.371019 48.5423C0.143542 48.3148 0.0157471 48.0063 0.0157471 47.6846L0.067905 26.3362C0.069833 25.2115 0.517967 24.1336 1.31393 23.339C2.10989 22.5444 3.18862 22.0981 4.31331 22.0981L48.7846 22.0896C49.1063 22.0896 49.4148 22.2174 49.6423 22.4449C49.8698 22.6723 49.9976 22.9809 49.9976 23.3026V45.0015C49.9976 45.3232 49.8698 45.6317 49.6423 45.8592C49.4148 46.0866 49.1063 46.2144 48.7846 46.2144H5.52386C5.20216 46.2144 4.89364 46.0866 4.66616 45.8592C4.43868 45.6317 4.31089 45.3232 4.31089 45.0015C4.31089 44.6798 4.43868 44.3712 4.66616 44.1438C4.89364 43.9163 5.20216 43.7885 5.52386 43.7885H47.5692V24.5083L4.30967 24.5168C3.82712 24.5168 3.36434 24.7085 3.02312 25.0497C2.68191 25.3909 2.49021 25.8537 2.49021 26.3362L2.43806 47.6846C2.43806 48.0056 2.31076 48.3136 2.08407 48.541C1.85738 48.7684 1.54979 48.8966 1.22872 48.8975Z"
+          fill="black"
+        />
+        <path
+          d="M18.1587 36.5474C18.4077 36.5474 18.6291 36.4948 18.8228 36.3896C19.022 36.2845 19.1797 36.1213 19.2959 35.8999C19.4121 35.673 19.4757 35.3797 19.4868 35.02H21.3047C21.2936 35.6564 21.147 36.2126 20.8647 36.6885C20.5825 37.1589 20.2062 37.5241 19.7358 37.7842C19.271 38.0387 18.7536 38.166 18.1836 38.166C17.5915 38.166 17.0741 38.0719 16.6313 37.8838C16.1942 37.6901 15.8317 37.4106 15.5439 37.0454C15.2562 36.6802 15.0404 36.2375 14.8965 35.7173C14.7581 35.1916 14.689 34.5911 14.689 33.916V33.1025C14.689 32.4329 14.7581 31.8353 14.8965 31.3096C15.0404 30.7839 15.2562 30.3384 15.5439 29.9731C15.8317 29.6079 16.1942 29.3312 16.6313 29.1431C17.0685 28.9494 17.5832 28.8525 18.1753 28.8525C18.8062 28.8525 19.3512 28.9854 19.8105 29.251C20.2754 29.5111 20.6379 29.8929 20.8979 30.3965C21.158 30.9001 21.2936 31.5171 21.3047 32.2476H19.4868C19.4757 31.8602 19.4176 31.5365 19.3125 31.2764C19.2074 31.0107 19.0579 30.8115 18.8643 30.6787C18.6761 30.5459 18.4382 30.4795 18.1504 30.4795C17.835 30.4795 17.5749 30.5404 17.3701 30.6621C17.1709 30.7783 17.016 30.9499 16.9053 31.1768C16.8001 31.4036 16.7254 31.6803 16.6812 32.0068C16.6424 32.3278 16.623 32.693 16.623 33.1025V33.916C16.623 34.3366 16.6424 34.7101 16.6812 35.0366C16.7199 35.3576 16.7918 35.6315 16.897 35.8584C17.0076 36.0853 17.1654 36.2568 17.3701 36.373C17.5749 36.4893 17.8377 36.5474 18.1587 36.5474ZM26.9658 35.6343C26.9658 35.4461 26.9105 35.2801 26.7998 35.1362C26.6947 34.9868 26.5231 34.8402 26.2852 34.6963C26.0472 34.5524 25.729 34.3975 25.3306 34.2314C24.9155 34.0599 24.5475 33.8883 24.2266 33.7168C23.9056 33.5452 23.6344 33.3571 23.4131 33.1523C23.1917 32.9421 23.0229 32.7013 22.9067 32.4302C22.7905 32.159 22.7324 31.8436 22.7324 31.4839C22.7324 31.1131 22.8016 30.77 22.9399 30.4546C23.0783 30.1392 23.2775 29.8625 23.5376 29.6245C23.7977 29.381 24.1104 29.1929 24.4756 29.0601C24.8408 28.9217 25.2531 28.8525 25.7124 28.8525C26.3599 28.8525 26.9132 28.9715 27.3726 29.2095C27.8374 29.4419 28.1916 29.7684 28.4351 30.189C28.6841 30.604 28.8086 31.0827 28.8086 31.625H26.8745C26.8745 31.3981 26.833 31.1934 26.75 31.0107C26.667 30.8226 26.5397 30.6732 26.3682 30.5625C26.1966 30.4463 25.978 30.3882 25.7124 30.3882C25.4689 30.3882 25.2642 30.4352 25.0981 30.5293C24.9377 30.6178 24.8159 30.7396 24.7329 30.8945C24.6554 31.0439 24.6167 31.2127 24.6167 31.4009C24.6167 31.5392 24.6416 31.661 24.6914 31.7661C24.7412 31.8713 24.8242 31.9736 24.9404 32.0732C25.0566 32.1673 25.2116 32.2642 25.4053 32.3638C25.6045 32.4634 25.8563 32.5768 26.1606 32.7041C26.7638 32.9365 27.2646 33.1772 27.6631 33.4263C28.0615 33.6753 28.3604 33.9658 28.5596 34.2979C28.7588 34.6299 28.8584 35.0422 28.8584 35.5347C28.8584 35.9331 28.7837 36.2928 28.6343 36.6138C28.4904 36.9347 28.2801 37.2114 28.0034 37.4438C27.7323 37.6763 27.403 37.8561 27.0156 37.9834C26.6338 38.1051 26.2077 38.166 25.7373 38.166C25.0345 38.166 24.4396 38.0277 23.9526 37.751C23.4712 37.4688 23.106 37.1063 22.8569 36.6636C22.6079 36.2209 22.4834 35.7533 22.4834 35.2607H24.3511C24.3677 35.6149 24.4424 35.8916 24.5752 36.0908C24.7135 36.29 24.8879 36.4284 25.0981 36.5059C25.3084 36.5778 25.527 36.6138 25.7539 36.6138C26.0251 36.6138 26.2492 36.5778 26.4263 36.5059C26.6034 36.4284 26.7362 36.3149 26.8247 36.1655C26.9188 36.0161 26.9658 35.839 26.9658 35.6343ZM33.0503 36.3896L34.7104 29.0186H36.7109L34.1543 38H32.9175L33.0503 36.3896ZM31.8716 29.0186L33.5898 36.4146L33.6729 38H32.4443L29.8711 29.0186H31.8716Z"
+          fill="black"
+        />
+      </svg>
+    );
+  }
+
   if (icon === "pdf") {
     return (
       <svg
diff --git a/samples/apps/autogen-studio/frontend/src/components/types.ts b/samples/apps/autogen-studio/frontend/src/components/types.ts
index 91f9119b738b..f9b140a4815b 100644
--- a/samples/apps/autogen-studio/frontend/src/components/types.ts
+++ b/samples/apps/autogen-studio/frontend/src/components/types.ts
@@ -57,7 +57,7 @@ export interface IGroupChatConfig {
   admin_name: string;
   messages: Array<any>;
   max_round: number;
-  speaker_selection_method: string;
+  speaker_selection_method: "auto" | "round_robin" | "random";
   allow_repeat_speaker: boolean | Array<IAgentConfig>;
 }
 
@@ -119,7 +119,7 @@ export interface IGalleryItem {
 
 export interface ISkill {
   title: string;
-  file_name: string;
+  file_name?: string;
   content: string;
   id?: string;
   timestamp?: string;
diff --git a/samples/apps/autogen-studio/frontend/src/components/utils.ts b/samples/apps/autogen-studio/frontend/src/components/utils.ts
index abf5ec914eb3..04e8c32b44a9 100644
--- a/samples/apps/autogen-studio/frontend/src/components/utils.ts
+++ b/samples/apps/autogen-studio/frontend/src/components/utils.ts
@@ -5,6 +5,7 @@ import {
   IGroupChatFlowSpec,
   ILLMConfig,
   IModelConfig,
+  ISkill,
   IStatus,
 } from "./types";
 
@@ -196,6 +197,25 @@ export const guid = () => {
   return `${w()}${w()}-${w()}-${w()}-${w()}-${w()}${w()}${w()}`;
 };
 
+/**
+ * Takes a string and returns the first n characters followed by asterisks.
+ * @param {string} str - The string to obscure
+ * @param {number} n - Number of characters to show before obscuring
+ * @returns {string} The obscured string with first n characters in clear text
+ */
+export const obscureString = (str: string, n: number = 3) => {
+  if (n < 0 || n > str.length) {
+    console.log("n cannot be less than 0 or greater than the string length.");
+    return str;
+  }
+  // First n characters in clear text
+  var clearText = str.substring(0, n);
+  // Remaining characters replaced with asterisks
+  var obscured = clearText + "*".repeat(str.length - n);
+
+  return obscured;
+};
+
 /**
  * Converts a number of seconds into a human-readable string representing the duration in days, hours, minutes, and seconds.
  * @param {number} seconds - The number of seconds to convert.
@@ -259,7 +279,7 @@ export const sampleWorkflowConfig = (type = "twoagents") => {
     human_input_mode: "NEVER",
     max_consecutive_auto_reply: 8,
     system_message:
-      "You are a helpful assistant that can use available functions when needed to solve problems. At each point, do your best to determine if the user's request has been addressed. IF THE REQUEST HAS NOT BEEN ADDRESSED, RESPOND WITH CODE TO ADDRESS IT. IF A FAILURE OCCURRED (e.g., due to a missing library) AND SOME ADDITIONAL CODE WAS WRITTEN (e.g. code to install the library), ENSURE THAT THE ORIGINAL CODE TO ADDRESS THE TASK STILL GETS EXECUTED. If the request HAS been addressed, respond with a summary of the result. The summary must be written as a coherent helpful response to the user request e.g. 'Sure, here is result to your request ' or 'The tallest mountain in Africa is ..' etc.  The summary MUST end with the word TERMINATE. If the user request is  pleasantry or greeting, you should respond with a pleasantry or greeting and TERMINATE.",
+      "You are a helpful assistant that can use available functions when needed to solve problems. At each point, do your best to determine if the user's request has been addressed. IF THE REQUEST HAS NOT BEEN ADDRESSED, RESPOND WITH CODE TO ADDRESS IT. IF A FAILURE OCCURRED (e.g., due to a missing library) AND SOME ADDITIONAL CODE WAS WRITTEN (e.g. code to install the library), ENSURE THAT THE ORIGINAL CODE TO ADDRESS THE TASK STILL GETS EXECUTED. If the request HAS been addressed, respond with a summary of the result. The summary must be written as a coherent helpful response to the user request e.g. 'Sure, here is result to your request ' or 'The tallest mountain in Africa is ..' etc. If the user request is  pleasantry or greeting, you should respond with a pleasantry or greeting and TERMINATE.",
   };
 
   const assistantFlowSpec: IAgentFlowSpec = {
@@ -278,7 +298,7 @@ export const sampleWorkflowConfig = (type = "twoagents") => {
   const groupChatAssistantConfig = Object.assign({}, assistantConfig);
   groupChatAssistantConfig.name = "groupchat_assistant";
   groupChatAssistantConfig.system_message =
-    "You are a helpful assistant skilled at coordinating a group of other assistants to solve a task. ";
+    "You are a helpful assistant skilled at cordinating a group of other assistants to solve a task. ";
 
   const groupChatFlowSpec: IGroupChatFlowSpec = {
     type: "groupchat",
@@ -327,8 +347,13 @@ export const getModels = () => {
 };
 
 export const getSampleSkill = () => {
-  const catSkill = `
-  # this is a sample skill. Replace with your own skill function
+  const content = `
+  ## This is a sample skill. Replace with your own skill function
+  ## In general, a good skill must have 3 sections:
+  ## 1. Imports (import libraries needed for your skill)
+  ## 2. Function definition  AND docstrings (this helps the LLM understand what the function does and how to use it)
+  ## 3. Function body (the actual code that implements the function)
+
   import numpy as np
   import matplotlib.pyplot as plt
   from matplotlib import font_manager as fm
@@ -367,7 +392,14 @@ export const getSampleSkill = () => {
       # Save figure to file
       plt.savefig(filename, dpi=120, bbox_inches='tight', pad_inches=0.1)
       plt.close(fig)`;
-  return catSkill;
+
+  const skill: ISkill = {
+    title: "save_cat_ascii_art_to_png",
+    description: "save cat ascii art to png",
+    content: content,
+  };
+
+  return skill;
 };
 
 export const timeAgo = (dateString: string): string => {
@@ -447,3 +479,29 @@ export const fetchVersion = () => {
       return null;
     });
 };
+
+/**
+ * Recursively sanitizes JSON objects by replacing specific keys with a given value.
+ * @param {JsonValue} data - The JSON data to be sanitized.
+ * @param {string[]} keys - An array of keys to be replaced in the JSON object.
+ * @param {string} replacement - The value to use as replacement for the specified keys.
+ * @returns {JsonValue} - The sanitized JSON data.
+ */
+export const sanitizeConfig = (
+  data: any,
+  keys: string[] = ["api_key"],
+  replacement: string = "********"
+): any => {
+  if (Array.isArray(data)) {
+    return data.map((item) => sanitizeConfig(item, keys, replacement));
+  } else if (typeof data === "object" && data !== null) {
+    Object.keys(data).forEach((key) => {
+      if (keys.includes(key)) {
+        data[key] = replacement;
+      } else {
+        data[key] = sanitizeConfig(data[key], keys, replacement);
+      }
+    });
+  }
+  return data;
+};
diff --git a/samples/apps/autogen-studio/frontend/src/components/views/builder/agents.tsx b/samples/apps/autogen-studio/frontend/src/components/views/builder/agents.tsx
index 8ee6a7a97753..ac2a08f11a55 100644
--- a/samples/apps/autogen-studio/frontend/src/components/views/builder/agents.tsx
+++ b/samples/apps/autogen-studio/frontend/src/components/views/builder/agents.tsx
@@ -186,19 +186,26 @@ const AgentsView = ({}: any) => {
               {truncateText(agent.description || "", 70)}
             </div>
             <div className="text-xs">{timeAgo(agent.timestamp || "")}</div>
-          </Card>
-          <div className="text-right mt-2">
             <div
-              role="button"
-              className="text-accent text-xs inline-block"
-              onClick={() => {
-                deleteAgent(agent);
+              onMouseEnter={(e) => {
+                e.stopPropagation();
               }}
+              className=" mt-2 text-right opacity-0 group-hover:opacity-100 "
             >
-              <TrashIcon className=" w-5, h-5 cursor-pointer inline-block" />
-              <span className="text-xs"> delete</span>
+              {" "}
+              <div
+                role="button"
+                className="text-accent text-xs inline-block hover:bg-primary p-2 rounded"
+                onClick={(e) => {
+                  e.stopPropagation();
+                  deleteAgent(agent);
+                }}
+              >
+                <TrashIcon className=" w-5, h-5 cursor-pointer inline-block" />
+                <span className="text-xs hidden"> delete</span>
+              </div>
             </div>
-          </div>
+          </Card>
         </div>
       </div>
     );
diff --git a/samples/apps/autogen-studio/frontend/src/components/views/builder/build.tsx b/samples/apps/autogen-studio/frontend/src/components/views/builder/build.tsx
index c7569ca05f7f..bf8128fe0f9c 100644
--- a/samples/apps/autogen-studio/frontend/src/components/views/builder/build.tsx
+++ b/samples/apps/autogen-studio/frontend/src/components/views/builder/build.tsx
@@ -15,7 +15,7 @@ const BuildView = () => {
   return (
     <div className=" ">
       {/* <div className="mb-4 text-2xl">Build </div> */}
-      <div className="mb-6 text-sm text-secondary">
+      <div className="mb-6 text-sm hidden text-secondary">
         {" "}
         Create skills, agents and workflows for building multiagent capabilities{" "}
       </div>
diff --git a/samples/apps/autogen-studio/frontend/src/components/views/builder/models.tsx b/samples/apps/autogen-studio/frontend/src/components/views/builder/models.tsx
index 8344c54ec971..d3e2f5b592b4 100644
--- a/samples/apps/autogen-studio/frontend/src/components/views/builder/models.tsx
+++ b/samples/apps/autogen-studio/frontend/src/components/views/builder/models.tsx
@@ -1,21 +1,15 @@
 import {
+  ExclamationTriangleIcon,
   InformationCircleIcon,
   PlusIcon,
   TrashIcon,
 } from "@heroicons/react/24/outline";
-import { Input, Modal, message } from "antd";
+import { Button, Input, Modal, message } from "antd";
 import * as React from "react";
-import { IAgentFlowSpec, IModelConfig, IStatus } from "../../types";
+import { IModelConfig, IStatus } from "../../types";
 import { appContext } from "../../../hooks/provider";
 import { fetchJSON, getServerUrl, timeAgo, truncateText } from "../../utils";
-import {
-  AgentFlowSpecView,
-  BounceLoader,
-  Card,
-  LaunchButton,
-  LoadBox,
-  LoadingOverlay,
-} from "../../atoms";
+import { BounceLoader, Card, LaunchButton, LoadingOverlay } from "../../atoms";
 import TextArea from "antd/es/input/TextArea";
 
 const ModelsView = ({}: any) => {
@@ -30,10 +24,15 @@ const ModelsView = ({}: any) => {
   const listModelsUrl = `${serverUrl}/models?user_id=${user?.email}`;
   const saveModelsUrl = `${serverUrl}/models`;
   const deleteModelUrl = `${serverUrl}/models/delete`;
+  const testModelUrl = `${serverUrl}/models/test`;
+  const [modelStatus, setModelStatus] = React.useState<IStatus | null>({
+    status: true,
+    message: "All good",
+  });
 
   const defaultModel: IModelConfig = {
     model: "gpt-4-1106-preview",
-    description: "Sample model",
+    description: "Sample OpenAI GPT-4 model",
     user_id: user?.email,
   };
 
@@ -148,12 +147,6 @@ const ModelsView = ({}: any) => {
     }
   }, []);
 
-  React.useEffect(() => {
-    if (selectedModel) {
-      console.log("selected agent", selectedModel);
-    }
-  }, [selectedModel]);
-
   const modelRows = (models || []).map((model: IModelConfig, i: number) => {
     return (
       <div key={"modelrow" + i} className=" " style={{ width: "200px" }}>
@@ -161,7 +154,7 @@ const ModelsView = ({}: any) => {
           <Card
             className="h-full p-2 cursor-pointer"
             title={
-              <div className="  ">{truncateText(model.model || "", 25)}</div>
+              <div className="  ">{truncateText(model.model || "", 20)}</div>
             }
             onClick={() => {
               setSelectedModel(model);
@@ -173,19 +166,26 @@ const ModelsView = ({}: any) => {
               {truncateText(model.description || model.model || "", 70)}
             </div>
             <div className="text-xs">{timeAgo(model.timestamp || "")}</div>
-          </Card>
-          <div className="text-right mt-2">
             <div
-              role="button"
-              className="text-accent text-xs inline-block"
-              onClick={() => {
-                deleteModel(model);
+              onMouseEnter={(e) => {
+                e.stopPropagation();
               }}
+              className=" mt-2 text-right opacity-0 group-hover:opacity-100 "
             >
-              <TrashIcon className=" w-5, h-5 cursor-pointer inline-block" />
-              <span className="text-xs"> delete</span>
+              {" "}
+              <div
+                role="button"
+                className="text-accent text-xs inline-block hover:bg-primary p-2 rounded"
+                onClick={(e) => {
+                  e.stopPropagation();
+                  deleteModel(model);
+                }}
+              >
+                <TrashIcon className=" w-5, h-5 cursor-pointer inline-block" />
+                <span className="text-xs hidden"> delete</span>
+              </div>
             </div>
-          </div>
+          </Card>
         </div>
       </div>
     );
@@ -204,9 +204,42 @@ const ModelsView = ({}: any) => {
     setShowModelModal: (show: boolean) => void;
     handler?: (agent: IModelConfig) => void;
   }) => {
+    const [loadingModelTest, setLoadingModelTest] = React.useState(false);
+    const [modelStatus, setModelStatus] = React.useState<IStatus | null>(null);
+
     const [localModel, setLocalModel] = React.useState<IModelConfig | null>(
       model
     );
+    const testModel = (model: IModelConfig) => {
+      setModelStatus(null);
+      setLoadingModelTest(true);
+      const payLoad = {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify({
+          user_id: user?.email,
+          model: model,
+        }),
+      };
+
+      const onSuccess = (data: any) => {
+        if (data && data.status) {
+          message.success(data.message);
+          setModelStatus(data.data);
+        } else {
+          message.error(data.message);
+        }
+        setLoadingModelTest(false);
+        setModelStatus(data);
+      };
+      const onError = (err: any) => {
+        message.error(err.message);
+        setLoadingModelTest(false);
+      };
+      fetchJSON(testModelUrl, payLoad, onSuccess, onError);
+    };
 
     return (
       <Modal
@@ -218,6 +251,44 @@ const ModelsView = ({}: any) => {
         }
         width={800}
         open={showModelModal}
+        footer={[
+          <Button
+            key="close"
+            onClick={() => {
+              setModel(null);
+              setShowModelModal(false);
+            }}
+          >
+            Close
+          </Button>,
+          <Button
+            key="test"
+            type="primary"
+            loading={loadingModelTest}
+            onClick={() => {
+              if (localModel) {
+                testModel(localModel);
+              }
+            }}
+          >
+            Test Model
+          </Button>,
+          <Button
+            key="save"
+            type="primary"
+            onClick={() => {
+              setModel(null);
+              setShowModelModal(false);
+              if (handler) {
+                if (localModel) {
+                  handler(localModel);
+                }
+              }
+            }}
+          >
+            Save
+          </Button>,
+        ]}
         onOk={() => {
           setModel(null);
           setShowModelModal(false);
@@ -232,70 +303,85 @@ const ModelsView = ({}: any) => {
           setShowModelModal(false);
         }}
       >
-        <div className="text-sm my-2">Enter parameters for your model.</div>
-        <Input
-          placeholder="Model Name"
-          value={localModel?.model}
-          onChange={(e) => {
-            setLocalModel({ ...localModel, model: e.target.value });
-          }}
-        />
-        <Input.Password
-          className="mt-2"
-          placeholder="API Key"
-          value={localModel?.api_key}
-          onChange={(e) => {
-            if (localModel) {
-              setLocalModel({ ...localModel, api_key: e.target.value });
-            }
-          }}
-        />
-        <Input
-          className="mt-2"
-          placeholder="Base URL"
-          value={localModel?.base_url}
-          onChange={(e) => {
-            if (localModel) {
-              setLocalModel({ ...localModel, base_url: e.target.value });
-            }
-          }}
-        />
-        <Input
-          className="mt-2"
-          placeholder="API Type (e.g. azure)"
-          value={localModel?.api_type}
-          onChange={(e) => {
-            if (localModel) {
-              setLocalModel({ ...localModel, api_type: e.target.value });
-            }
-          }}
-        />
-        <Input
-          className="mt-2"
-          placeholder="API Version (optional)"
-          value={localModel?.api_version}
-          onChange={(e) => {
-            if (localModel) {
-              setLocalModel({ ...localModel, api_version: e.target.value });
-            }
-          }}
-        />
-        <TextArea
-          className="mt-2"
-          placeholder="Description"
-          value={localModel?.description}
-          onChange={(e) => {
-            if (localModel) {
-              setLocalModel({ ...localModel, description: e.target.value });
-            }
-          }}
-        />
+        <div className="relative ">
+          <div className="text-sm my-2">Enter parameters for your model.</div>
+          <Input
+            placeholder="Model Name"
+            value={localModel?.model}
+            onChange={(e) => {
+              setLocalModel({ ...localModel, model: e.target.value });
+            }}
+          />
+          <Input.Password
+            className="mt-2"
+            placeholder="API Key"
+            value={localModel?.api_key}
+            onChange={(e) => {
+              if (localModel) {
+                setLocalModel({ ...localModel, api_key: e.target.value });
+              }
+            }}
+          />
+          <Input
+            className="mt-2"
+            placeholder="Base URL"
+            value={localModel?.base_url}
+            onChange={(e) => {
+              if (localModel) {
+                setLocalModel({ ...localModel, base_url: e.target.value });
+              }
+            }}
+          />
+          <Input
+            className="mt-2"
+            placeholder="API Type (e.g. azure)"
+            value={localModel?.api_type}
+            onChange={(e) => {
+              if (localModel) {
+                setLocalModel({ ...localModel, api_type: e.target.value });
+              }
+            }}
+          />
+          <Input
+            className="mt-2"
+            placeholder="API Version (optional)"
+            value={localModel?.api_version}
+            onChange={(e) => {
+              if (localModel) {
+                setLocalModel({ ...localModel, api_version: e.target.value });
+              }
+            }}
+          />
+          <TextArea
+            className="mt-2"
+            placeholder="Description"
+            value={localModel?.description}
+            onChange={(e) => {
+              if (localModel) {
+                setLocalModel({ ...localModel, description: e.target.value });
+              }
+            }}
+          />
 
-        {localModel?.api_type === "azure" && (
-          <div className="mt-4 text-xs">
-            Note: For Azure OAI models, you will need to specify all fields.
-          </div>
-        )}
+          {localModel?.api_type === "azure" && (
+            <div className="mt-4 text-xs">
+              Note: For Azure OAI models, you will need to specify all fields.
+            </div>
+          )}
+
+          {modelStatus && (
+            <div
+              className={`text-sm border mt-4 rounded text-secondary p-2 ${
+                modelStatus.status ? "border-accent" : " border-red-500 "
+              }`}
+            >
+              <InformationCircleIcon className="h-4 w-4 inline mr-1" />
+              {modelStatus.message}
+
+              {/* <span className="block"> Note </span> */}
+            </div>
+          )}
+        </div>
       </Modal>
     );
   };
@@ -349,6 +435,17 @@ const ModelsView = ({}: any) => {
             {" "}
             Create model configurations that can be reused in your agents and
             workflows. {selectedModel?.model}
+            {models && models.length > 0 && (
+              <span className="block my-2 border rounded border-secondary p-2">
+                <ExclamationTriangleIcon className="w-4 h-4 inline-block mr-1" />{" "}
+                Note: Changes made to your model do not automatically get
+                updated in your workflow. After creating or editing your model,{" "}
+                <span className="font-semibold underline">
+                  you must also (re-)add
+                </span>{" "}
+                it to your workflow.
+              </span>
+            )}
           </div>
           {models && models.length > 0 && (
             <div className="w-full  relative">
@@ -360,7 +457,8 @@ const ModelsView = ({}: any) => {
           {models && models.length === 0 && !loading && (
             <div className="text-sm border mt-4 rounded text-secondary p-2">
               <InformationCircleIcon className="h-4 w-4 inline mr-1" />
-              No models found. Please create a new agent.
+              No models found. Please create a new model which can be reused
+              with agents.
             </div>
           )}
 
diff --git a/samples/apps/autogen-studio/frontend/src/components/views/builder/skills.tsx b/samples/apps/autogen-studio/frontend/src/components/views/builder/skills.tsx
index 6117d905571d..7574fa72a623 100644
--- a/samples/apps/autogen-studio/frontend/src/components/views/builder/skills.tsx
+++ b/samples/apps/autogen-studio/frontend/src/components/views/builder/skills.tsx
@@ -3,7 +3,7 @@ import {
   PlusIcon,
   TrashIcon,
 } from "@heroicons/react/24/outline";
-import { Input, Modal, message } from "antd";
+import { Button, Input, Modal, message } from "antd";
 import * as React from "react";
 import { ISkill, IStatus } from "../../types";
 import { appContext } from "../../../hooks/provider";
@@ -20,8 +20,8 @@ import {
   CodeBlock,
   LaunchButton,
   LoadingOverlay,
+  MonacoEditor,
 } from "../../atoms";
-import { useConfigStore } from "../../../hooks/store";
 import TextArea from "antd/es/input/TextArea";
 
 const SkillsView = ({}: any) => {
@@ -43,10 +43,8 @@ const SkillsView = ({}: any) => {
   const [showSkillModal, setShowSkillModal] = React.useState(false);
   const [showNewSkillModal, setShowNewSkillModal] = React.useState(false);
 
-  const [newSkillTitle, setNewSkillTitle] = React.useState("");
-
   const sampleSkill = getSampleSkill();
-  const [skillCode, setSkillCode] = React.useState(sampleSkill);
+  const [newSkill, setNewSkill] = React.useState<ISkill | null>(sampleSkill);
 
   const deleteSkill = (skill: ISkill) => {
     setError(null);
@@ -109,21 +107,7 @@ const SkillsView = ({}: any) => {
     fetchJSON(listSkillsUrl, payLoad, onSuccess, onError);
   };
 
-  const saveSkill = () => {
-    // check if skillTextAreaRef.current is not null or ""
-
-    if (!skillCode || skillCode == "" || skillCode == sampleSkill) {
-      message.error("Please provide code for the skill");
-      return;
-    }
-
-    const skill: ISkill = {
-      title: newSkillTitle,
-      file_name: "skill.py",
-      content: skillCode,
-      user_id: user?.email,
-    };
-
+  const saveSkill = (skill: ISkill) => {
     setError(null);
     setLoading(true);
     // const fetch;
@@ -148,7 +132,6 @@ const SkillsView = ({}: any) => {
         message.error(data.message);
       }
       setLoading(false);
-      setSkillCode("");
     };
     const onError = (err: any) => {
       setError(err);
@@ -171,7 +154,7 @@ const SkillsView = ({}: any) => {
         <div>
           {" "}
           <Card
-            className="h-full p-2 cursor-pointer"
+            className="h-full p-2 cursor-pointer group"
             title={truncateText(skill.title, 25)}
             onClick={() => {
               setSelectedSkill(skill);
@@ -183,47 +166,131 @@ const SkillsView = ({}: any) => {
               {truncateText(skill.content, 70)}
             </div>
             <div className="text-xs">{timeAgo(skill.timestamp || "")}</div>
-          </Card>
-          <div className="text-right mt-2">
             <div
-              role="button"
-              className="text-accent text-xs inline-block"
-              onClick={() => {
-                deleteSkill(skill);
+              onMouseEnter={(e) => {
+                e.stopPropagation();
               }}
+              className=" mt-2 text-right opacity-0 group-hover:opacity-100 "
             >
-              <TrashIcon className=" w-5, h-5 cursor-pointer inline-block" />
-              <span className="text-xs"> delete</span>
+              {" "}
+              <div
+                role="button"
+                className="text-accent text-xs inline-block hover:bg-primary p-2 rounded"
+                onClick={(e) => {
+                  e.stopPropagation();
+                  deleteSkill(skill);
+                }}
+              >
+                <TrashIcon className=" w-5, h-5 cursor-pointer inline-block" />
+                <span className="text-xs hidden"> delete</span>
+              </div>
             </div>
-          </div>
+          </Card>
+          <div className="text-right mt-2"></div>
         </div>
       </div>
     );
   });
 
-  return (
-    <div className=" text-primary ">
+  const SkillModal = ({
+    skill,
+    setSkill,
+    showSkillModal,
+    setShowSkillModal,
+    handler,
+  }: {
+    skill: ISkill | null;
+    setSkill: any;
+    showSkillModal: boolean;
+    setShowSkillModal: any;
+    handler: any;
+  }) => {
+    const editorRef = React.useRef<any | null>(null);
+    const [localSkill, setLocalSkill] = React.useState<ISkill | null>(skill);
+    return (
       <Modal
-        title={selectedSkill?.name}
+        title={
+          <>
+            Skill Specification{" "}
+            <span className="text-accent font-normal">{localSkill?.title}</span>{" "}
+          </>
+        }
         width={800}
         open={showSkillModal}
-        onOk={() => {
-          setShowSkillModal(false);
-        }}
         onCancel={() => {
           setShowSkillModal(false);
         }}
+        footer={[
+          <Button
+            key="back"
+            onClick={() => {
+              setShowSkillModal(false);
+            }}
+          >
+            Cancel
+          </Button>,
+          <Button
+            key="submit"
+            type="primary"
+            loading={loading}
+            onClick={() => {
+              setShowSkillModal(false);
+              if (editorRef.current) {
+                const value = editorRef.current.getValue();
+                const updatedSkill = { ...localSkill, content: value };
+                setSkill(updatedSkill);
+                handler(updatedSkill);
+              }
+            }}
+          >
+            Save
+          </Button>,
+        ]}
       >
-        {selectedSkill && (
-          <div>
-            <div className="mb-2">{selectedSkill.title}</div>
+        {localSkill && (
+          <div style={{ minHeight: "70vh" }}>
+            <div className="mb-2">
+              <Input
+                placeholder="Skill Title"
+                value={localSkill.title}
+                onChange={(e) => {
+                  const updatedSkill = { ...localSkill, title: e.target.value };
+                  setLocalSkill(updatedSkill);
+                }}
+              />
+            </div>
+
+            {/* <div className="mb-2">
+              <div className="inline-block  "> Skill Description </div>
+              <TextArea
+                placeholder="Skill Description"
+                value={localSkill.description}
+                onChange={(e) => {
+                  const updatedSkill = {
+                    ...localSkill,
+                    description: e.target.value,
+                  };
+                  setLocalSkill(updatedSkill);
+                }}
+              />
+            </div> */}
 
-            <CodeBlock code={selectedSkill?.content} language="python" />
+            <div style={{ height: "70vh" }} className="h-full  mt-2 rounded">
+              <MonacoEditor
+                value={localSkill?.content}
+                language="python"
+                editorRef={editorRef}
+              />
+            </div>
           </div>
         )}
       </Modal>
+    );
+  };
 
-      <Modal
+  return (
+    <div className=" text-primary ">
+      {/* <Modal
         title={
           <div>
             <PlusIcon className="w-5 h-5 inline-block mr-1" /> Create New Skill
@@ -258,7 +325,27 @@ const SkillsView = ({}: any) => {
             rows={10}
           />
         </>
-      </Modal>
+      </Modal> */}
+
+      <SkillModal
+        skill={selectedSkill}
+        setSkill={setSelectedSkill}
+        showSkillModal={showSkillModal}
+        setShowSkillModal={setShowSkillModal}
+        handler={(skill: ISkill) => {
+          saveSkill(skill);
+        }}
+      />
+
+      <SkillModal
+        skill={newSkill}
+        setSkill={setNewSkill}
+        showSkillModal={showNewSkillModal}
+        setShowSkillModal={setShowNewSkillModal}
+        handler={(skill: ISkill) => {
+          saveSkill(skill);
+        }}
+      />
 
       <div className="mb-2   relative">
         <div className="">
diff --git a/samples/apps/autogen-studio/frontend/src/components/views/builder/workflow.tsx b/samples/apps/autogen-studio/frontend/src/components/views/builder/workflow.tsx
index 3282d02b86c8..fbb920691ee0 100644
--- a/samples/apps/autogen-studio/frontend/src/components/views/builder/workflow.tsx
+++ b/samples/apps/autogen-studio/frontend/src/components/views/builder/workflow.tsx
@@ -1,11 +1,14 @@
 import {
+  ArrowDownTrayIcon,
+  DocumentDuplicateIcon,
+  ExclamationTriangleIcon,
   InformationCircleIcon,
   PlusIcon,
   TrashIcon,
   UserGroupIcon,
   UsersIcon,
 } from "@heroicons/react/24/outline";
-import { Button, Dropdown, MenuProps, Modal, message } from "antd";
+import { Button, Dropdown, MenuProps, Modal, Tooltip, message } from "antd";
 import * as React from "react";
 import { IFlowConfig, IStatus } from "../../types";
 import { appContext } from "../../../hooks/provider";
@@ -13,6 +16,7 @@ import {
   fetchJSON,
   getServerUrl,
   sampleWorkflowConfig,
+  sanitizeConfig,
   timeAgo,
   truncateText,
 } from "../../utils";
@@ -177,22 +181,71 @@ const WorkflowView = ({}: any) => {
             >
               <div style={{ minHeight: "65px" }} className="break-words  my-2">
                 {" "}
-                {truncateText(workflow.name, 70)}
+                {truncateText(workflow.description, 70)}
               </div>
               <div className="text-xs">{timeAgo(workflow.timestamp || "")}</div>
-            </Card>
-            <div className="text-right  mt-2">
+
               <div
-                role="button"
-                className="text-accent text-xs inline-block"
-                onClick={() => {
-                  deleteWorkFlow(workflow);
+                onMouseEnter={(e) => {
+                  e.stopPropagation();
                 }}
+                className=" mt-2 text-right opacity-0 group-hover:opacity-100 "
               >
-                <TrashIcon className=" w-5, h-5 cursor-pointer inline-block" />
-                <span className="text-xs"> delete</span>
+                <div
+                  role="button"
+                  className="text-accent text-xs inline-block hover:bg-primary p-2 rounded"
+                  onClick={(e) => {
+                    e.stopPropagation();
+                    // download workflow as workflow.name.json
+                    const element = document.createElement("a");
+                    const sanitizedWorkflow = sanitizeConfig(workflow);
+                    const file = new Blob([JSON.stringify(sanitizedWorkflow)], {
+                      type: "application/json",
+                    });
+                    element.href = URL.createObjectURL(file);
+                    element.download = `${workflow.name}.json`;
+                    document.body.appendChild(element); // Required for this to work in FireFox
+                    element.click();
+
+                    // window.op
+                  }}
+                >
+                  <Tooltip title="Download">
+                    <ArrowDownTrayIcon className=" w-5, h-5 cursor-pointer inline-block" />
+                  </Tooltip>
+                </div>
+                <div
+                  role="button"
+                  className="text-accent text-xs inline-block hover:bg-primary p-2 rounded"
+                  onClick={(e) => {
+                    e.stopPropagation();
+                    let newWorkflow = { ...workflow };
+                    newWorkflow.name = `${workflow.name} Copy`;
+                    newWorkflow.user_id = user?.email;
+                    newWorkflow.timestamp = new Date().toISOString();
+                    delete newWorkflow.id;
+                    setNewWorkflow(newWorkflow);
+                    setShowNewWorkflowModal(true);
+                  }}
+                >
+                  <Tooltip title="Make a Copy">
+                    <DocumentDuplicateIcon className=" w-5, h-5 cursor-pointer inline-block" />
+                  </Tooltip>
+                </div>
+                <div
+                  role="button"
+                  className="text-accent text-xs inline-block hover:bg-primary p-2 rounded"
+                  onClick={(e) => {
+                    e.stopPropagation();
+                    deleteWorkFlow(workflow);
+                  }}
+                >
+                  <Tooltip title="Delete">
+                    <TrashIcon className=" w-5, h-5 cursor-pointer inline-block" />
+                  </Tooltip>
+                </div>
               </div>
-            </div>
+            </Card>
           </div>
         </div>
       );
diff --git a/samples/apps/autogen-studio/frontend/src/components/views/playground/chatbox.tsx b/samples/apps/autogen-studio/frontend/src/components/views/playground/chatbox.tsx
index 2164a57e8c18..3c96be640be2 100644
--- a/samples/apps/autogen-studio/frontend/src/components/views/playground/chatbox.tsx
+++ b/samples/apps/autogen-studio/frontend/src/components/views/playground/chatbox.tsx
@@ -52,7 +52,7 @@ const ChatBox = ({
   let pageHeight, chatMaxHeight;
   if (typeof window !== "undefined") {
     pageHeight = window.innerHeight;
-    chatMaxHeight = pageHeight - 350 + "px";
+    chatMaxHeight = pageHeight - 300 + "px";
   }
 
   const parseMessages = (messages: any) => {
@@ -131,37 +131,13 @@ const ChatBox = ({
       });
     }
 
-    // if (messages.length - 1 === i) {
-    //   // items.push({
-    //   //   type: "divider",
-    //   // });
-
-    //   items.push({
-    //     label: (
-    //       <div
-    //         onClick={() => {
-    //           console.log("deleting", message);
-    //           deleteMessage(message.msg_id);
-    //         }}
-    //       >
-    //         <TrashIcon
-    //           title={"Delete message"}
-    //           className="h-4 w-4 mr-1 inline-block"
-    //         />
-    //         Delete Message
-    //       </div>
-    //     ),
-    //     key: "deletemessage",
-    //   });
-    // }
-
     const menu = (
       <Dropdown menu={{ items }} trigger={["click"]} placement="bottomRight">
         <div
           role="button"
           className="float-right ml-2 duration-100 hover:bg-secondary font-semibold px-2 pb-1  rounded"
         >
-          <span className="block -mt-2  "> ...</span>
+          <span className="block -mt-2 text-primary  "> ...</span>
         </div>
       </Dropdown>
     );
@@ -173,20 +149,11 @@ const ChatBox = ({
       >
         {" "}
         <div className={`  ${isUser ? "" : " w-full"} inline-flex gap-2`}>
-          <div className="">
-            {" "}
-            {/* {isUser && <UserIcon className="inline-block h-6 " />}
-            {!isUser && (
-              <span className="inline-block  text-accent  bg-primary pb-2 ml-1">
-                <Icon icon="app" size={8} />
-              </span>
-            )} */}
-          </div>
+          <div className=""></div>
           <div className="font-semibold text-secondary text-sm w-16">{`${
             isUser ? "USER" : "AGENTS"
           }`}</div>
           <div
-            // style={{ minWidth: "70%" }}
             className={`inline-block group relative ${
               isUser ? "" : " w-full "
             } p-2 rounded  ${css}`}
@@ -358,7 +325,14 @@ const ChatBox = ({
   };
 
   return (
-    <div className="text-primary    relative  h-full rounded  ">
+    <div className="text-primary     relative  h-full rounded  ">
+      <div
+        style={{ zIndex: 100 }}
+        className=" absolute right-0  text-secondary -top-8 rounded p-2"
+      >
+        {" "}
+        <div className="text-xs"> {session?.flow_config.name}</div>
+      </div>
       <div
         style={{ zIndex: 100 }}
         className=" absolute right-0  text-secondary -top-8 rounded p-2"
@@ -368,7 +342,7 @@ const ChatBox = ({
       </div>
       <div
         ref={messageBoxInputRef}
-        className="flex h-full     flex-col rounded  scroll pr-2 overflow-auto  "
+        className="flex h-full  flex-col rounded  scroll pr-2 overflow-auto  "
         style={{ minHeight: "300px", maxHeight: chatMaxHeight }}
       >
         <div className="scroll-gradient h-10">
diff --git a/samples/apps/autogen-studio/frontend/src/components/views/playground/metadata.tsx b/samples/apps/autogen-studio/frontend/src/components/views/playground/metadata.tsx
index 39cdfe88a66d..2eb9a79ebeb7 100644
--- a/samples/apps/autogen-studio/frontend/src/components/views/playground/metadata.tsx
+++ b/samples/apps/autogen-studio/frontend/src/components/views/playground/metadata.tsx
@@ -7,6 +7,7 @@ import * as React from "react";
 import {
   CodeBlock,
   CodeLoader,
+  CsvLoader,
   CollapseBox,
   ExpandView,
   GroupView,
@@ -24,6 +25,7 @@ const MetaDataView = ({ metadata }: { metadata: any | null }) => {
     const file_type = file.extension;
     const is_image = ["image"].includes(file.type);
     const is_code = ["code"].includes(file.type);
+    const is_csv = ["csv"].includes(file.type);
     const is_pdf = ["pdf"].includes(file.type);
     const is_video = ["video"].includes(file.type);
     const file_name = file.name || "unknown";
@@ -64,7 +66,7 @@ const MetaDataView = ({ metadata }: { metadata: any | null }) => {
     } else if (is_video) {
       fileView = (
         <div className="mb-2">
-          {fileTitle}
+          <a href={`${serverUrl}/${file_path}`}>{fileTitle}</a>
           <video controls className="w-full rounded">
             <source
               src={`${serverUrl}/${file_path}`}
@@ -89,10 +91,41 @@ const MetaDataView = ({ metadata }: { metadata: any | null }) => {
           </div>
         </div>
       );
+    } else if (is_csv) {
+      fileView = (
+        <div className="h">
+          <a href={`${serverUrl}/${file_path}`}>
+            <div className="mb-4">{fileTitle}</div>
+          </a>
+          <CsvLoader
+            csvUrl={`${serverUrl}/${file_path}`}
+            className="w-full rounded"
+          />
+        </div>
+      );
+      icon = (
+        <div className="   relative rounded   h-full">
+          <div className="absolute rounded p-2 bg-secondary top-0 ">
+            {fileTitle}
+          </div>
+          <div
+            style={{ minHeight: "150px" }}
+            className="bg-secondary  h-full w-full rounded  flex items-center justify-center text-primary"
+          >
+            <Icon icon="csv" size={14} />
+          </div>
+        </div>
+      );
     } else if (is_code) {
       fileView = (
         <div className="h">
-          <div className="mb-4">{fileTitle}</div>
+          <a
+            href={`${serverUrl}/${file_path}`}
+            target="_blank"
+            rel="noopener noreferrer"
+          >
+            <div className="mb-4">{fileTitle}</div>
+          </a>
           <CodeLoader
             url={`${serverUrl}/${file_path}`}
             className="w-full rounded"
@@ -115,7 +148,15 @@ const MetaDataView = ({ metadata }: { metadata: any | null }) => {
     } else if (is_pdf) {
       fileView = (
         <div className="h-full">
-          <div className="mb-4">{fileTitle}</div>
+          <div className="mb-4">
+            <a
+              href={`${serverUrl}/${file_path}`}
+              target="_blank"
+              rel="noopener noreferrer"
+            >
+              {fileTitle}
+            </a>
+          </div>
           <PdfViewer url={`${serverUrl}/${file_path}`} />
         </div>
       );
@@ -156,7 +197,7 @@ const MetaDataView = ({ metadata }: { metadata: any | null }) => {
 
   const messages = (metadata.messages || []).map((message: any, i: number) => {
     return (
-      <div className="borpder-b mb-2 border-dashed" key={"messagerow" + i}>
+      <div className=" mb-2 border-dashed" key={"messagerow" + i}>
         <GroupView
           title={
             <div className="rounded p-1 px-2 inline-block text-xs bg-secondary">
@@ -178,7 +219,7 @@ const MetaDataView = ({ metadata }: { metadata: any | null }) => {
   return (
     <div>
       {hasMessages && (
-        <div className="rounded bg-primary p-2">
+        <div className="rounded   bg-primary  ">
           <CollapseBox
             open={false}
             title={`Agent Messages (${messages.length} message${
diff --git a/samples/apps/autogen-studio/frontend/src/components/views/playground/sessions.tsx b/samples/apps/autogen-studio/frontend/src/components/views/playground/sessions.tsx
index c30d58b19c14..4925d8f915f2 100644
--- a/samples/apps/autogen-studio/frontend/src/components/views/playground/sessions.tsx
+++ b/samples/apps/autogen-studio/frontend/src/components/views/playground/sessions.tsx
@@ -1,10 +1,11 @@
 import {
+  ChatBubbleLeftRightIcon,
   GlobeAltIcon,
   PlusIcon,
   Square3Stack3DIcon,
   TrashIcon,
 } from "@heroicons/react/24/outline";
-import { Button, Modal, message } from "antd";
+import { Button, Dropdown, MenuProps, Modal, message } from "antd";
 import * as React from "react";
 import { IChatSession, IStatus } from "../../types";
 import { appContext } from "../../../hooks/provider";
@@ -200,11 +201,72 @@ const SessionsView = ({}: any) => {
     const rowClass = isSelected
       ? "bg-accent text-white"
       : "bg-secondary text-primary";
+    let items: MenuProps["items"] = [
+      {
+        label: (
+          <div
+            onClick={() => {
+              console.log("deleting session");
+              deleteSession(data);
+            }}
+          >
+            <TrashIcon
+              role={"button"}
+              title={"Delete"}
+              className="h-4 w-4 mr-1 inline-block"
+            />
+            Delete
+          </div>
+        ),
+        key: "delete",
+      },
+      {
+        label: (
+          <div
+            onClick={() => {
+              console.log("publishing session");
+              publishSession();
+            }}
+          >
+            <GlobeAltIcon
+              role={"button"}
+              title={"Publish"}
+              className="h-4 w-4 mr-1 inline-block"
+            />
+            Publish
+          </div>
+        ),
+        key: "publish",
+      },
+    ];
+
+    items.push();
+    const menu = (
+      <Dropdown menu={{ items }} trigger={["click"]} placement="bottomRight">
+        <div
+          role="button"
+          className={`float-right ml-2 duration-100 hover:bg-secondary font-semibold px-2 pb-1  rounded ${
+            isSelected ? "hover:text-accent" : ""
+          }`}
+        >
+          <span className={`block -mt-2 ${isSelected ? "text-white" : ""}`}>
+            {" "}
+            ...
+          </span>
+        </div>
+      </Dropdown>
+    );
+
     return (
       <div
         key={"sessionsrow" + index}
-        className="  mb-2 pb-1  border-b border-dashed "
+        className="group relative  mb-2 pb-1  border-b border-dashed "
       >
+        {items.length > 0 && (
+          <div className="  absolute right-2 top-2 group-hover:opacity-100 opacity-0 ">
+            {menu}
+          </div>
+        )}
         <div
           className={`rounded p-2 cursor-pointer ${rowClass}`}
           role="button"
@@ -213,35 +275,13 @@ const SessionsView = ({}: any) => {
             setWorkflowConfig(data.flow_config);
           }}
         >
-          <div className="text-xs">{truncateText(data.id, 27)}</div>
-          <div className="text-xs">{data.flow_config.name}</div>
-          <div className="text-xs text-right ">{timeAgo(data.timestamp)} </div>
-        </div>
-        <div className="flex mt-2 text-secondary">
-          <div className="flex-1"></div>
-          <div
-            role="button"
-            onClick={() => {
-              deleteSession(data);
-            }}
-            className="text-xs px-2  hover:text-accent cursor-pointer"
-          >
-            <TrashIcon className="w-4 h-4 inline-block mr-1 " />
-            delete{" "}
-          </div>
-
-          <div
-            role="button"
-            onClick={() => {
-              publishSession();
-            }}
-            className="text-xs px-2  hover:text-accent cursor-pointer"
-          >
-            <GlobeAltIcon className="w-4 h-4 inline-block mr-1 " />
-            publish{" "}
+          <div className="text-xs">{truncateText(data.id, 20)}</div>
+          <div className="text-xs mt-1">
+            <Square3Stack3DIcon className="h-4 w-4 inline-block mr-1" />
+            {data.flow_config.name}
           </div>
+          <div className="text-xs text-right ">{timeAgo(data.timestamp)} </div>
         </div>
-        {/* <div className="border-b border-dashed mx-2 mt-1"></div> */}
       </div>
     );
   });
@@ -289,20 +329,20 @@ const SessionsView = ({}: any) => {
       <div className="mb-2 relative">
         <div className="">
           <div className="font-semibold mb-2 pb-1 border-b">
-            <Square3Stack3DIcon className="h-5 w-5 inline-block mr-1" />
+            <ChatBubbleLeftRightIcon className="h-5 w-5 inline-block mr-1" />
             Sessions{" "}
           </div>
           {sessions && sessions.length > 0 && (
-            <div className="text-xs mb-2 pb-1  ">
+            <div className="text-xs  hidden mb-2 pb-1  ">
               {" "}
               Create a new session or select an existing session to view chat.
             </div>
           )}
           <div
             style={{
-              maxHeight: "300px",
+              maxHeight: skillsMaxHeight,
             }}
-            className="mb-4 overflow-y-scroll scroll rounded relative "
+            className="mb-4 overflow-y-auto scroll rounded relative "
           >
             <LoadingOverlay loading={loading} />
             {sessionRows}
diff --git a/samples/apps/autogen-studio/frontend/src/styles/global.css b/samples/apps/autogen-studio/frontend/src/styles/global.css
index f2905feff3fa..209b4fac3027 100644
--- a/samples/apps/autogen-studio/frontend/src/styles/global.css
+++ b/samples/apps/autogen-studio/frontend/src/styles/global.css
@@ -2,6 +2,7 @@
   --color-bg-primary: #111827;
   --color-bg-secondary: #1e293b;
   --color-bg-light: #27354c;
+  --color-bg-tertiary: #374151;
   --color-bg-accent: #22c55e;
   --color-text-primary: #f7fafc;
   --color-text-secondary: #e2e8f0;
@@ -15,6 +16,7 @@
   --color-bg-primary: #ffffff;
   --color-bg-secondary: #edf2f7;
   --color-bg-light: #f9fafb;
+  --color-bg-tertiary: #ffffff;
   --color-bg-accent: #16a34a;
   --color-text-primary: #334155;
   --color-text-secondary: #64748b;
@@ -328,6 +330,10 @@ iiz__zoom-img {
   margin: 0px !important;
 }
 
+.monaco-editor {
+  @apply rounded !important;
+}
+
 /* div.chatbox > ul {
   list-style: disc !important;
 }
diff --git a/samples/apps/autogen-studio/frontend/tailwind.config.js b/samples/apps/autogen-studio/frontend/tailwind.config.js
index be32765d63d0..2e26f9a2506e 100644
--- a/samples/apps/autogen-studio/frontend/tailwind.config.js
+++ b/samples/apps/autogen-studio/frontend/tailwind.config.js
@@ -19,6 +19,7 @@ module.exports = {
         secondary: "var(--color-bg-secondary)",
         accent: "var(--color-bg-accent)",
         light: "var(--color-bg-light)",
+        tertiary: "var(--color-bg-tertiary)",
       },
       textColor: {
         accent: "var(--color-text-accent)",
diff --git a/samples/apps/autogen-studio/notebooks/agent_spec.json b/samples/apps/autogen-studio/notebooks/agent_spec.json
index bcd795532c2f..72d1e21ef1a7 100644
--- a/samples/apps/autogen-studio/notebooks/agent_spec.json
+++ b/samples/apps/autogen-studio/notebooks/agent_spec.json
@@ -23,13 +23,6 @@
         "config_list": [
           {
             "model": "gpt-4-1106-preview"
-          },
-          {
-            "model": "gpt-3.5-turbo-16k"
-          },
-          {
-            "model": "TheBloke/zephyr-7B-alpha-AWQ",
-            "base_url": "http://localhost:8000/v1"
           }
         ],
         "temperature": 0.1,
@@ -41,5 +34,5 @@
       "system_message": "You are a helpful assistant that can use available functions when needed to solve problems. At each point, do your best to determine if the user's request has been addressed. IF THE REQUEST HAS NOT BEEN ADDRESSED, RESPOND WITH CODE TO ADDRESS IT. IF A FAILURE OCCURRED (e.g., due to a missing library) AND SOME ADDITIONAL CODE WAS WRITTEN (e.g. code to install the library), ENSURE THAT THE ORIGINAL CODE TO ADDRESS THE TASK STILL GETS EXECUTED. If the request HAS been addressed, respond with a summary of the result. The summary must be written as a coherent helpful response to the user request e.g. 'Sure, here is result to your request ' or 'The tallest mountain in Africa is ..' etc. The summary MUST end with the word TERMINATE. If the  user request is pleasantry or greeting, you should respond with a pleasantry or greeting and TERMINATE."
     }
   },
-  "type": "default"
+  "type": "twoagents"
 }
diff --git a/samples/apps/autogen-studio/notebooks/groupchat_spec.json b/samples/apps/autogen-studio/notebooks/groupchat_spec.json
index 8aece5c5051a..21cced7135b7 100644
--- a/samples/apps/autogen-studio/notebooks/groupchat_spec.json
+++ b/samples/apps/autogen-studio/notebooks/groupchat_spec.json
@@ -56,7 +56,7 @@
             },
             "human_input_mode": "NEVER",
             "max_consecutive_auto_reply": 8,
-            "system_message": "You are a helpful assistant that can suggest a travel itinerary for a user. You are the primary coordinator who will receive suggestions or advice from other agents (local_assistant, language_assistant). You must ensure that the finally plan integrates the suggestions from other agents or team members. YOUR FINAL RESPONSE MUST BE THE COMPLETE PLAN that ends with the word TERMINATE. "
+            "system_message": "You are a helpful assistant that can suggest a travel itinerary for a user. You are the primary cordinator who will receive suggestions or advice from other agents (local_assistant, language_assistant). You must ensure that the finally plan integrates the suggestions from other agents or team members. YOUR FINAL RESPONSE MUST BE THE COMPLETE PLAN that ends with the word TERMINATE. "
           }
         },
         {
diff --git a/samples/apps/autogen-studio/notebooks/tutorial.ipynb b/samples/apps/autogen-studio/notebooks/tutorial.ipynb
index 262a13b88bff..2830585938f1 100644
--- a/samples/apps/autogen-studio/notebooks/tutorial.ipynb
+++ b/samples/apps/autogen-studio/notebooks/tutorial.ipynb
@@ -1,328 +1,115 @@
 {
- "cells": [
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "import json\n",
-    "\n",
-    "from autogenstudio import AgentWorkFlowConfig, AutoGenWorkFlowManager"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## AutoGen Studio Agent Workflow API Example\n",
-    "\n",
-    "This notebook focuses on demonstrating capabilities of the autogen studio workflow python api.  \n",
-    "\n",
-    "- Declarative Specification of an Agent Workflow \n",
-    "- Loading the specification and running the resulting agent\n",
-    "\n",
-    "\n",
-    "> Note: The notebook currently demonstrates support for a two agent setup. Support for GroupChat is currently in development."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 1,
-   "metadata": {},
-   "outputs": [
+  "cells": [
     {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "userproxy (to primary_assistant):\n",
-      "\n",
-      "What is the height of the Eiffel Tower?. Dont write code, just respond to the question.\n",
-      "\n",
-      "--------------------------------------------------------------------------------\n",
-      "primary_assistant (to userproxy):\n",
-      "\n",
-      "The Eiffel Tower is approximately 300 meters tall, not including antennas. With antennas, it reaches a height of about 330 meters. TERMINATE\n",
-      "\n",
-      "--------------------------------------------------------------------------------\n"
-     ]
-    }
-   ],
-   "source": [
-    "# load an agent specification in JSON\n",
-    "agent_spec = json.load(open(\"agent_spec.json\"))\n",
-    "\n",
-    "# Creat a An AutoGen Workflow Configuration from the agent specification\n",
-    "agent_work_flow_config = AgentWorkFlowConfig(**agent_spec)\n",
-    "\n",
-    "# # Create a Workflow from the configuration\n",
-    "agent_work_flow = AutoGenWorkFlowManager(agent_work_flow_config)\n",
-    "\n",
-    "# # Run the workflow on a task\n",
-    "task_query = \"What is the height of the Eiffel Tower?. Dont write code, just respond to the question.\"\n",
-    "agent_work_flow.run(message=task_query)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 2,
-   "metadata": {},
-   "outputs": [
+      "cell_type": "code",
+      "execution_count": null,
+      "metadata": {},
+      "outputs": [],
+      "source": [
+        "import json\n",
+        "from autogenstudio import AgentWorkFlowConfig, AutoGenWorkFlowManager"
+      ]
+    },
     {
-     "data": {
-      "text/plain": [
-       "[{'recipient': 'primary_assistant',\n",
-       "  'sender': 'userproxy',\n",
-       "  'message': {'content': 'What is the height of the Eiffel Tower?. Dont write code, just respond to the question.',\n",
-       "   'role': 'user'},\n",
-       "  'timestamp': '2024-01-02T21:38:35.131849'},\n",
-       " {'recipient': 'userproxy',\n",
-       "  'sender': 'primary_assistant',\n",
-       "  'message': {'content': 'The Eiffel Tower is approximately 300 meters tall, not including antennas. With antennas, it reaches a height of about 330 meters. TERMINATE',\n",
-       "   'role': 'user'},\n",
-       "  'timestamp': '2024-01-02T21:38:35.136795'}]"
+      "cell_type": "markdown",
+      "metadata": {},
+      "source": [
+        "## AutoGen Studio Agent Workflow API Example\n",
+        "\n",
+        "This notebook focuses on demonstrating capabilities of the autogen studio workflow python api.  \n",
+        "\n",
+        "- Declarative Specification of an Agent Workflow \n",
+        "- Loading the specification and running the resulting agent\n",
+        "\n",
+        "\n",
+        "> Note: The notebook currently demonstrates support for a two agent setup. Support for GroupChat is currently in development."
       ]
-     },
-     "execution_count": 2,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "agent_work_flow.agent_history"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## Group Chat Agent"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 5,
-   "metadata": {},
-   "outputs": [
+    },
     {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "userproxy (to group_chat_manager):\n",
-      "\n",
-      "plan a two day trip to Maui hawaii\n",
-      "\n",
-      "--------------------------------------------------------------------------------\n",
-      "local_assistant (to group_chat_manager):\n",
-      "\n",
-      "To plan a two-day trip to Maui, Hawaii, we'll need to consider a variety of activities that allow you to experience the island's natural beauty, culture, and cuisine. Here's a suggested itinerary:\n",
-      "\n",
-      "**Day 1: Exploring the Natural Beauty of Maui**\n",
-      "\n",
-      "- **Morning:**\n",
-      "  - Start your day with a sunrise visit to Haleakalā National Park. Witnessing the sunrise from the summit of Haleakalā is a breathtaking experience. Remember to make a reservation in advance as it's required for sunrise viewing.\n",
-      "  - After the sunrise, take a short hike in the park to explore the unique volcanic landscapes.\n",
-      "\n",
-      "- **Afternoon:**\n",
-      "  - Head to the town of Paia for lunch. Paia is known for its laid-back atmosphere and has several local restaurants where you can try Hawaiian specialties.\n",
-      "  - Visit Ho'okipa Beach Park to watch windsurfers and surfers, or if you're adventurous, take a surf lesson.\n",
-      "\n",
-      "- **Evening:**\n",
-      "  - Drive to Lahaina, a historic whaling village, and stroll along Front Street. Here you can shop for local crafts and art.\n",
-      "  - Have dinner at a traditional Hawaiian luau, where you can enjoy local food, music, and dance.\n",
-      "\n",
-      "**Day 2: Cultural and Relaxation Experience**\n",
-      "\n",
-      "- **Morning:**\n",
-      "  - Visit Iao Valley State Park to see the Iao Needle and learn about the historical significance of this area to Hawaiian culture.\n",
-      "  - Explore the Maui Tropical Plantation and take a tram tour to learn about the island's agriculture and taste some fresh tropical fruits.\n",
-      "\n",
-      "- **Afternoon:**\n",
-      "  - Relax at one of Maui's beautiful beaches, such as Ka'anapali Beach or Wailea Beach. Take the opportunity to snorkel and see the vibrant marine life.\n",
-      "  - Consider a visit to a local farm or a coffee plantation to learn more about the local produce.\n",
-      "\n",
-      "- **Evening:**\n",
-      "  - Enjoy a sunset dinner cruise off the coast of Maui for a memorable end to your trip. This is a great way to see the island from a different perspective and often includes entertainment.\n",
-      "\n",
-      "Remember to book accommodations in advance, rent a car for easier travel around the island, and check for any travel restrictions or requirements. Also, be respectful of the local environment and culture, and try to participate in activities that support the local economy and conservation efforts.\n",
-      "\n",
-      "This plan includes a mix of natural, cultural, and relaxation activities that should give you a well-rounded experience of Maui in two days. If you have specific interests or preferences, the itinerary can be adjusted accordingly.\n",
-      "\n",
-      "--------------------------------------------------------------------------------\n",
-      "language_assistant (to group_chat_manager):\n",
-      "\n",
-      "Regarding language or communication challenges, Hawaii is a part of the United States, so the primary language spoken is English. However, the Hawaiian language is also an official language of the state, and you will likely encounter Hawaiian words and place names during your visit.\n",
-      "\n",
-      "Here are some tips to address potential language or communication challenges in Maui:\n",
-      "\n",
-      "1. **Learn Basic Hawaiian Phrases**: While not necessary, learning a few basic Hawaiian phrases can be a sign of respect for the local culture. For example:\n",
-      "   - Aloha - Hello/Goodbye\n",
-      "   - Mahalo - Thank you\n",
-      "   - 'Aina - Land\n",
-      "   - Kai - Sea\n",
-      "\n",
-      "2. **Use Translation Apps**: If you come across Hawaiian words or phrases that you don't understand, you can use translation apps on your smartphone to help you understand their meanings.\n",
-      "\n",
-      "3. **Cultural Understanding**: Take some time to learn about Hawaiian culture and customs before your trip. This will help you communicate more effectively and respectfully with locals.\n",
-      "\n",
-      "4. **Ask for Help**: If you're unsure about pronunciation or meaning, don't hesitate to ask locals. People in Hawaii are generally friendly and willing to help.\n",
-      "\n",
-      "5. **Be Patient and Respectful**: Always be patient and respectful in your interactions. Remember that you are a guest in Hawaii, and showing respect for the local culture and language is important.\n",
-      "\n",
-      "Your travel plan should include these language tips to ensure smooth communication during your trip. If your plan already includes these tips, then it is satisfactory. If not, consider incorporating them to enhance your travel experience.\n",
-      "\n",
-      "--------------------------------------------------------------------------------\n",
-      "primary_assistant (to group_chat_manager):\n",
-      "\n",
-      "Based on the suggestions from the local and language assistants, here is the complete two-day travel itinerary for Maui, Hawaii:\n",
-      "\n",
-      "**Day 1: Exploring the Natural Beauty of Maui**\n",
-      "\n",
-      "- **Morning:**\n",
-      "  - Experience the sunrise at Haleakalā National Park (make a reservation in advance).\n",
-      "  - Enjoy a short hike in the park to see the volcanic landscapes.\n",
-      "\n",
-      "- **Afternoon:**\n",
-      "  - Have lunch in Paia and enjoy Hawaiian specialties.\n",
-      "  - Visit Ho'okipa Beach Park for surfing or to watch the surfers.\n",
-      "\n",
-      "- **Evening:**\n",
-      "  - Explore Lahaina and shop along Front Street.\n",
-      "  - Attend a traditional Hawaiian luau for dinner with music and dance.\n",
-      "\n",
-      "**Day 2: Cultural and Relaxation Experience**\n",
-      "\n",
-      "- **Morning:**\n",
-      "  - Visit Iao Valley State Park to see the Iao Needle.\n",
-      "  - Take a tram tour at Maui Tropical Plantation and taste fresh tropical fruits.\n",
-      "\n",
-      "- **Afternoon:**\n",
-      "  - Relax at Ka'anapali Beach or Wailea Beach and go snorkeling.\n",
-      "  - Optionally, visit a local farm or coffee plantation.\n",
-      "\n",
-      "- **Evening:**\n",
-      "  - Enjoy a sunset dinner cruise with entertainment.\n",
-      "\n",
-      "**Language and Communication Tips:**\n",
-      "\n",
-      "- Learn basic Hawaiian phrases such as \"Aloha\" (Hello/Goodbye) and \"Mahalo\" (Thank you).\n",
-      "- Use translation apps for any unfamiliar Hawaiian words or phrases.\n",
-      "- Educate yourself about Hawaiian culture and customs to communicate respectfully.\n",
-      "- Ask locals for help if you're unsure about pronunciation or meaning.\n",
-      "- Be patient and respectful in all interactions, honoring local culture and language.\n",
-      "\n",
-      "**Additional Travel Tips:**\n",
-      "\n",
-      "- Book accommodations and a car rental in advance.\n",
-      "- Check for any travel restrictions or requirements.\n",
-      "- Support local economy and conservation efforts.\n",
-      "- Be mindful of the environment and cultural sites.\n",
-      "\n",
-      "This itinerary provides a balanced mix of activities that showcase Maui's natural beauty, culture, and opportunities for relaxation. Remember to be respectful of the local environment and culture, and enjoy your two-day adventure in Maui, Hawaii.\n",
-      "\n",
-      "TERMINATE\n",
-      "\n",
-      "--------------------------------------------------------------------------------\n"
-     ]
-    }
-   ],
-   "source": [
-    "# load an agent specification in JSON\n",
-    "agent_spec = json.load(open(\"groupchat_spec.json\"))\n",
-    "\n",
-    "# Creat a An AutoGen Workflow Configuration from the agent specification\n",
-    "agent_work_flow_config = AgentWorkFlowConfig(**agent_spec)\n",
-    "\n",
-    "# Create a Workflow from the configuration\n",
-    "group_agent_work_flow = AutoGenWorkFlowManager(agent_work_flow_config)\n",
-    "\n",
-    "# Run the workflow on a task\n",
-    "task_query = \"plan a two day trip to Maui hawaii\"\n",
-    "group_agent_work_flow.run(message=task_query)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 6,
-   "metadata": {},
-   "outputs": [
+      "cell_type": "code",
+      "execution_count": null,
+      "metadata": {},
+      "outputs": [],
+      "source": [
+        "# load an agent specification in JSON\n",
+        "agent_spec = json.load(open(\"agent_spec.json\"))\n",
+        "\n",
+        "# Creat a An AutoGen Workflow Configuration from the agent specification\n",
+        "agent_work_flow_config = AgentWorkFlowConfig(**agent_spec)\n",
+        "\n",
+        "agent_work_flow = AutoGenWorkFlowManager(agent_work_flow_config)\n",
+        "\n",
+        "# # Run the workflow on a task\n",
+        "task_query = \"What is the height of the Eiffel Tower?. Dont write code, just respond to the question.\"\n",
+        "agent_work_flow.run(message=task_query)"
+      ]
+    },
+    {
+      "cell_type": "code",
+      "execution_count": null,
+      "metadata": {},
+      "outputs": [],
+      "source": [
+        "agent_work_flow.agent_history"
+      ]
+    },
+    {
+      "cell_type": "markdown",
+      "metadata": {},
+      "source": [
+        "## Group Chat Agent"
+      ]
+    },
     {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "4 agent messages were involved in the conversation\n"
-     ]
+      "cell_type": "code",
+      "execution_count": null,
+      "metadata": {},
+      "outputs": [],
+      "source": [
+        "# load an agent specification in JSON\n",
+        "agent_spec = json.load(open(\"groupchat_spec.json\"))\n",
+        "\n",
+        "# Creat a An AutoGen Workflow Configuration from the agent specification\n",
+        "agent_work_flow_config = AgentWorkFlowConfig(**agent_spec)\n",
+        "\n",
+        "# Create a Workflow from the configuration\n",
+        "group_agent_work_flow = AutoGenWorkFlowManager(agent_work_flow_config)\n",
+        "\n",
+        "# Run the workflow on a task\n",
+        "task_query = \"plan a two day trip to Maui hawaii\"\n",
+        "group_agent_work_flow.run(message=task_query)"
+      ]
     },
     {
-     "data": {
-      "text/plain": [
-       "[{'recipient': 'local_assistant',\n",
-       "  'sender': 'group_chat_manager',\n",
-       "  'message': {'content': 'plan a two day trip to Maui hawaii',\n",
-       "   'name': 'userproxy',\n",
-       "   'role': 'user'},\n",
-       "  'timestamp': '2024-01-02T21:38:42.433093'},\n",
-       " {'recipient': 'language_assistant',\n",
-       "  'sender': 'group_chat_manager',\n",
-       "  'message': {'content': \"To plan a two-day trip to Maui, Hawaii, we'll need to consider a variety of activities that allow you to experience the island's natural beauty, culture, and cuisine. Here's a suggested itinerary:\\n\\n**Day 1: Exploring the Natural Beauty of Maui**\\n\\n- **Morning:**\\n  - Start your day with a sunrise visit to Haleakalā National Park. Witnessing the sunrise from the summit of Haleakalā is a breathtaking experience. Remember to make a reservation in advance as it's required for sunrise viewing.\\n  - After the sunrise, take a short hike in the park to explore the unique volcanic landscapes.\\n\\n- **Afternoon:**\\n  - Head to the town of Paia for lunch. Paia is known for its laid-back atmosphere and has several local restaurants where you can try Hawaiian specialties.\\n  - Visit Ho'okipa Beach Park to watch windsurfers and surfers, or if you're adventurous, take a surf lesson.\\n\\n- **Evening:**\\n  - Drive to Lahaina, a historic whaling village, and stroll along Front Street. Here you can shop for local crafts and art.\\n  - Have dinner at a traditional Hawaiian luau, where you can enjoy local food, music, and dance.\\n\\n**Day 2: Cultural and Relaxation Experience**\\n\\n- **Morning:**\\n  - Visit Iao Valley State Park to see the Iao Needle and learn about the historical significance of this area to Hawaiian culture.\\n  - Explore the Maui Tropical Plantation and take a tram tour to learn about the island's agriculture and taste some fresh tropical fruits.\\n\\n- **Afternoon:**\\n  - Relax at one of Maui's beautiful beaches, such as Ka'anapali Beach or Wailea Beach. Take the opportunity to snorkel and see the vibrant marine life.\\n  - Consider a visit to a local farm or a coffee plantation to learn more about the local produce.\\n\\n- **Evening:**\\n  - Enjoy a sunset dinner cruise off the coast of Maui for a memorable end to your trip. This is a great way to see the island from a different perspective and often includes entertainment.\\n\\nRemember to book accommodations in advance, rent a car for easier travel around the island, and check for any travel restrictions or requirements. Also, be respectful of the local environment and culture, and try to participate in activities that support the local economy and conservation efforts.\\n\\nThis plan includes a mix of natural, cultural, and relaxation activities that should give you a well-rounded experience of Maui in two days. If you have specific interests or preferences, the itinerary can be adjusted accordingly.\",\n",
-       "   'name': 'local_assistant',\n",
-       "   'role': 'user'},\n",
-       "  'timestamp': '2024-01-02T21:39:33.989518'},\n",
-       " {'recipient': 'primary_assistant',\n",
-       "  'sender': 'group_chat_manager',\n",
-       "  'message': {'content': \"Regarding language or communication challenges, Hawaii is a part of the United States, so the primary language spoken is English. However, the Hawaiian language is also an official language of the state, and you will likely encounter Hawaiian words and place names during your visit.\\n\\nHere are some tips to address potential language or communication challenges in Maui:\\n\\n1. **Learn Basic Hawaiian Phrases**: While not necessary, learning a few basic Hawaiian phrases can be a sign of respect for the local culture. For example:\\n   - Aloha - Hello/Goodbye\\n   - Mahalo - Thank you\\n   - 'Aina - Land\\n   - Kai - Sea\\n\\n2. **Use Translation Apps**: If you come across Hawaiian words or phrases that you don't understand, you can use translation apps on your smartphone to help you understand their meanings.\\n\\n3. **Cultural Understanding**: Take some time to learn about Hawaiian culture and customs before your trip. This will help you communicate more effectively and respectfully with locals.\\n\\n4. **Ask for Help**: If you're unsure about pronunciation or meaning, don't hesitate to ask locals. People in Hawaii are generally friendly and willing to help.\\n\\n5. **Be Patient and Respectful**: Always be patient and respectful in your interactions. Remember that you are a guest in Hawaii, and showing respect for the local culture and language is important.\\n\\nYour travel plan should include these language tips to ensure smooth communication during your trip. If your plan already includes these tips, then it is satisfactory. If not, consider incorporating them to enhance your travel experience.\",\n",
-       "   'name': 'language_assistant',\n",
-       "   'role': 'user'},\n",
-       "  'timestamp': '2024-01-02T21:39:58.874159'},\n",
-       " {'recipient': 'primary_assistant',\n",
-       "  'sender': 'group_chat_manager',\n",
-       "  'message': {'content': 'Based on the suggestions from the local and language assistants, here is the complete two-day travel itinerary for Maui, Hawaii:\\n\\n**Day 1: Exploring the Natural Beauty of Maui**\\n\\n- **Morning:**\\n  - Experience the sunrise at Haleakalā National Park (make a reservation in advance).\\n  - Enjoy a short hike in the park to see the volcanic landscapes.\\n\\n- **Afternoon:**\\n  - Have lunch in Paia and enjoy Hawaiian specialties.\\n  - Visit Ho\\'okipa Beach Park for surfing or to watch the surfers.\\n\\n- **Evening:**\\n  - Explore Lahaina and shop along Front Street.\\n  - Attend a traditional Hawaiian luau for dinner with music and dance.\\n\\n**Day 2: Cultural and Relaxation Experience**\\n\\n- **Morning:**\\n  - Visit Iao Valley State Park to see the Iao Needle.\\n  - Take a tram tour at Maui Tropical Plantation and taste fresh tropical fruits.\\n\\n- **Afternoon:**\\n  - Relax at Ka\\'anapali Beach or Wailea Beach and go snorkeling.\\n  - Optionally, visit a local farm or coffee plantation.\\n\\n- **Evening:**\\n  - Enjoy a sunset dinner cruise with entertainment.\\n\\n**Language and Communication Tips:**\\n\\n- Learn basic Hawaiian phrases such as \"Aloha\" (Hello/Goodbye) and \"Mahalo\" (Thank you).\\n- Use translation apps for any unfamiliar Hawaiian words or phrases.\\n- Educate yourself about Hawaiian culture and customs to communicate respectfully.\\n- Ask locals for help if you\\'re unsure about pronunciation or meaning.\\n- Be patient and respectful in all interactions, honoring local culture and language.\\n\\n**Additional Travel Tips:**\\n\\n- Book accommodations and a car rental in advance.\\n- Check for any travel restrictions or requirements.\\n- Support local economy and conservation efforts.\\n- Be mindful of the environment and cultural sites.\\n\\nThis itinerary provides a balanced mix of activities that showcase Maui\\'s natural beauty, culture, and opportunities for relaxation. Remember to be respectful of the local environment and culture, and enjoy your two-day adventure in Maui, Hawaii.\\n\\nTERMINATE',\n",
-       "   'role': 'assistant'},\n",
-       "  'timestamp': '2024-01-02T21:40:30.240153'}]"
+      "cell_type": "code",
+      "execution_count": null,
+      "metadata": {},
+      "outputs": [],
+      "source": [
+        "print(len(group_agent_work_flow.agent_history), \"agent messages were involved in the conversation\")\n",
+        "group_agent_work_flow.agent_history"
       ]
-     },
-     "execution_count": 6,
-     "metadata": {},
-     "output_type": "execute_result"
     }
-   ],
-   "source": [
-    "print(len(group_agent_work_flow.agent_history), \"agent messages were involved in the conversation\")\n",
-    "group_agent_work_flow.agent_history"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": []
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": []
-  }
- ],
- "metadata": {
-  "kernelspec": {
-   "display_name": "coral",
-   "language": "python",
-   "name": "python3"
+  ],
+  "metadata": {
+    "kernelspec": {
+      "display_name": "coral",
+      "language": "python",
+      "name": "python3"
+    },
+    "language_info": {
+      "codemirror_mode": {
+        "name": "ipython",
+        "version": 3
+      },
+      "file_extension": ".py",
+      "mimetype": "text/x-python",
+      "name": "python",
+      "nbconvert_exporter": "python",
+      "pygments_lexer": "ipython3",
+      "version": "3.10.13"
+    }
   },
-  "language_info": {
-   "codemirror_mode": {
-    "name": "ipython",
-    "version": 3
-   },
-   "file_extension": ".py",
-   "mimetype": "text/x-python",
-   "name": "python",
-   "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.11.4"
-  }
- },
- "nbformat": 4,
- "nbformat_minor": 2
+  "nbformat": 4,
+  "nbformat_minor": 2
 }
diff --git a/website/blog/2023-12-01-AutoGenStudio/index.mdx b/website/blog/2023-12-01-AutoGenStudio/index.mdx
index e79a5b1f49d1..8692d43af4d5 100644
--- a/website/blog/2023-12-01-AutoGenStudio/index.mdx
+++ b/website/blog/2023-12-01-AutoGenStudio/index.mdx
@@ -202,7 +202,7 @@ We welcome contributions to AutoGen Studio. We recommend the following general s
 - Please initiate a discussion on the roadmap issue or a new issue to discuss your proposed contribution.
 - Please review the autogenstudio dev branch here [dev branch].(https://github.com/microsoft/autogen/tree/autogenstudio) and use as a base for your contribution. This way, your contribution will be aligned with the latest changes in the AutoGen Studio project.
 - Submit a pull request with your contribution!
-- If you are modifying AutoGen Studio in vscode, it has its own devcontainer. See instructions in `.devcontainer/README.md` to use it.
+- If you are modifying AutoGen Studio in vscode, it has its own devcontainer to simplify dev work. See instructions in `.devcontainer/README.md` on how to use it.
 - Please use the tag `studio` for any issues, questions, and PRs related to Studio.
 
 
@@ -224,5 +224,13 @@ A: We are constantly working to improve AutoGen Studio. For the latest updates,
 Yes. AutoGen standardizes on the openai model api format, and you can use any api server that offers an openai compliant endpoint. In the AutoGen Studio UI, each agent has an `llm_config` field where you can input your model endpoint details including `model name`, `api key`, `base url`, `model type` and `api version`. For Azure OpenAI models, you can find these details in the Azure portal. Note that for Azure OpenAI, the `model name` is the deployment id or engine, and the `model type` is "azure".
 For other OSS models, we recommend using a server such as vllm to instantiate an openai compliant endpoint.
 
+**Q: The Server Starts But I Can't Access the UI**
+A: If you are running the server on a remote machine (or a local machine that fails to resolve localhost correstly), you may need to specify the host address. By default, the host address is set to `localhost`.  You can specify the host address using the `--host <host>` argument. For example, to start the server on port 8081 and local address such that it is accessible from other machines on the network, you can run the following command:
+
+```bash
+autogenstudio ui --port 8081 --host 0.0.0.0
+```
+
+
 
 <br />


Code review comments:
diff_hunk: @@ -684,31 +688,71 @@ export const ModelSelector = ({
     models.length > 0
       ? models.map((model: IModelConfig, index: number) => ({
           key: index,
-          label: model.model,
+          label: (
+            <>
+              <div>{model.model}</div>
+              <div className="text-xs text-accent">
+                {truncateText(model.description || "", 20)}
+              </div>
+            </>
+          ),
           value: index,
         }))
       : [
           {
             key: -1,
-            label: "No models found",
+            label: <>No models found</>,
             value: 0,
           },
         ];
 
   const modelOnClick: MenuProps["onClick"] = ({ key }) => {
     const selectedIndex = parseInt(key.toString());
     let selectedModel = models[selectedIndex];
-    selectedModel = sanitizeModelConfig(selectedModel);
+    selectedModel = selectedModel;
path: samples/apps/autogen-studio/frontend/src/components/atoms.tsx
text: @user1:
nit: remove

diff_hunk: @@ -202,7 +184,7 @@
                 },
                 "human_input_mode": "NEVER",
                 "max_consecutive_auto_reply": 8,
-                "system_message": "You are a helpful assistant that can suggest a travel itinerary for a user. You are the primary coordinator who will receive suggestions or advice from other agents (local_assistant, language_assistant). You must ensure that the finally plan integrates the suggestions from other agents or team members. YOUR FINAL RESPONSE MUST BE THE COMPLETE PLAN that ends with the word TERMINATE. "
+                "system_message": "You are a helpful assistant that can suggest a travel itinerary for a user. You are the primary cordinator who will receive suggestions or advice from other agents (local_assistant, language_assistant). You must ensure that the finally plan integrates the suggestions from other agents or team members. YOUR FINAL RESPONSE MUST BE THE COMPLETE PLAN that ends with the word TERMINATE. "
path: samples/apps/autogen-studio/autogenstudio/utils/dbdefaults.json
text: @user1:
coordinator