From b7366b570fac189c66d9642e65f431cd43632239 Mon Sep 17 00:00:00 2001 From: Li Jiang Date: Sun, 21 Apr 2024 02:32:57 +0800 Subject: [PATCH 01/30] Update RetrieveChat extra dependencies (#2449) --- setup.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/setup.py b/setup.py index cea36b7052d..674df273883 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,8 @@ "ipykernel>=6.29.0", ] +rag = ["sentence_transformers", "pypdf", "ipython", "beautifulsoup4", "markdownify"] + setuptools.setup( name="pyautogen", version=__version__, @@ -59,24 +61,9 @@ ], "blendsearch": ["flaml[blendsearch]"], "mathchat": ["sympy", "pydantic==1.10.9", "wolframalpha"], - "retrievechat": ["chromadb", "sentence_transformers", "pypdf", "ipython", "beautifulsoup4", "markdownify"], - "retrievechat-pgvector": [ - "pgvector>=0.2.5", - "psycopg>=3.1.18", - "sentence_transformers", - "pypdf", - "ipython", - "beautifulsoup4", - "markdownify", - ], - "retrievechat-qdrant": [ - "qdrant_client[fastembed]", - "sentence_transformers", - "pypdf", - "ipython", - "beautifulsoup4", - "markdownify", - ], + "retrievechat": ["chromadb"] + rag, + "retrievechat-pgvector": ["pgvector>=0.2.5", "psycopg>=3.1.18"] + rag, + "retrievechat-qdrant": ["qdrant_client[fastembed]"] + rag, "autobuild": ["chromadb", "sentence-transformers", "huggingface-hub"], "teachable": ["chromadb"], "lmm": ["replicate", "pillow"], From 4e13f22cbf85e5ad43c81287729adc2aec925f6c Mon Sep 17 00:00:00 2001 From: luxuncang <2635886314@qq.com> Date: Tue, 23 Apr 2024 05:43:09 +0800 Subject: [PATCH 02/30] Make the port number optional in JupyterConnectionInfo() (#2473) * fix: JupyterConnectionInfo port type * fix: jupyter_client base_url * fix: JupyterConnectionInfo --- autogen/coding/jupyter/base.py | 6 +++--- autogen/coding/jupyter/jupyter_client.py | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/autogen/coding/jupyter/base.py b/autogen/coding/jupyter/base.py index d896b6ac3cc..0e7acaf1e87 100644 --- a/autogen/coding/jupyter/base.py +++ b/autogen/coding/jupyter/base.py @@ -10,9 +10,9 @@ class JupyterConnectionInfo: """`str` - Host of the Jupyter gateway server""" use_https: bool """`bool` - Whether to use HTTPS""" - port: int - """`int` - Port of the Jupyter gateway server""" - token: Optional[str] + port: Optional[int] = None + """`Optional[int]` - Port of the Jupyter gateway server. If None, the default port is used""" + token: Optional[str] = None """`Optional[str]` - Token for authentication. If None, no token is used""" diff --git a/autogen/coding/jupyter/jupyter_client.py b/autogen/coding/jupyter/jupyter_client.py index 44aafd8f5b0..b3de374fce9 100644 --- a/autogen/coding/jupyter/jupyter_client.py +++ b/autogen/coding/jupyter/jupyter_client.py @@ -41,10 +41,12 @@ def _get_headers(self) -> Dict[str, str]: def _get_api_base_url(self) -> str: protocol = "https" if self._connection_info.use_https else "http" - return f"{protocol}://{self._connection_info.host}:{self._connection_info.port}" + port = f":{self._connection_info.port}" if self._connection_info.port else "" + return f"{protocol}://{self._connection_info.host}{port}" def _get_ws_base_url(self) -> str: - return f"ws://{self._connection_info.host}:{self._connection_info.port}" + port = f":{self._connection_info.port}" if self._connection_info.port else "" + return f"ws://{self._connection_info.host}{port}" def list_kernel_specs(self) -> Dict[str, Dict[str, str]]: response = self._session.get(f"{self._get_api_base_url()}/api/kernelspecs", headers=self._get_headers()) From 7f1a810630435029a9c6ac54f1fa9215a6ca9b07 Mon Sep 17 00:00:00 2001 From: Wael Karkoub Date: Mon, 22 Apr 2024 22:44:43 +0100 Subject: [PATCH 03/30] Adds Message History to a `ConversableAgent` (#2437) * updates docstr + fix spelling * fix docstr * maybe a lfs fix? * Revert "maybe a lfs fix?" This reverts commit 2ea2dade26d4b34abbe62da413eb9a0f65ddac5b. * revert * rename arg to chat_messages * minor fix --- autogen/agentchat/conversable_agent.py | 11 +++- test/agentchat/test_conversable_agent.py | 71 ++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index f457667cf8b..b3c51837a0e 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -77,6 +77,7 @@ def __init__( llm_config: Optional[Union[Dict, Literal[False]]] = None, default_auto_reply: Union[str, Dict] = "", description: Optional[str] = None, + chat_messages: Optional[Dict[Agent, List[Dict]]] = None, ): """ Args: @@ -122,6 +123,9 @@ def __init__( default_auto_reply (str or dict): default auto reply when no code execution or llm-based reply is generated. description (str): a short description of the agent. This description is used by other agents (e.g. the GroupChatManager) to decide when to call upon this agent. (Default: system_message) + chat_messages (dict or None): the previous chat messages that this agent had in the past with other agents. + Can be used to give the agent a memory by providing the chat history. This will allow the agent to + resume previous had conversations. Defaults to an empty chat history. """ # we change code_execution_config below and we have to make sure we don't change the input # in case of UserProxyAgent, without this we could even change the default value {} @@ -131,7 +135,11 @@ def __init__( self._name = name # a dictionary of conversations, default value is list - self._oai_messages = defaultdict(list) + if chat_messages is None: + self._oai_messages = defaultdict(list) + else: + self._oai_messages = chat_messages + self._oai_system_message = [{"content": system_message, "role": "system"}] self._description = description if description is not None else system_message self._is_termination_msg = ( @@ -1210,7 +1218,6 @@ def initiate_chats(self, chat_queue: List[Dict[str, Any]]) -> List[ChatResult]: return self._finished_chats async def a_initiate_chats(self, chat_queue: List[Dict[str, Any]]) -> Dict[int, ChatResult]: - _chat_queue = self._check_chat_queue_for_sender(chat_queue) self._finished_chats = await a_initiate_chats(_chat_queue) return self._finished_chats diff --git a/test/agentchat/test_conversable_agent.py b/test/agentchat/test_conversable_agent.py index 19d724ff854..b57dcf1b597 100755 --- a/test/agentchat/test_conversable_agent.py +++ b/test/agentchat/test_conversable_agent.py @@ -1311,6 +1311,77 @@ def test_messages_with_carryover(): assert len(generated_message["content"]) == 2 +def test_chat_history(): + alice = autogen.ConversableAgent( + "alice", + human_input_mode="NEVER", + llm_config=False, + default_auto_reply="This is alice speaking.", + ) + + charlie = autogen.ConversableAgent( + "charlie", + human_input_mode="NEVER", + llm_config=False, + default_auto_reply="This is charlie speaking.", + ) + + max_turns = 2 + + def bob_initiate_chat(agent: ConversableAgent, text: Literal["past", "future"]): + _ = agent.initiate_chat( + alice, + message=f"This is bob from the {text} speaking.", + max_turns=max_turns, + clear_history=False, + silent=True, + ) + _ = agent.initiate_chat( + charlie, + message=f"This is bob from the {text} speaking.", + max_turns=max_turns, + clear_history=False, + silent=True, + ) + + bob = autogen.ConversableAgent( + "bob", + human_input_mode="NEVER", + llm_config=False, + default_auto_reply="This is bob from the past speaking.", + ) + bob_initiate_chat(bob, "past") + context = bob.chat_messages + + del bob + + # Test agent with chat history + bob = autogen.ConversableAgent( + "bob", + human_input_mode="NEVER", + llm_config=False, + default_auto_reply="This is bob from the future speaking.", + chat_messages=context, + ) + + assert bool(bob.chat_messages) + assert bob.chat_messages == context + + # two times the max turns due to bob replies + assert len(bob.chat_messages[alice]) == 2 * max_turns + assert len(bob.chat_messages[charlie]) == 2 * max_turns + + bob_initiate_chat(bob, "future") + assert len(bob.chat_messages[alice]) == 4 * max_turns + assert len(bob.chat_messages[charlie]) == 4 * max_turns + + assert bob.chat_messages[alice][0]["content"] == "This is bob from the past speaking." + assert bob.chat_messages[charlie][0]["content"] == "This is bob from the past speaking." + + assert bob.chat_messages[alice][-2]["content"] == "This is bob from the future speaking." + assert bob.chat_messages[charlie][-2]["content"] == "This is bob from the future speaking." + + if __name__ == "__main__": # test_trigger() # test_context() From 2daae427083b7de9c1e856c5d41946522c64f555 Mon Sep 17 00:00:00 2001 From: Xiaoyun Zhang Date: Tue, 23 Apr 2024 10:25:11 -0700 Subject: [PATCH 04/30] enable lfs support in dotnet workflow (#2483) * enable lfs support in dotnet workflow * Update dotnet-release.yml * Update dotnet-build.yml --- .github/workflows/dotnet-build.yml | 6 +++++- .github/workflows/dotnet-release.yml | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index d223fffd28b..61e811804a8 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -22,13 +22,15 @@ permissions: jobs: build: - name: Build + name: Dotnet Build runs-on: ubuntu-latest defaults: run: working-directory: dotnet steps: - uses: actions/checkout@v4 + with: + lfs: true - name: Setup .NET uses: actions/setup-dotnet@v4 with: @@ -54,6 +56,8 @@ jobs: needs: build steps: - uses: actions/checkout@v4 + with: + lfs: true - name: Setup .NET uses: actions/setup-dotnet@v4 with: diff --git a/.github/workflows/dotnet-release.yml b/.github/workflows/dotnet-release.yml index d66f21a6cd6..af7104cc0e6 100644 --- a/.github/workflows/dotnet-release.yml +++ b/.github/workflows/dotnet-release.yml @@ -27,6 +27,8 @@ jobs: working-directory: dotnet steps: - uses: actions/checkout@v4 + with: + lfs: true - name: Setup .NET uses: actions/setup-dotnet@v4 with: @@ -66,4 +68,4 @@ jobs: $version = $metaInfoContent | Select-String -Pattern "(.*)" | ForEach-Object { $_.Matches.Groups[1].Value } git tag -a "$version" -m "AutoGen.Net release $version" git push origin --tags - shell: pwsh \ No newline at end of file + shell: pwsh From a41182a93f5d2927e2d84e3510bad252b8b01527 Mon Sep 17 00:00:00 2001 From: Ian Date: Wed, 24 Apr 2024 01:26:06 +0800 Subject: [PATCH 05/30] Support openai assistant v2 API (#2466) * adapted to openai assistant v2 api * fix comments * format code * fix ci * Update autogen/agentchat/contrib/gpt_assistant_agent.py Co-authored-by: Eric Zhu --------- Co-authored-by: Eric Zhu --- .../agentchat/contrib/gpt_assistant_agent.py | 54 +++++---- autogen/oai/openai_utils.py | 103 ++++++++++++++++++ setup.py | 2 +- test/agentchat/contrib/test_gpt_assistant.py | 44 +++----- 4 files changed, 150 insertions(+), 53 deletions(-) diff --git a/autogen/agentchat/contrib/gpt_assistant_agent.py b/autogen/agentchat/contrib/gpt_assistant_agent.py index 253d4d18e2e..0f5de8adcb5 100644 --- a/autogen/agentchat/contrib/gpt_assistant_agent.py +++ b/autogen/agentchat/contrib/gpt_assistant_agent.py @@ -10,7 +10,7 @@ from autogen import OpenAIWrapper from autogen.agentchat.agent import Agent from autogen.agentchat.assistant_agent import AssistantAgent, ConversableAgent -from autogen.oai.openai_utils import retrieve_assistants_by_name +from autogen.oai.openai_utils import create_gpt_assistant, retrieve_assistants_by_name, update_gpt_assistant logger = logging.getLogger(__name__) @@ -50,7 +50,8 @@ def __init__( - check_every_ms: check thread run status interval - tools: Give Assistants access to OpenAI-hosted tools like Code Interpreter and Knowledge Retrieval, or build your own tools using Function calling. ref https://platform.openai.com/docs/assistants/tools - - file_ids: files used by retrieval in run + - file_ids: (Deprecated) files used by retrieval in run. It is Deprecated, use tool_resources instead. https://platform.openai.com/docs/assistants/migration/what-has-changed. + - tool_resources: A set of resources that are used by the assistant's tools. The resources are specific to the type of tool. overwrite_instructions (bool): whether to overwrite the instructions of an existing assistant. This parameter is in effect only when assistant_id is specified in llm_config. overwrite_tools (bool): whether to overwrite the tools of an existing assistant. This parameter is in effect only when assistant_id is specified in llm_config. kwargs (dict): Additional configuration options for the agent. @@ -90,7 +91,6 @@ def __init__( candidate_assistants, instructions, openai_assistant_cfg.get("tools", []), - openai_assistant_cfg.get("file_ids", []), ) if len(candidate_assistants) == 0: @@ -101,12 +101,12 @@ def __init__( "No instructions were provided for new assistant. Using default instructions from AssistantAgent.DEFAULT_SYSTEM_MESSAGE." ) instructions = AssistantAgent.DEFAULT_SYSTEM_MESSAGE - self._openai_assistant = self._openai_client.beta.assistants.create( + self._openai_assistant = create_gpt_assistant( + self._openai_client, name=name, instructions=instructions, - tools=openai_assistant_cfg.get("tools", []), model=model_name, - file_ids=openai_assistant_cfg.get("file_ids", []), + assistant_config=openai_assistant_cfg, ) else: logger.warning( @@ -127,9 +127,12 @@ def __init__( logger.warning( "overwrite_instructions is True. Provided instructions will be used and will modify the assistant in the API" ) - self._openai_assistant = self._openai_client.beta.assistants.update( + self._openai_assistant = update_gpt_assistant( + self._openai_client, assistant_id=openai_assistant_id, - instructions=instructions, + assistant_config={ + "instructions": instructions, + }, ) else: logger.warning( @@ -154,9 +157,13 @@ def __init__( logger.warning( "overwrite_tools is True. Provided tools will be used and will modify the assistant in the API" ) - self._openai_assistant = self._openai_client.beta.assistants.update( + self._openai_assistant = update_gpt_assistant( + self._openai_client, assistant_id=openai_assistant_id, - tools=openai_assistant_cfg.get("tools", []), + assistant_config={ + "tools": specified_tools, + "tool_resources": openai_assistant_cfg.get("tool_resources", None), + }, ) else: # Tools are specified but overwrite_tools is False; do not update the assistant's tools @@ -198,6 +205,8 @@ def _invoke_assistant( assistant_thread = self._openai_threads[sender] # Process each unread message for message in pending_messages: + if message["content"].strip() == "": + continue self._openai_client.beta.threads.messages.create( thread_id=assistant_thread.id, content=message["content"], @@ -426,22 +435,23 @@ def delete_assistant(self): logger.warning("Permanently deleting assistant...") self._openai_client.beta.assistants.delete(self.assistant_id) - def find_matching_assistant(self, candidate_assistants, instructions, tools, file_ids): + def find_matching_assistant(self, candidate_assistants, instructions, tools): """ Find the matching assistant from a list of candidate assistants. - Filter out candidates with the same name but different instructions, file IDs, and function names. - TODO: implement accurate match based on assistant metadata fields. + Filter out candidates with the same name but different instructions, and function names. """ matching_assistants = [] # Preprocess the required tools for faster comparison - required_tool_types = set(tool.get("type") for tool in tools) + required_tool_types = set( + "file_search" if tool.get("type") in ["retrieval", "file_search"] else tool.get("type") for tool in tools + ) + required_function_names = set( tool.get("function", {}).get("name") for tool in tools - if tool.get("type") not in ["code_interpreter", "retrieval"] + if tool.get("type") not in ["code_interpreter", "retrieval", "file_search"] ) - required_file_ids = set(file_ids) # Convert file_ids to a set for unordered comparison for assistant in candidate_assistants: # Check if instructions are similar @@ -454,11 +464,12 @@ def find_matching_assistant(self, candidate_assistants, instructions, tools, fil continue # Preprocess the assistant's tools - assistant_tool_types = set(tool.type for tool in assistant.tools) + assistant_tool_types = set( + "file_search" if tool.type in ["retrieval", "file_search"] else tool.type for tool in assistant.tools + ) assistant_function_names = set(tool.function.name for tool in assistant.tools if hasattr(tool, "function")) - assistant_file_ids = set(getattr(assistant, "file_ids", [])) # Convert to set for comparison - # Check if the tool types, function names, and file IDs match + # Check if the tool types, function names match if required_tool_types != assistant_tool_types or required_function_names != assistant_function_names: logger.warning( "tools not match, skip assistant(%s): tools %s, functions %s", @@ -467,9 +478,6 @@ def find_matching_assistant(self, candidate_assistants, instructions, tools, fil assistant_function_names, ) continue - if required_file_ids != assistant_file_ids: - logger.warning("file_ids not match, skip assistant(%s): %s", assistant.id, assistant_file_ids) - continue # Append assistant to matching list if all conditions are met matching_assistants.append(assistant) @@ -496,7 +504,7 @@ def _process_assistant_config(self, llm_config, assistant_config): # Move the assistant related configurations to assistant_config # It's important to keep forward compatibility - assistant_config_items = ["assistant_id", "tools", "file_ids", "check_every_ms"] + assistant_config_items = ["assistant_id", "tools", "file_ids", "tool_resources", "check_every_ms"] for item in assistant_config_items: if openai_client_cfg.get(item) is not None and openai_assistant_cfg.get(item) is None: openai_assistant_cfg[item] = openai_client_cfg[item] diff --git a/autogen/oai/openai_utils.py b/autogen/oai/openai_utils.py index 537390109c0..25ac8dae298 100644 --- a/autogen/oai/openai_utils.py +++ b/autogen/oai/openai_utils.py @@ -1,14 +1,17 @@ +import importlib.metadata import json import logging import os import re import tempfile +import time from pathlib import Path from typing import Any, Dict, List, Optional, Set, Union from dotenv import find_dotenv, load_dotenv from openai import OpenAI from openai.types.beta.assistant import Assistant +from packaging.version import parse NON_CACHE_KEY = ["api_key", "base_url", "api_type", "api_version"] DEFAULT_AZURE_API_VERSION = "2024-02-15-preview" @@ -675,3 +678,103 @@ def retrieve_assistants_by_name(client: OpenAI, name: str) -> List[Assistant]: if assistant.name == name: candidate_assistants.append(assistant) return candidate_assistants + + +def detect_gpt_assistant_api_version() -> str: + """Detect the openai assistant API version""" + oai_version = importlib.metadata.version("openai") + if parse(oai_version) < parse("1.21"): + return "v1" + else: + return "v2" + + +def create_gpt_vector_store(client: OpenAI, name: str, fild_ids: List[str]) -> Any: + """Create a openai vector store for gpt assistant""" + + vector_store = client.beta.vector_stores.create(name=name) + # poll the status of the file batch for completion. + batch = client.beta.vector_stores.file_batches.create_and_poll(vector_store_id=vector_store.id, file_ids=fild_ids) + + if batch.status == "in_progress": + time.sleep(1) + logging.debug(f"file batch status: {batch.file_counts}") + batch = client.beta.vector_stores.file_batches.poll(vector_store_id=vector_store.id, batch_id=batch.id) + + if batch.status == "completed": + return vector_store + + raise ValueError(f"Failed to upload files to vector store {vector_store.id}:{batch.status}") + + +def create_gpt_assistant( + client: OpenAI, name: str, instructions: str, model: str, assistant_config: Dict[str, Any] +) -> Assistant: + """Create a openai gpt assistant""" + + assistant_create_kwargs = {} + gpt_assistant_api_version = detect_gpt_assistant_api_version() + tools = assistant_config.get("tools", []) + + if gpt_assistant_api_version == "v2": + tool_resources = assistant_config.get("tool_resources", {}) + file_ids = assistant_config.get("file_ids") + if tool_resources.get("file_search") is not None and file_ids is not None: + raise ValueError( + "Cannot specify both `tool_resources['file_search']` tool and `file_ids` in the assistant config." + ) + + # Designed for backwards compatibility for the V1 API + # Instead of V1 AssistantFile, files are attached to Assistants using the tool_resources object. + for tool in tools: + if tool["type"] == "retrieval": + tool["type"] = "file_search" + if file_ids is not None: + # create a vector store for the file search tool + vs = create_gpt_vector_store(client, f"{name}-vectorestore", file_ids) + tool_resources["file_search"] = { + "vector_store_ids": [vs.id], + } + elif tool["type"] == "code_interpreter" and file_ids is not None: + tool_resources["code_interpreter"] = { + "file_ids": file_ids, + } + + assistant_create_kwargs["tools"] = tools + if len(tool_resources) > 0: + assistant_create_kwargs["tool_resources"] = tool_resources + else: + # not support forwards compatibility + if "tool_resources" in assistant_config: + raise ValueError("`tool_resources` argument are not supported in the openai assistant V1 API.") + if any(tool["type"] == "file_search" for tool in tools): + raise ValueError( + "`file_search` tool are not supported in the openai assistant V1 API, please use `retrieval`." + ) + assistant_create_kwargs["tools"] = tools + assistant_create_kwargs["file_ids"] = assistant_config.get("file_ids", []) + + logging.info(f"Creating assistant with config: {assistant_create_kwargs}") + return client.beta.assistants.create(name=name, instructions=instructions, model=model, **assistant_create_kwargs) + + +def update_gpt_assistant(client: OpenAI, assistant_id: str, assistant_config: Dict[str, Any]) -> Assistant: + """Update openai gpt assistant""" + + gpt_assistant_api_version = detect_gpt_assistant_api_version() + assistant_update_kwargs = {} + + if assistant_config.get("tools") is not None: + assistant_update_kwargs["tools"] = assistant_config["tools"] + + if assistant_config.get("instructions") is not None: + assistant_update_kwargs["instructions"] = assistant_config["instructions"] + + if gpt_assistant_api_version == "v2": + if assistant_config.get("tool_resources") is not None: + assistant_update_kwargs["tool_resources"] = assistant_config["tool_resources"] + else: + if assistant_config.get("file_ids") is not None: + assistant_update_kwargs["file_ids"] = assistant_config["file_ids"] + + return client.beta.assistants.update(assistant_id=assistant_id, **assistant_update_kwargs) diff --git a/setup.py b/setup.py index 674df273883..29c7dda0322 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ __version__ = version["__version__"] install_requires = [ - "openai>=1.3,<1.21", + "openai>=1.3", "diskcache", "termcolor", "flaml", diff --git a/test/agentchat/contrib/test_gpt_assistant.py b/test/agentchat/contrib/test_gpt_assistant.py index 052eedd311a..643e35e89f2 100755 --- a/test/agentchat/contrib/test_gpt_assistant.py +++ b/test/agentchat/contrib/test_gpt_assistant.py @@ -11,7 +11,7 @@ import autogen from autogen import OpenAIWrapper, UserProxyAgent from autogen.agentchat.contrib.gpt_assistant_agent import GPTAssistantAgent -from autogen.oai.openai_utils import retrieve_assistants_by_name +from autogen.oai.openai_utils import detect_gpt_assistant_api_version, retrieve_assistants_by_name sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) from conftest import reason, skip_openai # noqa: E402 @@ -264,6 +264,7 @@ def test_get_assistant_files() -> None: openai_client = OpenAIWrapper(config_list=openai_config_list)._clients[0]._oai_client file = openai_client.files.create(file=open(current_file_path, "rb"), purpose="assistants") name = f"For test_get_assistant_files {uuid.uuid4()}" + gpt_assistant_api_version = detect_gpt_assistant_api_version() # keep it to test older version of assistant config assistant = GPTAssistantAgent( @@ -277,10 +278,17 @@ def test_get_assistant_files() -> None: ) try: - files = assistant.openai_client.beta.assistants.files.list(assistant_id=assistant.assistant_id) - retrieved_file_ids = [fild.id for fild in files] + if gpt_assistant_api_version == "v1": + files = assistant.openai_client.beta.assistants.files.list(assistant_id=assistant.assistant_id) + retrieved_file_ids = [fild.id for fild in files] + elif gpt_assistant_api_version == "v2": + oas_assistant = assistant.openai_client.beta.assistants.retrieve(assistant_id=assistant.assistant_id) + vectorstore_ids = oas_assistant.tool_resources.file_search.vector_store_ids + retrieved_file_ids = [] + for vectorstore_id in vectorstore_ids: + files = assistant.openai_client.beta.vector_stores.files.list(vector_store_id=vectorstore_id) + retrieved_file_ids.extend([fild.id for fild in files]) expected_file_id = file.id - finally: assistant.delete_assistant() openai_client.files.delete(file.id) @@ -401,7 +409,7 @@ def test_assistant_mismatch_retrieval() -> None: "tools": [ {"type": "function", "function": function_1_schema}, {"type": "function", "function": function_2_schema}, - {"type": "retrieval"}, + {"type": "file_search"}, {"type": "code_interpreter"}, ], "file_ids": [file_1.id, file_2.id], @@ -411,7 +419,6 @@ def test_assistant_mismatch_retrieval() -> None: name = f"For test_assistant_retrieval {uuid.uuid4()}" assistant_first, assistant_instructions_mistaching = None, None - assistant_file_ids_mismatch, assistant_tools_mistaching = None, None try: assistant_first = GPTAssistantAgent( name, @@ -432,30 +439,11 @@ def test_assistant_mismatch_retrieval() -> None: ) assert len(candidate_instructions_mistaching) == 2 - # test mismatch fild ids - file_ids_mismatch_llm_config = { - "tools": [ - {"type": "code_interpreter"}, - {"type": "retrieval"}, - {"type": "function", "function": function_2_schema}, - {"type": "function", "function": function_1_schema}, - ], - "file_ids": [file_2.id], - "config_list": openai_config_list, - } - assistant_file_ids_mismatch = GPTAssistantAgent( - name, - instructions="This is a test", - llm_config=file_ids_mismatch_llm_config, - ) - candidate_file_ids_mismatch = retrieve_assistants_by_name(assistant_file_ids_mismatch.openai_client, name) - assert len(candidate_file_ids_mismatch) == 3 - # test tools mismatch tools_mismatch_llm_config = { "tools": [ {"type": "code_interpreter"}, - {"type": "retrieval"}, + {"type": "file_search"}, {"type": "function", "function": function_3_schema}, ], "file_ids": [file_2.id, file_1.id], @@ -467,15 +455,13 @@ def test_assistant_mismatch_retrieval() -> None: llm_config=tools_mismatch_llm_config, ) candidate_tools_mismatch = retrieve_assistants_by_name(assistant_tools_mistaching.openai_client, name) - assert len(candidate_tools_mismatch) == 4 + assert len(candidate_tools_mismatch) == 3 finally: if assistant_first: assistant_first.delete_assistant() if assistant_instructions_mistaching: assistant_instructions_mistaching.delete_assistant() - if assistant_file_ids_mismatch: - assistant_file_ids_mismatch.delete_assistant() if assistant_tools_mistaching: assistant_tools_mistaching.delete_assistant() From ebde196d6b893003ef7986dd721d1686e42b9ea8 Mon Sep 17 00:00:00 2001 From: Eduardo Salinas Date: Tue, 23 Apr 2024 18:27:47 -0400 Subject: [PATCH 06/30] feat: add event logging api and more tracing (#2478) * feat: add event logging api and more tracing * code fmt shenanigans * fixup * Update test_agent_logging.py * Update test_agent_logging.py * Update test_agent_logging.py * Update sqlite_logger.py * Update test_agent_logging.py * Update sqlite_logger.py --------- Co-authored-by: Chi Wang --- autogen/agentchat/conversable_agent.py | 14 ++++- autogen/logger/base_logger.py | 14 ++++- autogen/logger/sqlite_logger.py | 51 ++++++++++++++++++- autogen/runtime_logging.py | 10 +++- .../autogenbench/template/testbed_utils.py | 2 +- test/agentchat/test_agent_logging.py | 11 ++++ 6 files changed, 97 insertions(+), 5 deletions(-) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index b3c51837a0e..262fc513d23 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -32,7 +32,7 @@ from ..function_utils import get_function_schema, load_basemodels_if_needed, serialize_to_str from ..io.base import IOStream from ..oai.client import ModelClient, OpenAIWrapper -from ..runtime_logging import log_new_agent, logging_enabled +from ..runtime_logging import log_event, log_new_agent, logging_enabled from .agent import Agent, LLMAgent from .chat import ChatResult, a_initiate_chats, initiate_chats from .utils import consolidate_chat_info, gather_usage_summary @@ -757,6 +757,9 @@ def _print_received_message(self, message: Union[Dict, str], sender: Agent): def _process_received_message(self, message: Union[Dict, str], sender: Agent, silent: bool): # When the agent receives a message, the role of the message is "user". (If 'role' exists and is 'function', it will remain unchanged.) valid = self._append_oai_message(message, "user", sender) + if logging_enabled(): + log_event(self, "received_message", message=message, sender=sender.name, valid=valid) + if not valid: raise ValueError( "Received message can't be converted into a valid ChatCompletion message. Either content or function_call must be provided." @@ -1939,6 +1942,15 @@ def generate_reply( continue if self._match_trigger(reply_func_tuple["trigger"], sender): final, reply = reply_func(self, messages=messages, sender=sender, config=reply_func_tuple["config"]) + if logging_enabled(): + log_event( + self, + "reply_func_executed", + reply_func_module=reply_func.__module__, + reply_func_name=reply_func.__name__, + final=final, + reply=reply, + ) if final: return reply return self._default_auto_reply diff --git a/autogen/logger/base_logger.py b/autogen/logger/base_logger.py index 24e19c475c5..7c35f8a5091 100644 --- a/autogen/logger/base_logger.py +++ b/autogen/logger/base_logger.py @@ -9,7 +9,7 @@ from openai.types.chat import ChatCompletion if TYPE_CHECKING: - from autogen import ConversableAgent, OpenAIWrapper + from autogen import Agent, ConversableAgent, OpenAIWrapper ConfigItem = Dict[str, Union[str, List[str]]] LLMConfig = Dict[str, Union[None, float, int, ConfigItem, List[ConfigItem]]] @@ -68,6 +68,18 @@ def log_new_agent(self, agent: ConversableAgent, init_args: Dict[str, Any]) -> N """ ... + @abstractmethod + def log_event(self, source: Union[str, Agent], name: str, **kwargs: Dict[str, Any]) -> None: + """ + Log an event for an agent. + + Args: + source (str or Agent): The source/creator of the event as a string name or an Agent instance + name (str): The name of the event + kwargs (dict): The event information to log + """ + ... + @abstractmethod def log_new_wrapper(self, wrapper: OpenAIWrapper, init_args: Dict[str, Union[LLMConfig, List[LLMConfig]]]) -> None: """ diff --git a/autogen/logger/sqlite_logger.py b/autogen/logger/sqlite_logger.py index 62f758c51eb..6e95a571cd0 100644 --- a/autogen/logger/sqlite_logger.py +++ b/autogen/logger/sqlite_logger.py @@ -17,7 +17,7 @@ from .base_logger import LLMConfig if TYPE_CHECKING: - from autogen import ConversableAgent, OpenAIWrapper + from autogen import Agent, ConversableAgent, OpenAIWrapper logger = logging.getLogger(__name__) lock = threading.Lock() @@ -103,6 +103,20 @@ class TEXT, -- type or class name of cli """ self._run_query(query=query) + query = """ + CREATE TABLE IF NOT EXISTS events ( + event_name TEXT, + source_id INTEGER, + source_name TEXT, + agent_module TEXT DEFAULT NULL, + agent_class_name TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY, + json_state TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ); + """ + self._run_query(query=query) + current_verion = self._get_current_db_version() if current_verion is None: self._run_query( @@ -246,6 +260,41 @@ class = excluded.class, ) self._run_query(query=query, args=args) + def log_event(self, source: Union[str, Agent], name: str, **kwargs: Dict[str, Any]) -> None: + from autogen import Agent + + if self.con is None: + return + + json_args = json.dumps(kwargs, default=lambda o: f"<>") + + if isinstance(source, Agent): + query = """ + INSERT INTO events (source_id, source_name, event_name, agent_module, agent_class_name, json_state, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?) + """ + args = ( + id(source), + source.name if hasattr(source, "name") else source, + name, + source.__module__, + source.__class__.__name__, + json_args, + get_current_ts(), + ) + self._run_query(query=query, args=args) + else: + query = """ + INSERT INTO events (source_id, source_name, event_name, json_state, timestamp) VALUES (?, ?, ?, ?, ?) + """ + args_str_based = ( + id(source), + source.name if hasattr(source, "name") else source, + name, + json_args, + get_current_ts(), + ) + self._run_query(query=query, args=args_str_based) + def log_new_wrapper(self, wrapper: OpenAIWrapper, init_args: Dict[str, Union[LLMConfig, List[LLMConfig]]]) -> None: if self.con is None: return diff --git a/autogen/runtime_logging.py b/autogen/runtime_logging.py index 8c704b4383f..1b9835eaa4b 100644 --- a/autogen/runtime_logging.py +++ b/autogen/runtime_logging.py @@ -12,7 +12,7 @@ from autogen.logger.logger_factory import LoggerFactory if TYPE_CHECKING: - from autogen import ConversableAgent, OpenAIWrapper + from autogen import Agent, ConversableAgent, OpenAIWrapper logger = logging.getLogger(__name__) @@ -62,6 +62,14 @@ def log_new_agent(agent: ConversableAgent, init_args: Dict[str, Any]) -> None: autogen_logger.log_new_agent(agent, init_args) +def log_event(source: Union[str, Agent], name: str, **kwargs: Dict[str, Any]) -> None: + if autogen_logger is None: + logger.error("[runtime logging] log_event: autogen logger is None") + return + + autogen_logger.log_event(source, name, **kwargs) + + def log_new_wrapper(wrapper: OpenAIWrapper, init_args: Dict[str, Union[LLMConfig, List[LLMConfig]]]) -> None: if autogen_logger is None: logger.error("[runtime logging] log_new_wrapper: autogen logger is None") diff --git a/samples/tools/autogenbench/autogenbench/template/testbed_utils.py b/samples/tools/autogenbench/autogenbench/template/testbed_utils.py index 05ef9662e76..bce42a625b2 100644 --- a/samples/tools/autogenbench/autogenbench/template/testbed_utils.py +++ b/samples/tools/autogenbench/autogenbench/template/testbed_utils.py @@ -70,7 +70,7 @@ def init(): # Start logging if LOGGING_ENABLED: - autogen.runtime_logging.start(config={"dbname": "telemetry.db"}) + autogen.runtime_logging.start(config={"dbname": "telemetry.sqlite"}) def finalize(agents): diff --git a/test/agentchat/test_agent_logging.py b/test/agentchat/test_agent_logging.py index a776173253d..47798fbe0f6 100644 --- a/test/agentchat/test_agent_logging.py +++ b/test/agentchat/test_agent_logging.py @@ -34,6 +34,9 @@ OAI_WRAPPERS_QUERY = "SELECT id, wrapper_id, session_id, init_args, timestamp FROM oai_wrappers" +EVENTS_QUERY = ( + "SELECT source_id, source_name, event_name, agent_module, agent_class_name, json_state, timestamp FROM events" +) if not skip_openai: config_list = autogen.config_list_from_json( @@ -242,6 +245,14 @@ def test_groupchat_logging(db_connection): rows = cur.fetchall() assert len(rows) == 3 + # Verify events + cur.execute(EVENTS_QUERY) + rows = cur.fetchall() + json_state = json.loads(rows[0]["json_state"]) + assert rows[0]["event_name"] == "received_message" + assert json_state["message"] == "Can you explain the difference between eigenvalues and singular values again?" + assert len(rows) == 15 + # Verify schema version version_query = "SELECT id, version_number from version" cur.execute(version_query) From 31fe75ad0e657daa4caf3a8ffa4c937dfad9b1fb Mon Sep 17 00:00:00 2001 From: asandez1 <161049415+asandez1@users.noreply.github.com> Date: Tue, 23 Apr 2024 22:21:24 -0300 Subject: [PATCH 07/30] Add support for HTML, CSS and Javascript in LocalCommandLineCodeExecutor with Mapping executor/saver #2303 (#2464) * Add support for HTML, CSS and Javascript in LocalCommandLineCodeExecutor * init branch * init branch * feat: test code execution added * fix: test update * fix: test * fix: policy test * feat: default policy --------- Co-authored-by: Eric Zhu --- .../coding/local_commandline_code_executor.py | 87 ++++++++------ autogen/coding/utils.py | 32 +++-- test/coding/test_commandline_code_executor.py | 109 ++++++++++++++++++ 3 files changed, 183 insertions(+), 45 deletions(-) diff --git a/autogen/coding/local_commandline_code_executor.py b/autogen/coding/local_commandline_code_executor.py index 68ef76b7e7f..ed92cd527be 100644 --- a/autogen/coding/local_commandline_code_executor.py +++ b/autogen/coding/local_commandline_code_executor.py @@ -6,7 +6,7 @@ from hashlib import md5 from pathlib import Path from string import Template -from typing import Any, Callable, ClassVar, List, TypeVar, Union, cast +from typing import Any, Callable, ClassVar, Dict, List, Optional, Union from typing_extensions import ParamSpec @@ -28,7 +28,31 @@ class LocalCommandLineCodeExecutor(CodeExecutor): - SUPPORTED_LANGUAGES: ClassVar[List[str]] = ["bash", "shell", "sh", "pwsh", "powershell", "ps1", "python"] + SUPPORTED_LANGUAGES: ClassVar[List[str]] = [ + "bash", + "shell", + "sh", + "pwsh", + "powershell", + "ps1", + "python", + "javascript", + "html", + "css", + ] + DEFAULT_EXECUTION_POLICY: ClassVar[Dict[str, bool]] = { + "bash": True, + "shell": True, + "sh": True, + "pwsh": True, + "powershell": True, + "ps1": True, + "python": True, + "javascript": False, + "html": False, + "css": False, + } + FUNCTION_PROMPT_TEMPLATE: ClassVar[ str ] = """You have access to the following user defined functions. They can be accessed from the module called `$module_name` by their function names. @@ -43,29 +67,27 @@ def __init__( work_dir: Union[Path, str] = Path("."), functions: List[Union[FunctionWithRequirements[Any, A], Callable[..., Any], FunctionWithRequirementsStr]] = [], functions_module: str = "functions", + execution_policies: Optional[Dict[str, bool]] = None, ): - """(Experimental) A code executor class that executes code through a local command line + """(Experimental) A code executor class that executes or saves LLM generated code a local command line environment. - **This will execute LLM generated code on the local machine.** + **This will execute or save LLM generated code on the local machine.** - Each code block is saved as a file and executed in a separate process in - the working directory, and a unique file is generated and saved in the - working directory for each code block. - The code blocks are executed in the order they are received. - Command line code is sanitized using regular expression match against a list of dangerous commands in order to prevent self-destructive - commands from being executed which may potentially affect the users environment. - Currently the only supported languages is Python and shell scripts. - For Python code, use the language "python" for the code block. - For shell scripts, use the language "bash", "shell", or "sh" for the code - block. + Each code block is saved as a file in the working directory. Depending on the execution policy, + the code may be executed in a separate process. + The code blocks are executed or save in the order they are received. + Command line code is sanitized against a list of dangerous commands to prevent self-destructive commands from being executed, + which could potentially affect the user's environment. Supported languages include Python, shell scripts (bash, shell, sh), + PowerShell (pwsh, powershell, ps1), HTML, CSS, and JavaScript. + Execution policies determine whether each language's code blocks are executed or saved only. Args: - timeout (int): The timeout for code execution. Default is 60. - work_dir (str): The working directory for the code execution. If None, - a default working directory will be used. The default working - directory is the current directory ".". - functions (List[Union[FunctionWithRequirements[Any, A], Callable[..., Any]]]): A list of functions that are available to the code executor. Default is an empty list. + timeout (int): The timeout for code execution, default is 60 seconds. + work_dir (Union[Path, str]): The working directory for code execution, defaults to the current directory. + functions (List[Union[FunctionWithRequirements[Any, A], Callable[..., Any], FunctionWithRequirementsStr]]): A list of callable functions available to the executor. + functions_module (str): The module name under which functions are accessible. + execution_policies (Optional[Dict[str, bool]]): A dictionary mapping languages to execution policies (True for execution, False for saving only). Defaults to class-wide DEFAULT_EXECUTION_POLICY. """ if timeout < 1: @@ -91,6 +113,10 @@ def __init__( else: self._setup_functions_complete = True + self.execution_policies = self.DEFAULT_EXECUTION_POLICY.copy() + if execution_policies is not None: + self.execution_policies.update(execution_policies) + def format_functions_for_prompt(self, prompt_template: str = FUNCTION_PROMPT_TEMPLATE) -> str: """(Experimental) Format the functions for a prompt. @@ -104,7 +130,6 @@ def format_functions_for_prompt(self, prompt_template: str = FUNCTION_PROMPT_TEM Returns: str: The formatted prompt. """ - template = Template(prompt_template) return template.substitute( module_name=self._functions_module, @@ -171,26 +196,19 @@ def _setup_functions(self) -> None: required_packages = list(set(flattened_packages)) if len(required_packages) > 0: logging.info("Ensuring packages are installed in executor.") - - cmd = [sys.executable, "-m", "pip", "install"] - cmd.extend(required_packages) - + cmd = [sys.executable, "-m", "pip", "install"] + required_packages try: result = subprocess.run( cmd, cwd=self._work_dir, capture_output=True, text=True, timeout=float(self._timeout) ) except subprocess.TimeoutExpired as e: raise ValueError("Pip install timed out") from e - if result.returncode != 0: raise ValueError(f"Pip install failed. {result.stdout}, {result.stderr}") - # Attempt to load the function file to check for syntax errors, imports etc. exec_result = self._execute_code_dont_check_setup([CodeBlock(code=func_file_content, language="python")]) - if exec_result.exit_code != 0: raise ValueError(f"Functions failed to load: {exec_result.output}") - self._setup_functions_complete = True def execute_code_blocks(self, code_blocks: List[CodeBlock]) -> CommandLineCodeResult: @@ -201,10 +219,8 @@ def execute_code_blocks(self, code_blocks: List[CodeBlock]) -> CommandLineCodeRe Returns: CommandLineCodeResult: The result of the code execution.""" - if not self._setup_functions_complete: self._setup_functions() - return self._execute_code_dont_check_setup(code_blocks) def _execute_code_dont_check_setup(self, code_blocks: List[CodeBlock]) -> CommandLineCodeResult: @@ -229,6 +245,7 @@ def _execute_code_dont_check_setup(self, code_blocks: List[CodeBlock]) -> Comman logs_all += "\n" + f"unknown language {lang}" break + execute_code = self.execution_policies.get(lang, False) try: # Check if there is a filename comment filename = _get_file_name_from_content(code, self._work_dir) @@ -239,15 +256,19 @@ def _execute_code_dont_check_setup(self, code_blocks: List[CodeBlock]) -> Comman # create a file with an automatically generated name code_hash = md5(code.encode()).hexdigest() filename = f"tmp_code_{code_hash}.{'py' if lang.startswith('python') else lang}" - written_file = (self._work_dir / filename).resolve() with written_file.open("w", encoding="utf-8") as f: f.write(code) file_names.append(written_file) - program = sys.executable if lang.startswith("python") else _cmd(lang) - cmd = [program, str(written_file.absolute())] + if not execute_code: + # Just return a message that the file is saved. + logs_all += f"Code saved to {str(written_file)}\n" + exitcode = 0 + continue + program = _cmd(lang) + cmd = [program, str(written_file.absolute())] try: result = subprocess.run( cmd, cwd=self._work_dir, capture_output=True, text=True, timeout=float(self._timeout) diff --git a/autogen/coding/utils.py b/autogen/coding/utils.py index 0a7c5a7785d..d692bfe35b9 100644 --- a/autogen/coding/utils.py +++ b/autogen/coding/utils.py @@ -3,23 +3,31 @@ from pathlib import Path from typing import Optional +filename_patterns = [ + re.compile(r"^", re.DOTALL), + re.compile(r"^/\* (filename:)?(.+?) \*/", re.DOTALL), + re.compile(r"^// (filename:)?(.+?)$", re.DOTALL), + re.compile(r"^# (filename:)?(.+?)$", re.DOTALL), +] + # Raises ValueError if the file is not in the workspace def _get_file_name_from_content(code: str, workspace_path: Path) -> Optional[str]: - first_line = code.split("\n")[0] + first_line = code.split("\n")[0].strip() # TODO - support other languages - if first_line.startswith("# filename:"): - filename = first_line.split(":")[1].strip() - - # Handle relative paths in the filename - path = Path(filename) - if not path.is_absolute(): - path = workspace_path / path - path = path.resolve() - # Throws an error if the file is not in the workspace - relative = path.relative_to(workspace_path.resolve()) - return str(relative) + for pattern in filename_patterns: + matches = pattern.match(first_line) + if matches is not None: + filename = matches.group(2).strip() + # Handle relative paths in the filename + path = Path(filename) + if not path.is_absolute(): + path = workspace_path / path + path = path.resolve() + # Throws an error if the file is not in the workspace + relative = path.relative_to(workspace_path.resolve()) + return str(relative) return None diff --git a/test/coding/test_commandline_code_executor.py b/test/coding/test_commandline_code_executor.py index a83282dec78..20041c54b42 100644 --- a/test/coding/test_commandline_code_executor.py +++ b/test/coding/test_commandline_code_executor.py @@ -26,6 +26,34 @@ PYTHON_VARIANTS = ["python", "Python", "py"] +@pytest.mark.parametrize( + "lang, should_execute", + [ + ("python", False), # Python should not execute + ("bash", False), # Bash should execute + ("html", False), # HTML should not execute + ("javascript", False), # JavaScript should not execute + ], +) +def test_execution_policy_enforcement(lang, should_execute): + with tempfile.TemporaryDirectory() as temp_dir: + executor = LocalCommandLineCodeExecutor( + work_dir=temp_dir, + execution_policies={"python": False, "bash": False, "html": False, "javascript": False, "css": False}, + ) + code = "print('Hello, world!')" if lang == "python" else "echo 'Hello, world!'" + code_block = CodeBlock(code=code, language=lang) + result = executor.execute_code_blocks([code_block]) + + if should_execute: + assert "Hello, world!" in result.output, f"Expected execution for {lang}, but it didn't execute." + else: + assert "Hello, world!" not in result.output, f"Expected no execution for {lang}, but it executed." + + # Ensure files are saved regardless of execution + assert result.code_file is not None, f"Expected code file to be saved for {lang}, but it wasn't." + + @pytest.mark.parametrize("cls", classes_to_test) def test_is_code_executor(cls) -> None: assert isinstance(cls, CodeExecutor) @@ -114,6 +142,87 @@ def _test_execute_code(py_variant, executor: CodeExecutor) -> None: assert file_line.strip() == code_line.strip() +def test_local_commandline_code_executor_save_files() -> None: + with tempfile.TemporaryDirectory() as temp_dir: + executor = LocalCommandLineCodeExecutor(work_dir=temp_dir) + _test_save_files(executor, save_file_only=False) + + +def test_local_commandline_code_executor_save_files_only() -> None: + with tempfile.TemporaryDirectory() as temp_dir: + # Using execution_policies to specify that no languages should execute + executor = LocalCommandLineCodeExecutor( + work_dir=temp_dir, + execution_policies={"python": False, "bash": False, "javascript": False, "html": False, "css": False}, + ) + _test_save_files(executor, save_file_only=True) + + +def _test_save_files(executor: CodeExecutor, save_file_only: bool) -> None: + + def _check_output(code_result: CodeBlock, expected_output: str) -> None: + if save_file_only: + return expected_output not in code_result.output + else: + return expected_output in code_result.output + + # Test executable code block. + + # Test saving to a given filename, Python. + code_blocks = [CodeBlock(code="# filename: test.py\nimport sys; print('hello world!')", language="python")] + code_result = executor.execute_code_blocks(code_blocks) + assert ( + code_result.exit_code == 0 and _check_output(code_result, "hello world!") and code_result.code_file is not None + ) + assert os.path.basename(code_result.code_file) == "test.py" + + # Test saving to a given filename without "filename" prefix, Python. + code_blocks = [CodeBlock(code="# test.py\nimport sys; print('hello world!')", language="python")] + code_result = executor.execute_code_blocks(code_blocks) + assert ( + code_result.exit_code == 0 and _check_output(code_result, "hello world!") and code_result.code_file is not None + ) + assert os.path.basename(code_result.code_file) == "test.py" + + # Test non-executable code block. + + # Test saving to a given filename, Javascript. + code_blocks = [CodeBlock(code="// filename: test.js\nconsole.log('hello world!')", language="javascript")] + code_result = executor.execute_code_blocks(code_blocks) + assert code_result.exit_code == 0 and "hello world!" not in code_result.output and code_result.code_file is not None + assert os.path.basename(code_result.code_file) == "test.js" + + # Test saving to a given filename without "filename" prefix, Javascript. + code_blocks = [CodeBlock(code="// test.js\nconsole.log('hello world!')", language="javascript")] + code_result = executor.execute_code_blocks(code_blocks) + assert code_result.exit_code == 0 and "hello world!" not in code_result.output and code_result.code_file is not None + assert os.path.basename(code_result.code_file) == "test.js" + + # Test saving to a given filename, CSS. + code_blocks = [CodeBlock(code="/* filename: test.css */\nh1 { color: red; }", language="css")] + code_result = executor.execute_code_blocks(code_blocks) + assert code_result.exit_code == 0 and "hello world!" not in code_result.output and code_result.code_file is not None + assert os.path.basename(code_result.code_file) == "test.css" + + # Test saving to a given filename without "filename" prefix, CSS. + code_blocks = [CodeBlock(code="/* test.css */\nh1 { color: red; }", language="css")] + code_result = executor.execute_code_blocks(code_blocks) + assert code_result.exit_code == 0 and "hello world!" not in code_result.output and code_result.code_file is not None + assert os.path.basename(code_result.code_file) == "test.css" + + # Test saving to a given filename, HTML. + code_blocks = [CodeBlock(code="\n

hello world!

", language="html")] + code_result = executor.execute_code_blocks(code_blocks) + assert code_result.exit_code == 0 and "hello world!" not in code_result.output and code_result.code_file is not None + assert os.path.basename(code_result.code_file) == "test.html" + + # Test saving to a given filename without "filename" prefix, HTML. + code_blocks = [CodeBlock(code="\n

hello world!

", language="html")] + code_result = executor.execute_code_blocks(code_blocks) + assert code_result.exit_code == 0 and "hello world!" not in code_result.output and code_result.code_file is not None + assert os.path.basename(code_result.code_file) == "test.html" + + @pytest.mark.parametrize("cls", classes_to_test) def test_commandline_code_executor_timeout(cls) -> None: with tempfile.TemporaryDirectory() as temp_dir: From 2a4ccd062270de0a2217921f132d1fd6a4a9525d Mon Sep 17 00:00:00 2001 From: Li Jiang Date: Thu, 25 Apr 2024 22:33:36 +0800 Subject: [PATCH 08/30] Fix python not supported in macos (#2503) * Fix python 3.8 and 3.9 not supported for macos * Fix python 3.8 and 3.9 not supported for macos * Fix format --- .github/workflows/build.yml | 5 +++++ .github/workflows/contrib-tests.yml | 8 +++++++- .github/workflows/samples-tools-tests.yml | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1c32eee6036..1cdc942afea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,6 +30,11 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + exclude: + - os: macos-latest + python-version: "3.8" + - os: macos-latest + python-version: "3.9" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/contrib-tests.yml b/.github/workflows/contrib-tests.yml index 4e042b458e0..9766addcdcb 100644 --- a/.github/workflows/contrib-tests.yml +++ b/.github/workflows/contrib-tests.yml @@ -29,6 +29,9 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-2019] python-version: ["3.9", "3.10", "3.11"] + exclude: + - os: macos-latest + python-version: "3.9" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -79,7 +82,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-2019] - python-version: ["3.8"] + python-version: ["3.10"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -267,6 +270,9 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-2019] python-version: ["3.9", "3.10", "3.11", "3.12"] + exclude: + - os: macos-latest + python-version: "3.9" steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/samples-tools-tests.yml b/.github/workflows/samples-tools-tests.yml index 12c8de3b7af..af7dc6c4743 100644 --- a/.github/workflows/samples-tools-tests.yml +++ b/.github/workflows/samples-tools-tests.yml @@ -24,6 +24,9 @@ jobs: matrix: os: [ubuntu-latest, macos-latest] python-version: ["3.9", "3.10", "3.11"] + exclude: + - os: macos-latest + python-version: "3.9" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} From 83631b274ab18b9aefc0e20a02f1f350bf85cabf Mon Sep 17 00:00:00 2001 From: themataleao Date: Thu, 25 Apr 2024 19:58:38 +0200 Subject: [PATCH 09/30] fix typo (#2497) --- website/docs/tutorial/human-in-the-loop.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/tutorial/human-in-the-loop.ipynb b/website/docs/tutorial/human-in-the-loop.ipynb index 52bb7e1f7b9..d6d47a223fe 100644 --- a/website/docs/tutorial/human-in-the-loop.ipynb +++ b/website/docs/tutorial/human-in-the-loop.ipynb @@ -35,7 +35,7 @@ "1. `NEVER`: human input is never requested.\n", "2. `TERMINATE` (default): human input is only requested when a termination condition is\n", " met. Note that in this mode if the human chooses to intercept and reply, the conversation continues\n", - " and the counter used by `max_consectuive_auto_reply` is reset.\n", + " and the counter used by `max_consecutive_auto_reply` is reset.\n", "3. `ALWAYS`: human input is always requested and the human can choose to skip and trigger an auto-reply,\n", " intercept and provide feedback, or terminate the conversation. Note that in this mode\n", " termination based on `max_consecutive_auto_reply` is ignored.\n", From 0d29cfb27e6c354b084d776d96a656aa2e8d98b1 Mon Sep 17 00:00:00 2001 From: HRUSHIKESH DOKALA <96101829+Hk669@users.noreply.github.com> Date: Fri, 26 Apr 2024 02:20:01 +0530 Subject: [PATCH 10/30] fix: import fixed (#2504) --- website/blog/2023-10-18-RetrieveChat/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/blog/2023-10-18-RetrieveChat/index.mdx b/website/blog/2023-10-18-RetrieveChat/index.mdx index 92089ba247d..1685b22f5d8 100644 --- a/website/blog/2023-10-18-RetrieveChat/index.mdx +++ b/website/blog/2023-10-18-RetrieveChat/index.mdx @@ -311,7 +311,7 @@ boss_aid = RetrieveUserProxyAgent( code_execution_config=False, # we don't want to execute code in this case. ) -coder = AssistantAgent( +coder = autogen.AssistantAgent( name="Senior_Python_Engineer", is_termination_msg=termination_msg, system_message="You are a senior python engineer. Reply `TERMINATE` in the end when everything is done.", From fbcc56c90efa469b7660272b277850169377c110 Mon Sep 17 00:00:00 2001 From: William W Wang <107702013+wmwxwa@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:23:18 -0400 Subject: [PATCH 11/30] AutoGen cache using Azure Cosmos DB (#2327) * Create cosmos_db_cache.py * Update cosmos_db_cache.py * Update cosmos_db_cache.py * Update cosmos_db_cache.py * Update cosmos_db_cache.py * Update cosmos_db_cache.py * Update cosmos_db_cache.py * Create test_cosmos_db_cache.py * Update cosmos_db_cache.py * Update test_cosmos_db_cache.py * Update cosmos_db_cache.py * Update test_cosmos_db_cache.py * Update cosmos_db_cache.py * Update test_cosmos_db_cache.py * Update cosmos_db_cache.py * Update cosmos_db_cache.py * Update test_cosmos_db_cache.py * Update cosmos_db_cache.py * Update cosmos_db_cache.py * Update cache.py * Update cache_factory.py * Update cache.py * Update cache_factory.py * Update test_cache.py * Update test_cache.py * Update cache.py * Update llm-caching.md * Update cache.py * Update cache.py * Update cache.py * Update cache_factory.py * Update cosmos_db_cache.py * Update cache.py * Update cosmos_db_cache.py * Update cosmos_db_cache.py * Update cosmos_db_cache.py * Update cosmos_db_cache.py * Update build.yml * Update build.yml * Update test_cosmos_db_cache.py * Update test_cosmos_db_cache.py * Update test_cosmos_db_cache.py * Update test_cosmos_db_cache.py * Update test_cosmos_db_cache.py * Update autogen/cache/cache_factory.py Co-authored-by: Chi Wang * Update cache_factory.py * Update cosmos_db_cache.py * Update cache.py * Update cache_factory.py * Update cosmos_db_cache.py * Update .github/workflows/build.yml Co-authored-by: Chi Wang * Update cache.py * Update cache.py * Update cache.py * Update cache_factory.py * Update cosmos_db_cache.py * Update cache.py * Update cache_factory.py * Update cosmos_db_cache.py * Update cache.py * Update cache_factory.py * Update cache_factory.py * Update cache.py * Update cosmos_db_cache.py * Update cache.py * Update cache.py * Update cache_factory.py * Update cache.py * Update cache_factory.py * Update cosmos_db_cache.py * Update cache.py * Update cache_factory.py * Update cosmos_db_cache.py * Update cache.py * Update cache_factory.py * Update cosmos_db_cache.py * Update test_cache.py * Update test_cache.py * Update test_cache.py * Update cache.py * Update cache.py * Update cache_factory.py * Update cache.py * Update cache_factory.py * Update test_cache.py * Update test_cache.py * Update cache.py * Update cache.py * Update test_cache.py * Update cache.py * Update cache.py * Update cache_factory.py * Update cache_factory.py * Update cache_factory.py * Update cache_factory.py * Update cache_factory.py * Update build.yml * Update test_cache.py * Update test_cosmos_db_cache.py * Update test_cache.py * Update cache.py * Update cache_factory.py * Update cosmos_db_cache.py * Update test_cache.py * Update test_cosmos_db_cache.py * Update test_cache.py * Update test_cosmos_db_cache.py * Update build.yml * Update build.yml * Update build.yml * Update build.yml * Update cache_factory.py * Update cache.py * Update cosmos_db_cache.py * Update cache.py * Update build.yml * Update test_cache.py * Update test_cache.py * Update test_cache.py * Update test_cache.py * Update cache_factory.py * Update cosmos_db_cache.py * Update test_cache.py * Update test_cache.py * Update test_cache.py * Update test_cache.py * Update test_cosmos_db_cache.py * Update cosmos_db_cache.py * Update test_cosmos_db_cache.py * Update cosmos_db_cache.py * Update cosmos_db_cache.py * Update test_cache.py * Update test_cosmos_db_cache.py * Update test_cosmos_db_cache.py * Update test_cosmos_db_cache.py * Update test_cosmos_db_cache.py * Update test_cosmos_db_cache.py * Update test_cosmos_db_cache.py * Update cosmos_db_cache.py * Update test_cache.py * Update test_cosmos_db_cache.py * Update cache.py * Update cache.py * Update cache.py * Update test_cosmos_db_cache.py * Update test_cosmos_db_cache.py * Update cache.py * Update test_cosmos_db_cache.py * Update cosmos_db_cache.py * Update cache.py * Update test_cosmos_db_cache.py * Update test_cosmos_db_cache.py --------- Co-authored-by: Chi Wang --- .github/workflows/build.yml | 8 +- autogen/cache/cache.py | 45 ++++++++- autogen/cache/cache_factory.py | 58 ++++++++---- autogen/cache/cosmos_db_cache.py | 144 +++++++++++++++++++++++++++++ test/cache/test_cache.py | 98 +++++++++++++++----- test/cache/test_cosmos_db_cache.py | 81 ++++++++++++++++ website/docs/topics/llm-caching.md | 8 +- 7 files changed, 391 insertions(+), 51 deletions(-) create mode 100644 autogen/cache/cosmos_db_cache.py create mode 100644 test/cache/test_cosmos_db_cache.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1cdc942afea..b45de32dddd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,7 @@ jobs: - name: Install packages and dependencies run: | python -m pip install --upgrade pip wheel - pip install -e . + pip install -e .[cosmosdb] python -c "import autogen" pip install pytest mock - name: Install optional dependencies for code executors @@ -67,12 +67,16 @@ jobs: if: matrix.python-version != '3.10' && matrix.os != 'ubuntu-latest' run: | pytest test --ignore=test/agentchat/contrib --skip-openai --skip-docker --durations=10 --durations-min=1.0 - - name: Coverage + - name: Coverage with Redis if: matrix.python-version == '3.10' run: | pip install -e .[test,redis,websockets] coverage run -a -m pytest test --ignore=test/agentchat/contrib --skip-openai --durations=10 --durations-min=1.0 coverage xml + - name: Test with Cosmos DB + run: | + pip install -e .[test,cosmosdb] + coverage run -a -m pytest test/cache/test_cosmos_db_cache.py --skip-openai --durations=10 --durations-min=1.0 - name: Upload coverage to Codecov if: matrix.python-version == '3.10' uses: codecov/codecov-action@v3 diff --git a/autogen/cache/cache.py b/autogen/cache/cache.py index 0770079f295..6a15d993ff6 100644 --- a/autogen/cache/cache.py +++ b/autogen/cache/cache.py @@ -2,7 +2,7 @@ import sys from types import TracebackType -from typing import Any, Dict, Optional, Type, Union +from typing import Any, Dict, Optional, Type, TypedDict, Union from .abstract_cache_base import AbstractCache from .cache_factory import CacheFactory @@ -26,7 +26,12 @@ class Cache(AbstractCache): cache: The cache instance created based on the provided configuration. """ - ALLOWED_CONFIG_KEYS = ["cache_seed", "redis_url", "cache_path_root"] + ALLOWED_CONFIG_KEYS = [ + "cache_seed", + "redis_url", + "cache_path_root", + "cosmos_db_config", + ] @staticmethod def redis(cache_seed: Union[str, int] = 42, redis_url: str = "redis://localhost:6379/0") -> "Cache": @@ -56,6 +61,32 @@ def disk(cache_seed: Union[str, int] = 42, cache_path_root: str = ".cache") -> " """ return Cache({"cache_seed": cache_seed, "cache_path_root": cache_path_root}) + @staticmethod + def cosmos_db( + connection_string: Optional[str] = None, + container_id: Optional[str] = None, + cache_seed: Union[str, int] = 42, + client: Optional[any] = None, + ) -> "Cache": + """ + Create a Cosmos DB cache instance with 'autogen_cache' as database ID. + + Args: + connection_string (str, optional): Connection string to the Cosmos DB account. + container_id (str, optional): The container ID for the Cosmos DB account. + cache_seed (Union[str, int], optional): A seed for the cache. + client: Optional[CosmosClient]: Pass an existing Cosmos DB client. + Returns: + Cache: A Cache instance configured for Cosmos DB. + """ + cosmos_db_config = { + "connection_string": connection_string, + "database_id": "autogen_cache", + "container_id": container_id, + "client": client, + } + return Cache({"cache_seed": str(cache_seed), "cosmos_db_config": cosmos_db_config}) + def __init__(self, config: Dict[str, Any]): """ Initialize the Cache with the given configuration. @@ -69,15 +100,19 @@ def __init__(self, config: Dict[str, Any]): ValueError: If an invalid configuration key is provided. """ self.config = config + # Ensure that the seed is always treated as a string before being passed to any cache factory or stored. + self.config["cache_seed"] = str(self.config.get("cache_seed", 42)) + # validate config for key in self.config.keys(): if key not in self.ALLOWED_CONFIG_KEYS: raise ValueError(f"Invalid config key: {key}") # create cache instance self.cache = CacheFactory.cache_factory( - self.config.get("cache_seed", "42"), - self.config.get("redis_url", None), - self.config.get("cache_path_root", None), + seed=self.config["cache_seed"], + redis_url=self.config.get("redis_url"), + cache_path_root=self.config.get("cache_path_root"), + cosmosdb_config=self.config.get("cosmos_db_config"), ) def __enter__(self) -> "Cache": diff --git a/autogen/cache/cache_factory.py b/autogen/cache/cache_factory.py index 8fc4713f06e..437893570b4 100644 --- a/autogen/cache/cache_factory.py +++ b/autogen/cache/cache_factory.py @@ -1,5 +1,5 @@ import logging -from typing import Optional, Union +from typing import Any, Dict, Optional, Union from .abstract_cache_base import AbstractCache from .disk_cache import DiskCache @@ -8,25 +8,28 @@ class CacheFactory: @staticmethod def cache_factory( - seed: Union[str, int], redis_url: Optional[str] = None, cache_path_root: str = ".cache" + seed: Union[str, int], + redis_url: Optional[str] = None, + cache_path_root: str = ".cache", + cosmosdb_config: Optional[Dict[str, Any]] = None, ) -> AbstractCache: """ Factory function for creating cache instances. - Based on the provided redis_url, this function decides whether to create a RedisCache - or DiskCache instance. If RedisCache is available and redis_url is provided, - a RedisCache instance is created. Otherwise, a DiskCache instance is used. + This function decides whether to create a RedisCache, DiskCache, or CosmosDBCache instance + based on the provided parameters. If RedisCache is available and a redis_url is provided, + a RedisCache instance is created. If connection_string, database_id, and container_id + are provided, a CosmosDBCache is created. Otherwise, a DiskCache instance is used. Args: - seed (Union[str, int]): A string or int used as a seed or namespace for the cache. - This could be useful for creating distinct cache instances - or for namespacing keys in the cache. - redis_url (str or None): The URL for the Redis server. If this is None - or if RedisCache is not available, a DiskCache instance is created. + seed (Union[str, int]): Used as a seed or namespace for the cache. + redis_url (Optional[str]): URL for the Redis server. + cache_path_root (str): Root path for the disk cache. + cosmosdb_config (Optional[Dict[str, str]]): Dictionary containing 'connection_string', + 'database_id', and 'container_id' for Cosmos DB cache. Returns: - An instance of either RedisCache or DiskCache, depending on the availability of RedisCache - and the provided redis_url. + An instance of RedisCache, DiskCache, or CosmosDBCache. Examples: @@ -40,14 +43,35 @@ def cache_factory( ```python disk_cache = cache_factory("myseed", None) ``` + + Creating a Cosmos DB cache: + ```python + cosmos_cache = cache_factory("myseed", cosmosdb_config={ + "connection_string": "your_connection_string", + "database_id": "your_database_id", + "container_id": "your_container_id"} + ) + ``` + """ - if redis_url is not None: + if redis_url: try: from .redis_cache import RedisCache return RedisCache(seed, redis_url) except ImportError: - logging.warning("RedisCache is not available. Creating a DiskCache instance instead.") - return DiskCache(f"./{cache_path_root}/{seed}") - else: - return DiskCache(f"./{cache_path_root}/{seed}") + logging.warning( + "RedisCache is not available. Checking other cache options. The last fallback is DiskCache." + ) + + if cosmosdb_config: + try: + from .cosmos_db_cache import CosmosDBCache + + return CosmosDBCache.create_cache(seed, cosmosdb_config) + + except ImportError: + logging.warning("CosmosDBCache is not available. Fallback to DiskCache.") + + # Default to DiskCache if neither Redis nor Cosmos DB configurations are provided + return DiskCache(f"./{cache_path_root}/{seed}") diff --git a/autogen/cache/cosmos_db_cache.py b/autogen/cache/cosmos_db_cache.py new file mode 100644 index 00000000000..b85be923c2f --- /dev/null +++ b/autogen/cache/cosmos_db_cache.py @@ -0,0 +1,144 @@ +# Install Azure Cosmos DB SDK if not already + +import pickle +from typing import Any, Optional, TypedDict, Union + +from azure.cosmos import CosmosClient, PartitionKey, exceptions +from azure.cosmos.exceptions import CosmosResourceNotFoundError + +from autogen.cache.abstract_cache_base import AbstractCache + + +class CosmosDBConfig(TypedDict, total=False): + connection_string: str + database_id: str + container_id: str + cache_seed: Optional[Union[str, int]] + client: Optional[CosmosClient] + + +class CosmosDBCache(AbstractCache): + """ + Synchronous implementation of AbstractCache using Azure Cosmos DB NoSQL API. + + This class provides a concrete implementation of the AbstractCache + interface using Azure Cosmos DB for caching data, with synchronous operations. + + Attributes: + seed (Union[str, int]): A seed or namespace used as a partition key. + client (CosmosClient): The Cosmos DB client used for caching. + container: The container instance used for caching. + """ + + def __init__(self, seed: Union[str, int], cosmosdb_config: CosmosDBConfig): + """ + Initialize the CosmosDBCache instance. + + Args: + seed (Union[str, int]): A seed or namespace for the cache, used as a partition key. + connection_string (str): The connection string for the Cosmos DB account. + container_id (str): The container ID to be used for caching. + client (Optional[CosmosClient]): An existing CosmosClient instance to be used for caching. + """ + self.seed = str(seed) + self.client = cosmosdb_config.get("client") or CosmosClient.from_connection_string( + cosmosdb_config["connection_string"] + ) + database_id = cosmosdb_config.get("database_id", "autogen_cache") + self.database = self.client.get_database_client(database_id) + container_id = cosmosdb_config.get("container_id") + self.container = self.database.create_container_if_not_exists( + id=container_id, partition_key=PartitionKey(path="/partitionKey") + ) + + @classmethod + def create_cache(cls, seed: Union[str, int], cosmosdb_config: CosmosDBConfig): + """ + Factory method to create a CosmosDBCache instance based on the provided configuration. + This method decides whether to use an existing CosmosClient or create a new one. + """ + if "client" in cosmosdb_config and isinstance(cosmosdb_config["client"], CosmosClient): + return cls.from_existing_client(seed, **cosmosdb_config) + else: + return cls.from_config(seed, cosmosdb_config) + + @classmethod + def from_config(cls, seed: Union[str, int], cosmosdb_config: CosmosDBConfig): + return cls(str(seed), cosmosdb_config) + + @classmethod + def from_connection_string(cls, seed: Union[str, int], connection_string: str, database_id: str, container_id: str): + config = {"connection_string": connection_string, "database_id": database_id, "container_id": container_id} + return cls(str(seed), config) + + @classmethod + def from_existing_client(cls, seed: Union[str, int], client: CosmosClient, database_id: str, container_id: str): + config = {"client": client, "database_id": database_id, "container_id": container_id} + return cls(str(seed), config) + + def get(self, key: str, default: Optional[Any] = None) -> Optional[Any]: + """ + Retrieve an item from the Cosmos DB cache. + + Args: + key (str): The key identifying the item in the cache. + default (optional): The default value to return if the key is not found. + + Returns: + The deserialized value associated with the key if found, else the default value. + """ + try: + response = self.container.read_item(item=key, partition_key=str(self.seed)) + return pickle.loads(response["data"]) + except CosmosResourceNotFoundError: + return default + except Exception as e: + # Log the exception or rethrow after logging if needed + # Consider logging or handling the error appropriately here + raise e + + def set(self, key: str, value: Any) -> None: + """ + Set an item in the Cosmos DB cache. + + Args: + key (str): The key under which the item is to be stored. + value: The value to be stored in the cache. + + Notes: + The value is serialized using pickle before being stored. + """ + try: + serialized_value = pickle.dumps(value) + item = {"id": key, "partitionKey": str(self.seed), "data": serialized_value} + self.container.upsert_item(item) + except Exception as e: + # Log or handle exception + raise e + + def close(self) -> None: + """ + Close the Cosmos DB client. + + Perform any necessary cleanup, such as closing network connections. + """ + # CosmosClient doesn"t require explicit close in the current SDK + # If you created the client inside this class, you should close it if necessary + pass + + def __enter__(self): + """ + Context management entry. + + Returns: + self: The instance itself. + """ + return self + + def __exit__(self, exc_type: Optional[type], exc_value: Optional[Exception], traceback: Optional[Any]) -> None: + """ + Context management exit. + + Perform cleanup actions such as closing the Cosmos DB client. + """ + self.close() diff --git a/test/cache/test_cache.py b/test/cache/test_cache.py index 45043ccc9e7..d01b1cf4952 100755 --- a/test/cache/test_cache.py +++ b/test/cache/test_cache.py @@ -1,55 +1,103 @@ #!/usr/bin/env python3 -m pytest import unittest -from unittest.mock import MagicMock, patch +from typing import Optional, TypedDict, Union +from unittest.mock import ANY, MagicMock, patch + +try: + from azure.cosmos import CosmosClient +except ImportError: + CosmosClient = None from autogen.cache.cache import Cache +from autogen.cache.cosmos_db_cache import CosmosDBConfig class TestCache(unittest.TestCase): def setUp(self): - self.config = {"cache_seed": "test_seed", "redis_url": "redis://test", "cache_path_root": ".test_cache"} + self.redis_config = { + "cache_seed": "test_seed", + "redis_url": "redis://test", + "cache_path_root": ".test_cache", + } + self.cosmos_config = { + "cosmos_db_config": { + "connection_string": "AccountEndpoint=https://example.documents.azure.com:443/;", + "database_id": "autogen_cache", + "container_id": "TestContainer", + "cache_seed": "42", + "client": MagicMock(spec=CosmosClient), + } + } @patch("autogen.cache.cache_factory.CacheFactory.cache_factory", return_value=MagicMock()) - def test_init(self, mock_cache_factory): - cache = Cache(self.config) + def test_redis_cache_initialization(self, mock_cache_factory): + cache = Cache(self.redis_config) self.assertIsInstance(cache.cache, MagicMock) - mock_cache_factory.assert_called_with("test_seed", "redis://test", ".test_cache") + mock_cache_factory.assert_called() @patch("autogen.cache.cache_factory.CacheFactory.cache_factory", return_value=MagicMock()) - def test_context_manager(self, mock_cache_factory): + def test_cosmosdb_cache_initialization(self, mock_cache_factory): + cache = Cache(self.cosmos_config) + self.assertIsInstance(cache.cache, MagicMock) + mock_cache_factory.assert_called_with( + seed="42", + redis_url=None, + cache_path_root=None, + cosmosdb_config={ + "connection_string": "AccountEndpoint=https://example.documents.azure.com:443/;", + "database_id": "autogen_cache", + "container_id": "TestContainer", + "cache_seed": "42", + "client": ANY, + }, + ) + + def context_manager_common(self, config): mock_cache_instance = MagicMock() - mock_cache_factory.return_value = mock_cache_instance + with patch("autogen.cache.cache_factory.CacheFactory.cache_factory", return_value=mock_cache_instance): + with Cache(config) as cache: + self.assertIsInstance(cache, MagicMock) - with Cache(self.config) as cache: - self.assertIsInstance(cache, MagicMock) + mock_cache_instance.__enter__.assert_called() + mock_cache_instance.__exit__.assert_called() - mock_cache_instance.__enter__.assert_called() - mock_cache_instance.__exit__.assert_called() + def test_redis_context_manager(self): + self.context_manager_common(self.redis_config) - @patch("autogen.cache.cache_factory.CacheFactory.cache_factory", return_value=MagicMock()) - def test_get_set(self, mock_cache_factory): + def test_cosmos_context_manager(self): + self.context_manager_common(self.cosmos_config) + + def get_set_common(self, config): key = "key" value = "value" mock_cache_instance = MagicMock() - mock_cache_factory.return_value = mock_cache_instance + with patch("autogen.cache.cache_factory.CacheFactory.cache_factory", return_value=mock_cache_instance): + cache = Cache(config) + cache.set(key, value) + cache.get(key) - cache = Cache(self.config) - cache.set(key, value) - cache.get(key) + mock_cache_instance.set.assert_called_with(key, value) + mock_cache_instance.get.assert_called_with(key, None) - mock_cache_instance.set.assert_called_with(key, value) - mock_cache_instance.get.assert_called_with(key, None) + def test_redis_get_set(self): + self.get_set_common(self.redis_config) - @patch("autogen.cache.cache_factory.CacheFactory.cache_factory", return_value=MagicMock()) - def test_close(self, mock_cache_factory): + def test_cosmos_get_set(self): + self.get_set_common(self.cosmos_config) + + def close_common(self, config): mock_cache_instance = MagicMock() - mock_cache_factory.return_value = mock_cache_instance + with patch("autogen.cache.cache_factory.CacheFactory.cache_factory", return_value=mock_cache_instance): + cache = Cache(config) + cache.close() + mock_cache_instance.close.assert_called() - cache = Cache(self.config) - cache.close() + def test_redis_close(self): + self.close_common(self.redis_config) - mock_cache_instance.close.assert_called() + def test_cosmos_close(self): + self.close_common(self.cosmos_config) if __name__ == "__main__": diff --git a/test/cache/test_cosmos_db_cache.py b/test/cache/test_cosmos_db_cache.py new file mode 100644 index 00000000000..f89a4c96cf4 --- /dev/null +++ b/test/cache/test_cosmos_db_cache.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 -m pytest + +import pickle +import unittest +from unittest.mock import MagicMock, patch + +from azure.cosmos.exceptions import CosmosResourceNotFoundError + +from autogen.cache.cosmos_db_cache import CosmosDBCache + + +class TestCosmosDBCache(unittest.TestCase): + def setUp(self): + self.seed = "42" + self.connection_string = "AccountEndpoint=https://example.documents.azure.com:443/;" + self.database_id = "autogen_cache" + self.container_id = "TestContainer" + self.client = MagicMock() + + @patch("autogen.cache.cosmos_db_cache.CosmosClient.from_connection_string", return_value=MagicMock()) + def test_init(self, mock_from_connection_string): + cache = CosmosDBCache.from_connection_string( + self.seed, self.connection_string, self.database_id, self.container_id + ) + self.assertEqual(cache.seed, self.seed) + mock_from_connection_string.assert_called_with(self.connection_string) + + def test_get(self): + key = "key" + value = "value" + serialized_value = pickle.dumps(value) + cache = CosmosDBCache( + self.seed, + { + "connection_string": self.connection_string, + "database_id": self.database_id, + "container_id": self.container_id, + "client": self.client, + }, + ) + cache.container.read_item.return_value = {"data": serialized_value} + self.assertEqual(cache.get(key), value) + cache.container.read_item.assert_called_with(item=key, partition_key=str(self.seed)) + + cache.container.read_item.side_effect = CosmosResourceNotFoundError(status_code=404, message="Item not found") + self.assertIsNone(cache.get(key, default=None)) + + def test_set(self): + key = "key" + value = "value" + serialized_value = pickle.dumps(value) + cache = CosmosDBCache( + self.seed, + { + "connection_string": self.connection_string, + "database_id": self.database_id, + "container_id": self.container_id, + "client": self.client, + }, + ) + cache.set(key, value) + expected_item = {"id": key, "partitionKey": str(self.seed), "data": serialized_value} + cache.container.upsert_item.assert_called_with(expected_item) + + def test_context_manager(self): + with patch("autogen.cache.cosmos_db_cache.CosmosDBCache.close", MagicMock()) as mock_close: + with CosmosDBCache( + self.seed, + { + "connection_string": self.connection_string, + "database_id": self.database_id, + "container_id": self.container_id, + "client": self.client, + }, + ) as cache: + self.assertIsInstance(cache, CosmosDBCache) + mock_close.assert_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/website/docs/topics/llm-caching.md b/website/docs/topics/llm-caching.md index 870fdad56b5..3d14fe49f51 100644 --- a/website/docs/topics/llm-caching.md +++ b/website/docs/topics/llm-caching.md @@ -3,8 +3,7 @@ AutoGen supports caching API requests so that they can be reused when the same request is issued. This is useful when repeating or continuing experiments for reproducibility and cost saving. Since version [`0.2.8`](https://github.com/microsoft/autogen/releases/tag/v0.2.8), a configurable context manager allows you to easily -configure LLM cache, using either [`DiskCache`](/docs/reference/cache/disk_cache#diskcache) or [`RedisCache`](/docs/reference/cache/redis_cache#rediscache). All agents inside the -context manager will use the same cache. +configure LLM cache, using either [`DiskCache`](/docs/reference/cache/disk_cache#diskcache), [`RedisCache`](/docs/reference/cache/redis_cache#rediscache), or Cosmos DB Cache. All agents inside the context manager will use the same cache. ```python from autogen import Cache @@ -16,6 +15,11 @@ with Cache.redis(redis_url="redis://localhost:6379/0") as cache: # Use DiskCache as cache with Cache.disk() as cache: user.initiate_chat(assistant, message=coding_task, cache=cache) + +# Use Azure Cosmos DB as cache +with Cache.cosmos_db(connection_string="your_connection_string", database_id="your_database_id", container_id="your_container_id") as cache: + user.initiate_chat(assistant, message=coding_task, cache=cache) + ``` The cache can also be passed directly to the model client's create call. From 600bd3f2fe0596d263e30ce6821ce8260d15dd43 Mon Sep 17 00:00:00 2001 From: Xiaoyun Zhang Date: Fri, 26 Apr 2024 09:21:46 -0700 Subject: [PATCH 12/30] Bring Dotnet AutoGen (#924) * update readme * update * update * update * update * update * update * add sample project * revert notebook change back * update * update interactive version * add nuget package * refactor Message * update example * add azure nightly build pipeline * Set up CI with Azure Pipelines [skip ci] * Update nightly-build.yml for Azure Pipelines * add dotnet interactive package * add dotnet interactive package * update pipeline * add nuget feed back * remove dotnet-tool feed * remove dotnet-tool feed comment * update pipeline * update build name * Update nightly-build.yml * Delete .github/workflows/dotnet-ci.yml * update * add working_dir to use step * add initateChat api * update oai package * Update dotnet-build.yml * Update dotnet-run-openai-test-and-notebooks.yml * update build workflow * update build workflow * update nuget feed * update nuget feed * update aoai and sk version * Update InteractiveService.cs * add support for GPT 4V * add DalleAndGPT4V example * update example * add user proxy agent * add readme * bump version * update example * add dotnet interactive hook * update * udpate tests * add website * update index.md * add docs * update doc * move sk dependency out of core package * udpate doc * Update Use-function-call.md * add type safe function call document * update doc * update doc * add dock * Update Use-function-call.md * add GenerateReplyOptions * remove IChatLLM * update version * update doc * update website * add sample * fix link * add middleware agent * clean up doc * bump version * update doc * update * add Other Language * remove warnings * add sign.props * add sign step * fix pipelien * auth * real sign * disable PR trigger * update * disable PR trigger * use microbuild machine * update build pipeline to add publish to internal feed * add internal feed * fix build pipeline * add dotnet prefix * update ci * add build number * update run number * update source * update token * update * remove adding source * add publish to github package * try again * try again * ask for write pacakge * disable package when branch is not main * update * implement streaming agent * add test for streaming function call * update * fix #1588 * enable PR check for dotnet branch * add website readme * only publish to dotnet feed when pushing to dotnet branch * remove openai-test-and-notebooks workflow * update readme * update readme * update workflow * update getting-start * upgrade test and sample proejct to use .net 8 * fix global.json format && make loadFromConfig API internal only before implementing * update * add support for LM studio * add doc * Update README.md * add push and workflow_dispatch trigger * disable PR for main * add dotnet env * Update Installation.md * add nuget * refer to newtonsoft 13 * update branch to dotnet in docfx * Update Installation.md * pull out HumanInputMiddleware and FunctionCallMiddleware * fix tests * add link to sample folder * refactor message * refactor over IMessage * add more tests * add more test * fix build error * rename header * add semantic kernel project * update sk example * update dotnet version * add LMStudio function call example * rename LLaMAFunctin * remove dotnet run openai test and notebook workflow * add FunctionContract and test * update doc * add documents * add workflow * update * update sample * fix warning in test * reult length can be less then maximumOutputToKeep (#1804) * merge with main * add option to retrieve inner agent and middlewares from MiddlewareAgent * update doc * adjust namespace * update readme * fix test * use IMessage * more updates * update * fix test * add comments * use FunctionContract to replace FunctionDefinition * move AutoGen contrac to AutoGen.Core * update installation * refactor streamingAgent by adding StreamingMessage type * update sample * update samples * update * update * add test * fix test * bump version * add openaichat test * update * Update Example03_Agent_FunctionCall.cs * [.Net] improve docs (#1862) * add doc * add doc * add doc * add doc * add doc * add doc * update * fix test error * fix some error * fix test * fix test * add more tests * edits --------- Co-authored-by: ekzhu * [.Net] Add fill form example (#1911) * add form filler example * update * fix ci error * [.Net] Add using AutoGen.Core in source generator (#1983) * fix using namespace bug in source generator * remove using in sourcegenerator test * disable PR test * Add .idea to .gitignore (#1988) * [.Net] publish to nuget.org feed (#1987) * publish to nuget * update ci * update dotnet-release * update release pipeline * add source * remove empty symbol package * update pipeline * remove tag * update installation guide * [.Net] Rename some classes && APIs based on doc review (#1980) * rename sequential group chat to round robin group chat * rename to sendInstruction * rename workflow to graph * rename some api * bump version * move Graph to GroupChat folder * rename fill application example * [.Net] Improve package description (#2161) * add discord link and update package description * Update getting-start.md * [.Net] Fix document comment from the most recent AutoGen.Net engineer sync (#2231) * update * rename RegisterPrintMessageHook to RegisterPrintMessage * update website * update update.md * fix link error * [.Net] Enable JsonMode and deterministic output in AutoGen.OpenAI OpenAIChatAgent (#2347) * update openai version && add sample for json output * add example in web * update update.md * update image url * [.Net] Add AutoGen.Mistral package (#2330) * add mstral client * enable streaming support * add mistralClientAgent * add test for function call * add extension * add support for toolcall and toolcall result message * add support for aggregate message * implement streaming function call * track (#2471) * [.Net] add mistral example (#2482) * update existing examples to use messageCOnnector * add overview * add function call document * add example 14 * add mistral token count usage example * update version * Update dotnet-release.yml (#2488) * update * revert gitattributes --------- Co-authored-by: mhensen Co-authored-by: ekzhu Co-authored-by: Krzysztof Kasprowicz <60486987+Krzysztof318@users.noreply.github.com> --- .github/workflows/dotnet-build.yml | 3 +- .github/workflows/dotnet-release.yml | 14 +- dotnet/.config/dotnet-tools.json | 26 +- dotnet/.editorconfig | 178 +++++++ dotnet/.gitignore | 30 ++ dotnet/AutoGen.sln | 111 +++++ dotnet/Directory.Build.props | 23 + dotnet/NuGet.config | 10 + dotnet/README.md | 103 ++++ dotnet/eng/MetaInfo.props | 12 + dotnet/eng/Sign.props | 22 + dotnet/eng/Version.props | 17 + dotnet/eng/opensource.snk | Bin 0 -> 596 bytes dotnet/global.json | 6 + dotnet/nuget/NUGET.md | 8 + dotnet/nuget/icon.png | 3 + dotnet/nuget/nuget-package.props | 54 +++ .../AutoGen.BasicSample.csproj | 19 + .../CodeSnippet/AgentCodeSnippet.cs | 31 ++ .../CodeSnippet/BuildInMessageCodeSnippet.cs | 42 ++ .../CodeSnippet/CreateAnAgent.cs | 142 ++++++ .../CodeSnippet/FunctionCallCodeSnippet.cs | 149 ++++++ .../CodeSnippet/GetStartCodeSnippet.cs | 41 ++ .../CodeSnippet/MiddlewareAgentCodeSnippet.cs | 169 +++++++ .../CodeSnippet/MistralAICodeSnippet.cs | 86 ++++ .../CodeSnippet/OpenAICodeSnippet.cs | 136 ++++++ .../PrintMessageMiddlewareCodeSnippet.cs | 44 ++ .../CodeSnippet/RunCodeSnippetCodeSnippet.cs | 48 ++ .../CodeSnippet/SemanticKernelCodeSnippet.cs | 102 ++++ .../TypeSafeFunctionCallCodeSnippet.cs | 121 +++++ .../CodeSnippet/UserProxyAgentCodeSnippet.cs | 20 + .../Example01_AssistantAgent.cs | 46 ++ .../Example02_TwoAgent_MathChat.cs | 79 ++++ .../Example03_Agent_FunctionCall.cs | 96 ++++ ...Example04_Dynamic_GroupChat_Coding_Task.cs | 263 +++++++++++ .../Example05_Dalle_And_GPT4V.cs | 152 ++++++ .../Example06_UserProxyAgent.cs | 32 ++ ...7_Dynamic_GroupChat_Calculate_Fibonacci.cs | 377 +++++++++++++++ .../Example08_LMStudio.cs | 44 ++ .../Example09_LMStudio_FunctionCall.cs | 135 ++++++ .../Example10_SemanticKernel.cs | 80 ++++ .../Example11_Sequential_GroupChat_Example.cs | 94 ++++ .../Example12_TwoAgent_Fill_Application.cs | 199 ++++++++ .../Example13_OpenAIAgent_JsonMode.cs | 67 +++ ...Example14_MistralClientAgent_TokenCount.cs | 65 +++ .../AutoGen.BasicSamples/GlobalUsing.cs | 3 + .../AutoGen.BasicSamples/LLMConfiguration.cs | 40 ++ dotnet/sample/AutoGen.BasicSamples/Program.cs | 5 + .../AutoGen.Core/Agent/DefaultReplyAgent.cs | 31 ++ .../AutoGen.Core/Agent/GroupChatManager.cs | 34 ++ dotnet/src/AutoGen.Core/Agent/IAgent.cs | 50 ++ .../AutoGen.Core/Agent/IMiddlewareAgent.cs | 50 ++ .../src/AutoGen.Core/Agent/IStreamingAgent.cs | 19 + .../src/AutoGen.Core/Agent/MiddlewareAgent.cs | 136 ++++++ .../Agent/MiddlewareStreamingAgent.cs | 124 +++++ dotnet/src/AutoGen.Core/AutoGen.Core.csproj | 21 + .../AutoGen.Core/Extension/AgentExtension.cs | 174 +++++++ .../Extension/GroupChatExtension.cs | 109 +++++ .../Extension/MessageExtension.cs | 213 +++++++++ .../Extension/MiddlewareExtension.cs | 138 ++++++ .../PrintMessageMiddlewareExtension.cs | 69 +++ .../Extension/StreamingMiddlewareExtension.cs | 114 +++++ .../Function/FunctionAttribute.cs | 93 ++++ dotnet/src/AutoGen.Core/GroupChat/Graph.cs | 117 +++++ .../src/AutoGen.Core/GroupChat/GroupChat.cs | 183 +++++++ .../GroupChat/RoundRobinGroupChat.cs | 100 ++++ dotnet/src/AutoGen.Core/IGroupChat.cs | 22 + dotnet/src/AutoGen.Core/ILLMConfig.cs | 8 + .../AutoGen.Core/Message/AggregateMessage.cs | 53 +++ dotnet/src/AutoGen.Core/Message/IMessage.cs | 52 ++ .../src/AutoGen.Core/Message/ImageMessage.cs | 34 ++ dotnet/src/AutoGen.Core/Message/Message.cs | 53 +++ .../AutoGen.Core/Message/MessageEnvelope.cs | 37 ++ .../AutoGen.Core/Message/MultiModalMessage.cs | 58 +++ dotnet/src/AutoGen.Core/Message/Role.cs | 54 +++ .../src/AutoGen.Core/Message/TextMessage.cs | 63 +++ .../AutoGen.Core/Message/ToolCallMessage.cs | 108 +++++ .../Message/ToolCallResultMessage.cs | 56 +++ .../Middleware/DelegateMiddleware.cs | 45 ++ .../Middleware/DelegateStreamingMiddleware.cs | 38 ++ .../Middleware/FunctionCallMiddleware.cs | 178 +++++++ .../AutoGen.Core/Middleware/IMiddleware.cs | 26 + .../Middleware/IStreamingMiddleware.cs | 21 + .../Middleware/MiddlewareContext.cs | 27 ++ .../Middleware/PrintMessageMiddleware.cs | 87 ++++ .../AutoGen.DotnetInteractive.csproj | 40 ++ .../DotnetInteractiveFunction.cs | 278 +++++++++++ .../Extension/AgentExtension.cs | 83 ++++ .../AutoGen.DotnetInteractive/GlobalUsing.cs | 4 + .../InteractiveService.cs | 261 ++++++++++ .../RestoreInteractive.config | 9 + dotnet/src/AutoGen.DotnetInteractive/Utils.cs | 86 ++++ .../dotnet-tools.json | 12 + .../AutoGen.LMStudio/AutoGen.LMStudio.csproj | 23 + dotnet/src/AutoGen.LMStudio/GlobalUsing.cs | 4 + dotnet/src/AutoGen.LMStudio/LMStudioAgent.cs | 80 ++++ dotnet/src/AutoGen.LMStudio/LMStudioConfig.cs | 25 + dotnet/src/AutoGen.LMStudio/README.md | 31 ++ .../Agent/MistralClientAgent.cs | 133 ++++++ .../AutoGen.Mistral/AutoGen.Mistral.csproj | 27 ++ .../JsonPropertyNameEnumConverter.cs | 43 ++ .../DTOs/ChatCompletionRequest.cs | 116 +++++ .../DTOs/ChatCompletionResponse.cs | 47 ++ .../src/AutoGen.Mistral/DTOs/ChatMessage.cs | 96 ++++ dotnet/src/AutoGen.Mistral/DTOs/Choice.cs | 58 +++ dotnet/src/AutoGen.Mistral/DTOs/Error.cs | 36 ++ .../src/AutoGen.Mistral/DTOs/ErrorResponse.cs | 19 + .../DTOs/FunctionDefinition.cs | 26 + dotnet/src/AutoGen.Mistral/DTOs/Model.cs | 61 +++ .../AutoGen.Mistral/DTOs/ResponseFormat.cs | 12 + dotnet/src/AutoGen.Mistral/DTOs/Tool.cs | 51 ++ dotnet/src/AutoGen.Mistral/DTOs/Usage.cs | 26 + .../Extension/FunctionContractExtension.cs | 59 +++ .../Extension/MistralAgentExtension.cs | 40 ++ .../Middleware/MistralChatMessageConnector.cs | 324 +++++++++++++ .../src/AutoGen.Mistral/MistralAIModelID.cs | 14 + dotnet/src/AutoGen.Mistral/MistralClient.cs | 168 +++++++ dotnet/src/AutoGen.OpenAI/Agent/GPTAgent.cs | 119 +++++ .../AutoGen.OpenAI/Agent/OpenAIChatAgent.cs | 158 +++++++ .../src/AutoGen.OpenAI/AutoGen.OpenAI.csproj | 25 + .../src/AutoGen.OpenAI/AzureOpenAIConfig.cs | 23 + .../Extension/FunctionContractExtension.cs | 63 +++ .../Extension/MessageExtension.cs | 228 +++++++++ .../Extension/OpenAIAgentExtension.cs | 37 ++ dotnet/src/AutoGen.OpenAI/GlobalUsing.cs | 4 + .../OpenAIChatRequestMessageConnector.cs | 445 +++++++++++++++++ dotnet/src/AutoGen.OpenAI/OpenAIConfig.cs | 17 + .../AutoGen.SemanticKernel.csproj | 27 ++ .../Extension/KernelExtension.cs | 14 + .../Extension/SemanticKernelAgentExtension.cs | 37 ++ .../src/AutoGen.SemanticKernel/GlobalUsing.cs | 4 + ...manticKernelChatMessageContentConnector.cs | 260 ++++++++++ .../SemanticKernelAgent.cs | 125 +++++ .../AutoGen.SourceGenerator.csproj | 60 +++ .../DocumentCommentExtension.cs | 295 ++++++++++++ .../FunctionCallGenerator.cs | 248 ++++++++++ .../FunctionContract.cs | 40 ++ .../FunctionExtension.cs | 32 ++ dotnet/src/AutoGen.SourceGenerator/README.md | 113 +++++ .../Template/FunctionCallTemplate.cs | 447 ++++++++++++++++++ .../Template/FunctionCallTemplate.tt | 116 +++++ dotnet/src/AutoGen/API/LLMConfigAPI.cs | 50 ++ dotnet/src/AutoGen/Agent/AssistantAgent.cs | 30 ++ dotnet/src/AutoGen/Agent/ConversableAgent.cs | 156 ++++++ dotnet/src/AutoGen/Agent/UserProxyAgent.cs | 30 ++ dotnet/src/AutoGen/AutoGen.csproj | 31 ++ dotnet/src/AutoGen/ConversableAgentConfig.cs | 17 + dotnet/src/AutoGen/GlobalUsing.cs | 4 + .../Middleware/HumanInputMiddleware.cs | 97 ++++ dotnet/test/.editorconfig | 7 + .../AutoGen.Mistral.Tests.csproj | 26 + .../MistralClientAgentTests.cs | 237 ++++++++++ .../MistralClientTests.cs | 287 +++++++++++ .../FunctionExample.Add_Test.approved.txt | 21 + ...ample.DictionaryToString_Test.approved.txt | 19 + .../FunctionExample.Query_Test.approved.txt | 24 + .../FunctionExample.Sum_Test.approved.txt | 19 + .../AutoGen.SourceGenerator.Tests.csproj | 24 + .../FilescopeNamespaceFunctionExample.cs | 14 + .../FunctionExample.test.cs | 130 +++++ .../FunctionExamples.cs | 70 +++ .../TopLevelStatementFunctionExample.cs | 13 + ...MessageTests.BasicMessageTest.approved.txt | 219 +++++++++ .../EnvironmentSpecificFactAttribute.cs | 33 ++ .../AutoGen.Tests/Attribute/OpenAIFact.cs | 26 + .../test/AutoGen.Tests/AutoGen.Tests.csproj | 24 + dotnet/test/AutoGen.Tests/BasicSampleTest.cs | 85 ++++ dotnet/test/AutoGen.Tests/EchoAgent.cs | 31 ++ dotnet/test/AutoGen.Tests/GlobalUsing.cs | 4 + dotnet/test/AutoGen.Tests/MathClassTest.cs | 242 ++++++++++ .../test/AutoGen.Tests/MiddlewareAgentTest.cs | 105 ++++ dotnet/test/AutoGen.Tests/MiddlewareTest.cs | 126 +++++ .../test/AutoGen.Tests/OpenAIChatAgentTest.cs | 238 ++++++++++ .../test/AutoGen.Tests/OpenAIMessageTests.cs | 377 +++++++++++++++ .../AutoGen.Tests/RegisterReplyAgentTest.cs | 27 ++ .../AutoGen.Tests/SemanticKernelAgentTest.cs | 134 ++++++ dotnet/test/AutoGen.Tests/SingleAgentTest.cs | 325 +++++++++++++ dotnet/test/AutoGen.Tests/TwoAgentTest.cs | 106 +++++ dotnet/test/AutoGen.Tests/WorkflowTest.cs | 70 +++ dotnet/website/.gitignore | 12 + dotnet/website/README.md | 13 + dotnet/website/articles/Agent-overview.md | 44 ++ .../articles/AutoGen-Mistral-Overview.md | 26 + .../articles/AutoGen-OpenAI-Overview.md | 17 + .../AutoGen-SemanticKernel-Overview.md | 17 + dotnet/website/articles/Built-in-messages.md | 34 ++ .../Consume-LLM-server-from-LM-Studio.md | 20 + .../articles/Create-a-user-proxy-agent.md | 16 + dotnet/website/articles/Create-an-agent.md | 11 + .../Create-type-safe-function-call.md | 41 ++ .../website/articles/Create-your-own-agent.md | 1 + .../articles/Create-your-own-middleware.md | 1 + .../articles/Function-call-middleware.md | 1 + .../articles/Function-call-overview.md | 52 ++ .../website/articles/Group-chat-overview.md | 8 + dotnet/website/articles/Group-chat.md | 73 +++ dotnet/website/articles/Installation.md | 63 +++ .../website/articles/Middleware-overview.md | 27 ++ .../MistralChatAgent-count-token-usage.md | 28 ++ .../MistralChatAgent-use-function-call.md | 41 ++ .../articles/OpenAIChatAgent-simple-chat.md | 11 + .../OpenAIChatAgent-support-more-messages.md | 6 + .../OpenAIChatAgent-use-function-call.md | 33 ++ .../articles/OpenAIChatAgent-use-json-mode.md | 31 ++ .../articles/Print-message-middleware.md | 27 ++ dotnet/website/articles/Roundrobin-chat.md | 33 ++ dotnet/website/articles/Run-dotnet-code.md | 32 ++ .../SemanticKernelAgent-simple-chat.md | 9 + ...manticKernelAgent-support-more-messages.md | 10 + dotnet/website/articles/Two-agent-chat.md | 19 + dotnet/website/articles/Use-function-call.md | 43 ++ .../articles/Use-graph-in-group-chat.md | 25 + dotnet/website/articles/getting-start.md | 24 + dotnet/website/articles/toc.yml | 91 ++++ dotnet/website/docfx.json | 68 +++ dotnet/website/filterConfig.yml | 3 + dotnet/website/images/ag.ico | Bin 0 -> 3126 bytes dotnet/website/images/ag.svg | 9 + .../articles/CreateUserProxyAgent/image-1.png | 3 + .../articles/DynamicGroupChat/dynamicChat.gif | 3 + .../PrintMessageMiddleware/printMessage.png | 3 + .../streamingoutput.gif | 3 + .../SearcherSummarizer.gif | 3 + dotnet/website/index.md | 4 + dotnet/website/toc.yml | 14 + dotnet/website/update.md | 45 ++ 226 files changed, 16125 insertions(+), 22 deletions(-) create mode 100644 dotnet/.editorconfig create mode 100644 dotnet/.gitignore create mode 100644 dotnet/AutoGen.sln create mode 100644 dotnet/Directory.Build.props create mode 100644 dotnet/NuGet.config create mode 100644 dotnet/README.md create mode 100644 dotnet/eng/MetaInfo.props create mode 100644 dotnet/eng/Sign.props create mode 100644 dotnet/eng/Version.props create mode 100644 dotnet/eng/opensource.snk create mode 100644 dotnet/global.json create mode 100644 dotnet/nuget/NUGET.md create mode 100644 dotnet/nuget/icon.png create mode 100644 dotnet/nuget/nuget-package.props create mode 100644 dotnet/sample/AutoGen.BasicSamples/AutoGen.BasicSample.csproj create mode 100644 dotnet/sample/AutoGen.BasicSamples/CodeSnippet/AgentCodeSnippet.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/CodeSnippet/BuildInMessageCodeSnippet.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/CodeSnippet/CreateAnAgent.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/CodeSnippet/FunctionCallCodeSnippet.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/CodeSnippet/GetStartCodeSnippet.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MiddlewareAgentCodeSnippet.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/CodeSnippet/PrintMessageMiddlewareCodeSnippet.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/CodeSnippet/SemanticKernelCodeSnippet.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/CodeSnippet/TypeSafeFunctionCallCodeSnippet.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/CodeSnippet/UserProxyAgentCodeSnippet.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/Example01_AssistantAgent.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/Example02_TwoAgent_MathChat.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/Example03_Agent_FunctionCall.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/Example04_Dynamic_GroupChat_Coding_Task.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/Example05_Dalle_And_GPT4V.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/Example06_UserProxyAgent.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/Example08_LMStudio.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/Example09_LMStudio_FunctionCall.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/Example10_SemanticKernel.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/Example11_Sequential_GroupChat_Example.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/Example12_TwoAgent_Fill_Application.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/Example13_OpenAIAgent_JsonMode.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/Example14_MistralClientAgent_TokenCount.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/GlobalUsing.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/LLMConfiguration.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/Program.cs create mode 100644 dotnet/src/AutoGen.Core/Agent/DefaultReplyAgent.cs create mode 100644 dotnet/src/AutoGen.Core/Agent/GroupChatManager.cs create mode 100644 dotnet/src/AutoGen.Core/Agent/IAgent.cs create mode 100644 dotnet/src/AutoGen.Core/Agent/IMiddlewareAgent.cs create mode 100644 dotnet/src/AutoGen.Core/Agent/IStreamingAgent.cs create mode 100644 dotnet/src/AutoGen.Core/Agent/MiddlewareAgent.cs create mode 100644 dotnet/src/AutoGen.Core/Agent/MiddlewareStreamingAgent.cs create mode 100644 dotnet/src/AutoGen.Core/AutoGen.Core.csproj create mode 100644 dotnet/src/AutoGen.Core/Extension/AgentExtension.cs create mode 100644 dotnet/src/AutoGen.Core/Extension/GroupChatExtension.cs create mode 100644 dotnet/src/AutoGen.Core/Extension/MessageExtension.cs create mode 100644 dotnet/src/AutoGen.Core/Extension/MiddlewareExtension.cs create mode 100644 dotnet/src/AutoGen.Core/Extension/PrintMessageMiddlewareExtension.cs create mode 100644 dotnet/src/AutoGen.Core/Extension/StreamingMiddlewareExtension.cs create mode 100644 dotnet/src/AutoGen.Core/Function/FunctionAttribute.cs create mode 100644 dotnet/src/AutoGen.Core/GroupChat/Graph.cs create mode 100644 dotnet/src/AutoGen.Core/GroupChat/GroupChat.cs create mode 100644 dotnet/src/AutoGen.Core/GroupChat/RoundRobinGroupChat.cs create mode 100644 dotnet/src/AutoGen.Core/IGroupChat.cs create mode 100644 dotnet/src/AutoGen.Core/ILLMConfig.cs create mode 100644 dotnet/src/AutoGen.Core/Message/AggregateMessage.cs create mode 100644 dotnet/src/AutoGen.Core/Message/IMessage.cs create mode 100644 dotnet/src/AutoGen.Core/Message/ImageMessage.cs create mode 100644 dotnet/src/AutoGen.Core/Message/Message.cs create mode 100644 dotnet/src/AutoGen.Core/Message/MessageEnvelope.cs create mode 100644 dotnet/src/AutoGen.Core/Message/MultiModalMessage.cs create mode 100644 dotnet/src/AutoGen.Core/Message/Role.cs create mode 100644 dotnet/src/AutoGen.Core/Message/TextMessage.cs create mode 100644 dotnet/src/AutoGen.Core/Message/ToolCallMessage.cs create mode 100644 dotnet/src/AutoGen.Core/Message/ToolCallResultMessage.cs create mode 100644 dotnet/src/AutoGen.Core/Middleware/DelegateMiddleware.cs create mode 100644 dotnet/src/AutoGen.Core/Middleware/DelegateStreamingMiddleware.cs create mode 100644 dotnet/src/AutoGen.Core/Middleware/FunctionCallMiddleware.cs create mode 100644 dotnet/src/AutoGen.Core/Middleware/IMiddleware.cs create mode 100644 dotnet/src/AutoGen.Core/Middleware/IStreamingMiddleware.cs create mode 100644 dotnet/src/AutoGen.Core/Middleware/MiddlewareContext.cs create mode 100644 dotnet/src/AutoGen.Core/Middleware/PrintMessageMiddleware.cs create mode 100644 dotnet/src/AutoGen.DotnetInteractive/AutoGen.DotnetInteractive.csproj create mode 100644 dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveFunction.cs create mode 100644 dotnet/src/AutoGen.DotnetInteractive/Extension/AgentExtension.cs create mode 100644 dotnet/src/AutoGen.DotnetInteractive/GlobalUsing.cs create mode 100644 dotnet/src/AutoGen.DotnetInteractive/InteractiveService.cs create mode 100644 dotnet/src/AutoGen.DotnetInteractive/RestoreInteractive.config create mode 100644 dotnet/src/AutoGen.DotnetInteractive/Utils.cs create mode 100644 dotnet/src/AutoGen.DotnetInteractive/dotnet-tools.json create mode 100644 dotnet/src/AutoGen.LMStudio/AutoGen.LMStudio.csproj create mode 100644 dotnet/src/AutoGen.LMStudio/GlobalUsing.cs create mode 100644 dotnet/src/AutoGen.LMStudio/LMStudioAgent.cs create mode 100644 dotnet/src/AutoGen.LMStudio/LMStudioConfig.cs create mode 100644 dotnet/src/AutoGen.LMStudio/README.md create mode 100644 dotnet/src/AutoGen.Mistral/Agent/MistralClientAgent.cs create mode 100644 dotnet/src/AutoGen.Mistral/AutoGen.Mistral.csproj create mode 100644 dotnet/src/AutoGen.Mistral/Converters/JsonPropertyNameEnumConverter.cs create mode 100644 dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionRequest.cs create mode 100644 dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs create mode 100644 dotnet/src/AutoGen.Mistral/DTOs/ChatMessage.cs create mode 100644 dotnet/src/AutoGen.Mistral/DTOs/Choice.cs create mode 100644 dotnet/src/AutoGen.Mistral/DTOs/Error.cs create mode 100644 dotnet/src/AutoGen.Mistral/DTOs/ErrorResponse.cs create mode 100644 dotnet/src/AutoGen.Mistral/DTOs/FunctionDefinition.cs create mode 100644 dotnet/src/AutoGen.Mistral/DTOs/Model.cs create mode 100644 dotnet/src/AutoGen.Mistral/DTOs/ResponseFormat.cs create mode 100644 dotnet/src/AutoGen.Mistral/DTOs/Tool.cs create mode 100644 dotnet/src/AutoGen.Mistral/DTOs/Usage.cs create mode 100644 dotnet/src/AutoGen.Mistral/Extension/FunctionContractExtension.cs create mode 100644 dotnet/src/AutoGen.Mistral/Extension/MistralAgentExtension.cs create mode 100644 dotnet/src/AutoGen.Mistral/Middleware/MistralChatMessageConnector.cs create mode 100644 dotnet/src/AutoGen.Mistral/MistralAIModelID.cs create mode 100644 dotnet/src/AutoGen.Mistral/MistralClient.cs create mode 100644 dotnet/src/AutoGen.OpenAI/Agent/GPTAgent.cs create mode 100644 dotnet/src/AutoGen.OpenAI/Agent/OpenAIChatAgent.cs create mode 100644 dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj create mode 100644 dotnet/src/AutoGen.OpenAI/AzureOpenAIConfig.cs create mode 100644 dotnet/src/AutoGen.OpenAI/Extension/FunctionContractExtension.cs create mode 100644 dotnet/src/AutoGen.OpenAI/Extension/MessageExtension.cs create mode 100644 dotnet/src/AutoGen.OpenAI/Extension/OpenAIAgentExtension.cs create mode 100644 dotnet/src/AutoGen.OpenAI/GlobalUsing.cs create mode 100644 dotnet/src/AutoGen.OpenAI/Middleware/OpenAIChatRequestMessageConnector.cs create mode 100644 dotnet/src/AutoGen.OpenAI/OpenAIConfig.cs create mode 100644 dotnet/src/AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj create mode 100644 dotnet/src/AutoGen.SemanticKernel/Extension/KernelExtension.cs create mode 100644 dotnet/src/AutoGen.SemanticKernel/Extension/SemanticKernelAgentExtension.cs create mode 100644 dotnet/src/AutoGen.SemanticKernel/GlobalUsing.cs create mode 100644 dotnet/src/AutoGen.SemanticKernel/Middleware/SemanticKernelChatMessageContentConnector.cs create mode 100644 dotnet/src/AutoGen.SemanticKernel/SemanticKernelAgent.cs create mode 100644 dotnet/src/AutoGen.SourceGenerator/AutoGen.SourceGenerator.csproj create mode 100644 dotnet/src/AutoGen.SourceGenerator/DocumentCommentExtension.cs create mode 100644 dotnet/src/AutoGen.SourceGenerator/FunctionCallGenerator.cs create mode 100644 dotnet/src/AutoGen.SourceGenerator/FunctionContract.cs create mode 100644 dotnet/src/AutoGen.SourceGenerator/FunctionExtension.cs create mode 100644 dotnet/src/AutoGen.SourceGenerator/README.md create mode 100644 dotnet/src/AutoGen.SourceGenerator/Template/FunctionCallTemplate.cs create mode 100644 dotnet/src/AutoGen.SourceGenerator/Template/FunctionCallTemplate.tt create mode 100644 dotnet/src/AutoGen/API/LLMConfigAPI.cs create mode 100644 dotnet/src/AutoGen/Agent/AssistantAgent.cs create mode 100644 dotnet/src/AutoGen/Agent/ConversableAgent.cs create mode 100644 dotnet/src/AutoGen/Agent/UserProxyAgent.cs create mode 100644 dotnet/src/AutoGen/AutoGen.csproj create mode 100644 dotnet/src/AutoGen/ConversableAgentConfig.cs create mode 100644 dotnet/src/AutoGen/GlobalUsing.cs create mode 100644 dotnet/src/AutoGen/Middleware/HumanInputMiddleware.cs create mode 100644 dotnet/test/.editorconfig create mode 100644 dotnet/test/AutoGen.Mistral.Tests/AutoGen.Mistral.Tests.csproj create mode 100644 dotnet/test/AutoGen.Mistral.Tests/MistralClientAgentTests.cs create mode 100644 dotnet/test/AutoGen.Mistral.Tests/MistralClientTests.cs create mode 100644 dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionExample.Add_Test.approved.txt create mode 100644 dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionExample.DictionaryToString_Test.approved.txt create mode 100644 dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionExample.Query_Test.approved.txt create mode 100644 dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionExample.Sum_Test.approved.txt create mode 100644 dotnet/test/AutoGen.SourceGenerator.Tests/AutoGen.SourceGenerator.Tests.csproj create mode 100644 dotnet/test/AutoGen.SourceGenerator.Tests/FilescopeNamespaceFunctionExample.cs create mode 100644 dotnet/test/AutoGen.SourceGenerator.Tests/FunctionExample.test.cs create mode 100644 dotnet/test/AutoGen.SourceGenerator.Tests/FunctionExamples.cs create mode 100644 dotnet/test/AutoGen.SourceGenerator.Tests/TopLevelStatementFunctionExample.cs create mode 100644 dotnet/test/AutoGen.Tests/ApprovalTests/OpenAIMessageTests.BasicMessageTest.approved.txt create mode 100644 dotnet/test/AutoGen.Tests/Attribute/EnvironmentSpecificFactAttribute.cs create mode 100644 dotnet/test/AutoGen.Tests/Attribute/OpenAIFact.cs create mode 100644 dotnet/test/AutoGen.Tests/AutoGen.Tests.csproj create mode 100644 dotnet/test/AutoGen.Tests/BasicSampleTest.cs create mode 100644 dotnet/test/AutoGen.Tests/EchoAgent.cs create mode 100644 dotnet/test/AutoGen.Tests/GlobalUsing.cs create mode 100644 dotnet/test/AutoGen.Tests/MathClassTest.cs create mode 100644 dotnet/test/AutoGen.Tests/MiddlewareAgentTest.cs create mode 100644 dotnet/test/AutoGen.Tests/MiddlewareTest.cs create mode 100644 dotnet/test/AutoGen.Tests/OpenAIChatAgentTest.cs create mode 100644 dotnet/test/AutoGen.Tests/OpenAIMessageTests.cs create mode 100644 dotnet/test/AutoGen.Tests/RegisterReplyAgentTest.cs create mode 100644 dotnet/test/AutoGen.Tests/SemanticKernelAgentTest.cs create mode 100644 dotnet/test/AutoGen.Tests/SingleAgentTest.cs create mode 100644 dotnet/test/AutoGen.Tests/TwoAgentTest.cs create mode 100644 dotnet/test/AutoGen.Tests/WorkflowTest.cs create mode 100644 dotnet/website/.gitignore create mode 100644 dotnet/website/README.md create mode 100644 dotnet/website/articles/Agent-overview.md create mode 100644 dotnet/website/articles/AutoGen-Mistral-Overview.md create mode 100644 dotnet/website/articles/AutoGen-OpenAI-Overview.md create mode 100644 dotnet/website/articles/AutoGen-SemanticKernel-Overview.md create mode 100644 dotnet/website/articles/Built-in-messages.md create mode 100644 dotnet/website/articles/Consume-LLM-server-from-LM-Studio.md create mode 100644 dotnet/website/articles/Create-a-user-proxy-agent.md create mode 100644 dotnet/website/articles/Create-an-agent.md create mode 100644 dotnet/website/articles/Create-type-safe-function-call.md create mode 100644 dotnet/website/articles/Create-your-own-agent.md create mode 100644 dotnet/website/articles/Create-your-own-middleware.md create mode 100644 dotnet/website/articles/Function-call-middleware.md create mode 100644 dotnet/website/articles/Function-call-overview.md create mode 100644 dotnet/website/articles/Group-chat-overview.md create mode 100644 dotnet/website/articles/Group-chat.md create mode 100644 dotnet/website/articles/Installation.md create mode 100644 dotnet/website/articles/Middleware-overview.md create mode 100644 dotnet/website/articles/MistralChatAgent-count-token-usage.md create mode 100644 dotnet/website/articles/MistralChatAgent-use-function-call.md create mode 100644 dotnet/website/articles/OpenAIChatAgent-simple-chat.md create mode 100644 dotnet/website/articles/OpenAIChatAgent-support-more-messages.md create mode 100644 dotnet/website/articles/OpenAIChatAgent-use-function-call.md create mode 100644 dotnet/website/articles/OpenAIChatAgent-use-json-mode.md create mode 100644 dotnet/website/articles/Print-message-middleware.md create mode 100644 dotnet/website/articles/Roundrobin-chat.md create mode 100644 dotnet/website/articles/Run-dotnet-code.md create mode 100644 dotnet/website/articles/SemanticKernelAgent-simple-chat.md create mode 100644 dotnet/website/articles/SemanticKernelAgent-support-more-messages.md create mode 100644 dotnet/website/articles/Two-agent-chat.md create mode 100644 dotnet/website/articles/Use-function-call.md create mode 100644 dotnet/website/articles/Use-graph-in-group-chat.md create mode 100644 dotnet/website/articles/getting-start.md create mode 100644 dotnet/website/articles/toc.yml create mode 100644 dotnet/website/docfx.json create mode 100644 dotnet/website/filterConfig.yml create mode 100644 dotnet/website/images/ag.ico create mode 100644 dotnet/website/images/ag.svg create mode 100644 dotnet/website/images/articles/CreateUserProxyAgent/image-1.png create mode 100644 dotnet/website/images/articles/DynamicGroupChat/dynamicChat.gif create mode 100644 dotnet/website/images/articles/PrintMessageMiddleware/printMessage.png create mode 100644 dotnet/website/images/articles/PrintMessageMiddleware/streamingoutput.gif create mode 100644 dotnet/website/images/articles/SequentialGroupChat/SearcherSummarizer.gif create mode 100644 dotnet/website/index.md create mode 100644 dotnet/website/toc.yml create mode 100644 dotnet/website/update.md diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 61e811804a8..e337f714334 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -142,5 +142,4 @@ jobs: ls -R ./output/nightly dotnet nuget push --api-key ${{ secrets.MYGET_TOKEN }} --source "https://www.myget.org/F/agentchat/api/v3/index.json" ./output/nightly/*.nupkg --skip-duplicate env: - MYGET_TOKEN: ${{ secrets.MYGET_TOKEN }} - + MYGET_TOKEN: ${{ secrets.MYGET_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/dotnet-release.yml b/.github/workflows/dotnet-release.yml index af7104cc0e6..84b1f43b71e 100644 --- a/.github/workflows/dotnet-release.yml +++ b/.github/workflows/dotnet-release.yml @@ -7,6 +7,7 @@ on: workflow_dispatch: push: branches: + - dotnet/release/** - dotnet/release concurrency: @@ -59,13 +60,6 @@ jobs: echo "Publish package to Nuget" echo "ls output directory" ls -R ./output/release - dotnet nuget push --api-key AzureArtifacts ./output/release/*.nupkg --skip-duplicate --api-key ${{ secrets.AUTOGEN_NUGET_API_KEY }} - - name: Tag commit - run: | - Write-Host "Tag commit" - # version = eng/MetaInfo.props.Project.PropertyGroup.VersionPrefix - $metaInfoContent = cat ./eng/MetaInfo.props - $version = $metaInfoContent | Select-String -Pattern "(.*)" | ForEach-Object { $_.Matches.Groups[1].Value } - git tag -a "$version" -m "AutoGen.Net release $version" - git push origin --tags - shell: pwsh + # remove AutoGen.SourceGenerator.snupkg because it's an empty package + rm ./output/release/AutoGen.SourceGenerator.*.snupkg + dotnet nuget push --api-key ${{ secrets.AUTOGEN_NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json ./output/release/*.nupkg --skip-duplicate diff --git a/dotnet/.config/dotnet-tools.json b/dotnet/.config/dotnet-tools.json index 5b341cff736..6b2517ea2c6 100644 --- a/dotnet/.config/dotnet-tools.json +++ b/dotnet/.config/dotnet-tools.json @@ -1,12 +1,18 @@ { - "version": 1, - "isRoot": true, - "tools": { - "dotnet-repl": { - "version": "0.1.205", - "commands": [ - "dotnet-repl" - ] - } + "version": 1, + "isRoot": true, + "tools": { + "dotnet-repl": { + "version": "0.1.205", + "commands": [ + "dotnet-repl" + ] + }, + "docfx": { + "version": "2.67.5", + "commands": [ + "docfx" + ] } - } \ No newline at end of file + } +} \ No newline at end of file diff --git a/dotnet/.editorconfig b/dotnet/.editorconfig new file mode 100644 index 00000000000..4da1adc5de6 --- /dev/null +++ b/dotnet/.editorconfig @@ -0,0 +1,178 @@ +# EditorConfig is awesome:http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Don't use tabs for indentation. +[*] +indent_style = space +# (Please don't specify an indent_size here; that has too many unintended consequences.) + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true +charset = utf-8-bom + +[*.xaml] +indent_size = 4 + +[*.ps1] +indent_size = 2 + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# Xml config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +[*.groovy] +indent_size = 2 + +# Dotnet code style settings: +[*.{cs,vb}] +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true +dotnet_style_require_accessibility_modifiers = always:warning + +# No blank line between System.* and Microsoft.* +dotnet_separate_import_directive_groups = false + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:error +dotnet_style_null_propagation = true:error +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = false +dotnet_style_prefer_conditional_expression_over_assignment = false +dotnet_style_prefer_auto_properties = false + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:error +dotnet_style_predefined_type_for_member_access = true:error + +# Prefer read-only on fields +dotnet_style_readonly_field = false + +# CSharp code style settings: +[*.cs] + +# Prefer "var" only when the type is apparent +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = false:suggestion + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Use block body for local functions +csharp_style_expression_bodied_local_functions = when_on_single_line:silent + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:error +csharp_style_pattern_matching_over_as_with_null_check = true:error +csharp_style_inlined_variable_declaration = true:error +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion + +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Identation options +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_switch_labels = true +csharp_indent_labels = no_change +csharp_indent_block_contents = true +csharp_indent_braces = false + +# Spacing options +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false +csharp_space_between_empty_square_brackets = false +csharp_space_before_open_square_brackets = false +csharp_space_around_declaration_statements = false +csharp_space_around_binary_operators = before_and_after +csharp_space_after_cast = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_before_dot = false +csharp_space_after_dot = false +csharp_space_before_comma = false +csharp_space_after_comma = true +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_semicolon_in_for_statement = true + +# Wrapping +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true + +# Code block +csharp_prefer_braces = false:none + +# Using statements +csharp_using_directive_placement = outside_namespace:error + +# Modifier settings +csharp_prefer_static_local_function = true:warning +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning + +# Header template +file_header_template = Copyright (c) Microsoft Corporation. All rights reserved.\n{fileName} +dotnet_diagnostic.IDE0073.severity = error + +# enable format error +dotnet_diagnostic.IDE0055.severity = error + +# IDE0035: Remove unreachable code +dotnet_diagnostic.IDE0035.severity = error + +# IDE0005: Remove unncecessary usings +dotnet_diagnostic.CS8019.severity = error +dotnet_diagnostic.IDE0005.severity = error + +# IDE0069: Remove unused local variable +dotnet_diagnostic.IDE0069.severity = error + +# disable CS1573: Parameter has no matching param tag in the XML comment for +dotnet_diagnostic.CS1573.severity = none + +# disable CS1570: XML comment has badly formed XML +dotnet_diagnostic.CS1570.severity = none + +# disable check for generated code +[*.generated.cs] +generated_code = true \ No newline at end of file diff --git a/dotnet/.gitignore b/dotnet/.gitignore new file mode 100644 index 00000000000..65e7ba678dd --- /dev/null +++ b/dotnet/.gitignore @@ -0,0 +1,30 @@ +# gitignore file for C#/VS + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# vs cache +.vs/ + +# vs code cache +.vscode/ + +# Properties +Properties/ + +artifacts/ +output/ + +*.binlog + +# JetBrains Rider +.idea/ \ No newline at end of file diff --git a/dotnet/AutoGen.sln b/dotnet/AutoGen.sln new file mode 100644 index 00000000000..3841b9acf7b --- /dev/null +++ b/dotnet/AutoGen.sln @@ -0,0 +1,111 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34322.80 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen", "src\AutoGen\AutoGen.csproj", "{B2B27ACB-AA50-4FED-A06C-3AD6B4218188}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{18BF8DD7-0585-48BF-8F97-AD333080CE06}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F823671B-3ECA-4AE6-86DA-25E920D3FE64}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.Tests", "test\AutoGen.Tests\AutoGen.Tests.csproj", "{FDD99AEC-4C57-4020-B23F-650612856102}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.SourceGenerator", "src\AutoGen.SourceGenerator\AutoGen.SourceGenerator.csproj", "{3FFD14E3-D6BC-4EA7-97A2-D21733060FD6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.SourceGenerator.Tests", "test\AutoGen.SourceGenerator.Tests\AutoGen.SourceGenerator.Tests.csproj", "{05A2FAD8-03B0-4B2F-82AF-2F6BF0F050E5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.BasicSample", "sample\AutoGen.BasicSamples\AutoGen.BasicSample.csproj", "{7EBF916A-A7B1-4B74-AF10-D705B7A18F58}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{FBFEAD1F-29EB-4D99-A672-0CD8473E10B9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.DotnetInteractive", "src\AutoGen.DotnetInteractive\AutoGen.DotnetInteractive.csproj", "{B61D8008-7FB7-4C0E-8044-3A74AA63A596}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.LMStudio", "src\AutoGen.LMStudio\AutoGen.LMStudio.csproj", "{F98BDA9B-8657-4BA8-9B03-BAEA454CAE60}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.SemanticKernel", "src\AutoGen.SemanticKernel\AutoGen.SemanticKernel.csproj", "{45D6FC80-36F3-4967-9663-E20B63824621}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.Core", "src\AutoGen.Core\AutoGen.Core.csproj", "{D58D43D1-0617-4A3D-9932-C773E6398535}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.OpenAI", "src\AutoGen.OpenAI\AutoGen.OpenAI.csproj", "{63445BB7-DBB9-4AEF-9D6F-98BBE75EE1EC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.Mistral", "src\AutoGen.Mistral\AutoGen.Mistral.csproj", "{6585D1A4-3D97-4D76-A688-1933B61AEB19}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.Mistral.Tests", "test\AutoGen.Mistral.Tests\AutoGen.Mistral.Tests.csproj", "{15441693-3659-4868-B6C1-B106F52FF3BA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B2B27ACB-AA50-4FED-A06C-3AD6B4218188}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2B27ACB-AA50-4FED-A06C-3AD6B4218188}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2B27ACB-AA50-4FED-A06C-3AD6B4218188}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2B27ACB-AA50-4FED-A06C-3AD6B4218188}.Release|Any CPU.Build.0 = Release|Any CPU + {FDD99AEC-4C57-4020-B23F-650612856102}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FDD99AEC-4C57-4020-B23F-650612856102}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FDD99AEC-4C57-4020-B23F-650612856102}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FDD99AEC-4C57-4020-B23F-650612856102}.Release|Any CPU.Build.0 = Release|Any CPU + {3FFD14E3-D6BC-4EA7-97A2-D21733060FD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3FFD14E3-D6BC-4EA7-97A2-D21733060FD6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3FFD14E3-D6BC-4EA7-97A2-D21733060FD6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3FFD14E3-D6BC-4EA7-97A2-D21733060FD6}.Release|Any CPU.Build.0 = Release|Any CPU + {05A2FAD8-03B0-4B2F-82AF-2F6BF0F050E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05A2FAD8-03B0-4B2F-82AF-2F6BF0F050E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05A2FAD8-03B0-4B2F-82AF-2F6BF0F050E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05A2FAD8-03B0-4B2F-82AF-2F6BF0F050E5}.Release|Any CPU.Build.0 = Release|Any CPU + {7EBF916A-A7B1-4B74-AF10-D705B7A18F58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EBF916A-A7B1-4B74-AF10-D705B7A18F58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EBF916A-A7B1-4B74-AF10-D705B7A18F58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EBF916A-A7B1-4B74-AF10-D705B7A18F58}.Release|Any CPU.Build.0 = Release|Any CPU + {B61D8008-7FB7-4C0E-8044-3A74AA63A596}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B61D8008-7FB7-4C0E-8044-3A74AA63A596}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B61D8008-7FB7-4C0E-8044-3A74AA63A596}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B61D8008-7FB7-4C0E-8044-3A74AA63A596}.Release|Any CPU.Build.0 = Release|Any CPU + {F98BDA9B-8657-4BA8-9B03-BAEA454CAE60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F98BDA9B-8657-4BA8-9B03-BAEA454CAE60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F98BDA9B-8657-4BA8-9B03-BAEA454CAE60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F98BDA9B-8657-4BA8-9B03-BAEA454CAE60}.Release|Any CPU.Build.0 = Release|Any CPU + {45D6FC80-36F3-4967-9663-E20B63824621}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {45D6FC80-36F3-4967-9663-E20B63824621}.Debug|Any CPU.Build.0 = Debug|Any CPU + {45D6FC80-36F3-4967-9663-E20B63824621}.Release|Any CPU.ActiveCfg = Release|Any CPU + {45D6FC80-36F3-4967-9663-E20B63824621}.Release|Any CPU.Build.0 = Release|Any CPU + {D58D43D1-0617-4A3D-9932-C773E6398535}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D58D43D1-0617-4A3D-9932-C773E6398535}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D58D43D1-0617-4A3D-9932-C773E6398535}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D58D43D1-0617-4A3D-9932-C773E6398535}.Release|Any CPU.Build.0 = Release|Any CPU + {63445BB7-DBB9-4AEF-9D6F-98BBE75EE1EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63445BB7-DBB9-4AEF-9D6F-98BBE75EE1EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63445BB7-DBB9-4AEF-9D6F-98BBE75EE1EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63445BB7-DBB9-4AEF-9D6F-98BBE75EE1EC}.Release|Any CPU.Build.0 = Release|Any CPU + {6585D1A4-3D97-4D76-A688-1933B61AEB19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6585D1A4-3D97-4D76-A688-1933B61AEB19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6585D1A4-3D97-4D76-A688-1933B61AEB19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6585D1A4-3D97-4D76-A688-1933B61AEB19}.Release|Any CPU.Build.0 = Release|Any CPU + {15441693-3659-4868-B6C1-B106F52FF3BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15441693-3659-4868-B6C1-B106F52FF3BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15441693-3659-4868-B6C1-B106F52FF3BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15441693-3659-4868-B6C1-B106F52FF3BA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B2B27ACB-AA50-4FED-A06C-3AD6B4218188} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} + {FDD99AEC-4C57-4020-B23F-650612856102} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} + {3FFD14E3-D6BC-4EA7-97A2-D21733060FD6} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} + {05A2FAD8-03B0-4B2F-82AF-2F6BF0F050E5} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} + {7EBF916A-A7B1-4B74-AF10-D705B7A18F58} = {FBFEAD1F-29EB-4D99-A672-0CD8473E10B9} + {B61D8008-7FB7-4C0E-8044-3A74AA63A596} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} + {F98BDA9B-8657-4BA8-9B03-BAEA454CAE60} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} + {45D6FC80-36F3-4967-9663-E20B63824621} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} + {D58D43D1-0617-4A3D-9932-C773E6398535} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} + {63445BB7-DBB9-4AEF-9D6F-98BBE75EE1EC} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} + {6585D1A4-3D97-4D76-A688-1933B61AEB19} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} + {15441693-3659-4868-B6C1-B106F52FF3BA} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {93384647-528D-46C8-922C-8DB36A382F0B} + EndGlobalSection +EndGlobal diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props new file mode 100644 index 00000000000..03a11d92c23 --- /dev/null +++ b/dotnet/Directory.Build.props @@ -0,0 +1,23 @@ + + + + + + + net8.0 + preview + enable + True + $(MSBuildThisFileDirectory)eng/opensource.snk + CS1998;CS1591 + $(NoWarn);$(CSNoWarn);NU5104 + true + false + true + true + + + + $(MSBuildThisFileDirectory)../ + + \ No newline at end of file diff --git a/dotnet/NuGet.config b/dotnet/NuGet.config new file mode 100644 index 00000000000..2eb25136c6a --- /dev/null +++ b/dotnet/NuGet.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/README.md b/dotnet/README.md new file mode 100644 index 00000000000..5b0803b6e11 --- /dev/null +++ b/dotnet/README.md @@ -0,0 +1,103 @@ +### AutoGen for .NET + +[![dotnet-ci](https://github.com/microsoft/autogen/actions/workflows/dotnet-build.yml/badge.svg)](https://github.com/microsoft/autogen/actions/workflows/dotnet-build.yml) +[![NuGet version](https://badge.fury.io/nu/AutoGen.Core.svg)](https://badge.fury.io/nu/AutoGen.Core) + +> [!NOTE] +> Nightly build is available at: +> - ![Static Badge](https://img.shields.io/badge/public-blue?style=flat) ![Static Badge](https://img.shields.io/badge/nightly-yellow?style=flat) ![Static Badge](https://img.shields.io/badge/github-grey?style=flat): https://nuget.pkg.github.com/microsoft/index.json +> - ![Static Badge](https://img.shields.io/badge/public-blue?style=flat) ![Static Badge](https://img.shields.io/badge/nightly-yellow?style=flat) ![Static Badge](https://img.shields.io/badge/myget-grey?style=flat): https://www.myget.org/F/agentchat/api/v3/index.json +> - ![Static Badge](https://img.shields.io/badge/internal-blue?style=flat) ![Static Badge](https://img.shields.io/badge/nightly-yellow?style=flat) ![Static Badge](https://img.shields.io/badge/azure_devops-grey?style=flat) : https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/AutoGen/nuget/v3/index.json + + +Firstly, following the [installation guide](./website/articles/Installation.md) to install AutoGen packages. + +Then you can start with the following code snippet to create a conversable agent and chat with it. + +```csharp +using AutoGen; +using AutoGen.OpenAI; + +var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); +var gpt35Config = new OpenAIConfig(openAIKey, "gpt-3.5-turbo"); + +var assistantAgent = new AssistantAgent( + name: "assistant", + systemMessage: "You are an assistant that help user to do some tasks.", + llmConfig: new ConversableAgentConfig + { + Temperature = 0, + ConfigList = [gpt35Config], + }) + .RegisterPrintMessage(); // register a hook to print message nicely to console + +// set human input mode to ALWAYS so that user always provide input +var userProxyAgent = new UserProxyAgent( + name: "user", + humanInputMode: ConversableAgent.HumanInputMode.ALWAYS) + .RegisterPrintMessage(); + +// start the conversation +await userProxyAgent.InitiateChatAsync( + receiver: assistantAgent, + message: "Hey assistant, please do me a favor.", + maxRound: 10); +``` + +#### Samples +You can find more examples under the [sample project](https://github.com/microsoft/autogen/tree/dotnet/dotnet/sample/AutoGen.BasicSamples). + +#### Functionality +- ConversableAgent + - [x] function call + - [x] code execution (dotnet only, powered by [`dotnet-interactive`](https://github.com/dotnet/interactive)) + +- Agent communication + - [x] Two-agent chat + - [x] Group chat + +- [ ] Enhanced LLM Inferences + +- Exclusive for dotnet + - [x] Source generator for type-safe function definition generation + +#### Update log +##### Update on 0.0.11 (2024-03-26) +- Add link to Discord channel in nuget's readme.md +- Document improvements +##### Update on 0.0.10 (2024-03-12) +- Rename `Workflow` to `Graph` +- Rename `AddInitializeMessage` to `SendIntroduction` +- Rename `SequentialGroupChat` to `RoundRobinGroupChat` +##### Update on 0.0.9 (2024-03-02) +- Refactor over @AutoGen.Message and introducing `TextMessage`, `ImageMessage`, `MultiModalMessage` and so on. PR [#1676](https://github.com/microsoft/autogen/pull/1676) +- Add `AutoGen.SemanticKernel` to support seamless integration with Semantic Kernel +- Move the agent contract abstraction to `AutoGen.Core` package. The `AutoGen.Core` package provides the abstraction for message type, agent and group chat and doesn't contain dependencies over `Azure.AI.OpenAI` or `Semantic Kernel`. This is useful when you want to leverage AutoGen's abstraction only and want to avoid introducing any other dependencies. +- Move `GPTAgent`, `OpenAIChatAgent` and all openai-dependencies to `AutoGen.OpenAI` +##### Update on 0.0.8 (2024-02-28) +- Fix [#1804](https://github.com/microsoft/autogen/pull/1804) +- Streaming support for IAgent [#1656](https://github.com/microsoft/autogen/pull/1656) +- Streaming support for middleware via `MiddlewareStreamingAgent` [#1656](https://github.com/microsoft/autogen/pull/1656) +- Graph chat support with conditional transition workflow [#1761](https://github.com/microsoft/autogen/pull/1761) +- AutoGen.SourceGenerator: Generate `FunctionContract` from `FunctionAttribute` [#1736](https://github.com/microsoft/autogen/pull/1736) +##### Update on 0.0.7 (2024-02-11) +- Add `AutoGen.LMStudio` to support comsume openai-like API from LMStudio local server +##### Update on 0.0.6 (2024-01-23) +- Add `MiddlewareAgent` +- Use `MiddlewareAgent` to implement existing agent hooks (RegisterPreProcess, RegisterPostProcess, RegisterReply) +- Remove `AutoReplyAgent`, `PreProcessAgent`, `PostProcessAgent` because they are replaced by `MiddlewareAgent` +##### Update on 0.0.5 +- Simplify `IAgent` interface by removing `ChatLLM` Property +- Add `GenerateReplyOptions` to `IAgent.GenerateReplyAsync` which allows user to specify or override the options when generating reply + +##### Update on 0.0.4 +- Move out dependency of Semantic Kernel +- Add type `IChatLLM` as connector to LLM + +##### Update on 0.0.3 +- In AutoGen.SourceGenerator, rename FunctionAttribution to FunctionAttribute +- In AutoGen, refactor over ConversationAgent, UserProxyAgent, and AssistantAgent + +##### Update on 0.0.2 +- update Azure.OpenAI.AI to 1.0.0-beta.12 +- update Semantic kernel to 1.0.1 diff --git a/dotnet/eng/MetaInfo.props b/dotnet/eng/MetaInfo.props new file mode 100644 index 00000000000..4c354d8fee2 --- /dev/null +++ b/dotnet/eng/MetaInfo.props @@ -0,0 +1,12 @@ + + + + 0.0.12 + AutoGen + https://microsoft.github.io/autogen-for-net/ + https://github.com/microsoft/autogen + git + MIT + false + + \ No newline at end of file diff --git a/dotnet/eng/Sign.props b/dotnet/eng/Sign.props new file mode 100644 index 00000000000..0d69e7797e4 --- /dev/null +++ b/dotnet/eng/Sign.props @@ -0,0 +1,22 @@ + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + Microsoft400 + + + + + NuGet + + + diff --git a/dotnet/eng/Version.props b/dotnet/eng/Version.props new file mode 100644 index 00000000000..a497dc594c0 --- /dev/null +++ b/dotnet/eng/Version.props @@ -0,0 +1,17 @@ + + + + 1.0.0-beta.15 + 1.7.1 + 1.7.1-alpha + 5.0.0 + 4.3.0 + 6.0.0 + 6.8.0 + 2.4.2 + 17.7.0 + 1.0.0-beta.23523.2 + 8.0.0 + 4.0.0 + + \ No newline at end of file diff --git a/dotnet/eng/opensource.snk b/dotnet/eng/opensource.snk new file mode 100644 index 0000000000000000000000000000000000000000..779df7c83664a99f7ecef0a978ef597bbc3e2f6b GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50098;&^XYzTH}(faseIr9+N|`wci`K2nwPb zlq5DSX=xeY>8v`7$|T~0;SYLNoNq)v9Zo*Hgg1PL%3P{eE`a%yEA{G;w}vZIjuW`L zk;hhC@aW&{&+43jTI0Q-L>CUsf5!1guIj`h-IlJo>mOQJf~sW>{wY}U_z-;{IP$A^ zwA%A1IYdpPT4o15gwN$S+;#Q~SbSjtnK zDS`h^TLh5hf0cRA71bJX?AY{HLXfE-%(UIC#t#-jd)KFvb|t)g5SknHIVsWq7r!Vy zWT^Gw&j>Of(WJh4pZ~2zt$53ex;$yV5ctpScq+6$0sS6X1^{-?+egYRn>>Dp*ifLD zncU!xeq8d{#`?@2kMLo#OT~tD|1RYMR&i5VETzCiiRuSXNJ8zy72Wh?k;~Ow_)f)V zu({=oxR9jDJ8467)to=xPPDacfK6}zDuK`7Tp<5cKeyhV-Q7EZitn|MqX!7fIsiI^ zoVbS>$d^>X;)aNzj^=x!@r$?6Om*K5nzfz?X?$v0S1(6m!}D{B>~?|7c4}$fAsbYe ivvj9*^^Ltsc;DGw6150G4x6SQMA1pXwDJv5I literal 0 HcmV?d00001 diff --git a/dotnet/global.json b/dotnet/global.json new file mode 100644 index 00000000000..a93054a455c --- /dev/null +++ b/dotnet/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.101", + "rollForward": "latestMinor" + } +} \ No newline at end of file diff --git a/dotnet/nuget/NUGET.md b/dotnet/nuget/NUGET.md new file mode 100644 index 00000000000..34fdbca33ca --- /dev/null +++ b/dotnet/nuget/NUGET.md @@ -0,0 +1,8 @@ +### About AutoGen for .NET +`AutoGen for .NET` is the official .NET SDK for [AutoGen](https://github.com/microsoft/autogen). It enables you to create LLM agents and construct multi-agent workflows with ease. It also provides integration with popular platforms like OpenAI, Semantic Kernel, and LM Studio. + +### Gettings started +- Find documents and examples on our [document site](https://microsoft.github.io/autogen-for-net/) +- Join our [Discord channel](https://discord.gg/pAbnFJrkgZ) to get help and discuss with the community +- Report a bug or request a feature by creating a new issue in our [github repo](https://github.com/microsoft/autogen) +- Consume the nightly build package from one of the [nightly build feeds](https://microsoft.github.io/autogen-for-net/articles/Installation.html#nighly-build) \ No newline at end of file diff --git a/dotnet/nuget/icon.png b/dotnet/nuget/icon.png new file mode 100644 index 00000000000..076fc48c562 --- /dev/null +++ b/dotnet/nuget/icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02dbf31fea0b92714c80fdc90888da7e96374a1f52c621a939835fd3c876ddcc +size 426084 diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props new file mode 100644 index 00000000000..237fa96bcb2 --- /dev/null +++ b/dotnet/nuget/nuget-package.props @@ -0,0 +1,54 @@ + + + true + + + AutoGen + Microsoft + AutoGen + A programming framework for agentic AI + AI, Artificial Intelligence, SDK + $(AssemblyName) + + + MIT + © Microsoft Corporation. All rights reserved. + https://microsoft.github.io/autogen-for-net + https://github.com/microsoft/autogen + true + + + icon.png + icon.png + NUGET.md + + + true + snupkg + + + true + + + true + + + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + + + + + + + + + + + + + + + + true + + \ No newline at end of file diff --git a/dotnet/sample/AutoGen.BasicSamples/AutoGen.BasicSample.csproj b/dotnet/sample/AutoGen.BasicSamples/AutoGen.BasicSample.csproj new file mode 100644 index 00000000000..c4e41261933 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/AutoGen.BasicSample.csproj @@ -0,0 +1,19 @@ + + + + Exe + $(TestTargetFramework) + enable + enable + True + $(NoWarn);CS8981;CS8600;CS8602;CS8604;CS8618;CS0219;SKEXP0054;SKEXP0050 + + + + + + + + + + diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/AgentCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/AgentCodeSnippet.cs new file mode 100644 index 00000000000..df45e4bfe9f --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/AgentCodeSnippet.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AgentCodeSnippet.cs +using AutoGen.Core; + +namespace AutoGen.BasicSample.CodeSnippet; + +internal class AgentCodeSnippet +{ + public async Task ChatWithAnAgent(IStreamingAgent agent) + { + #region ChatWithAnAgent_GenerateReplyAsync + var message = new TextMessage(Role.User, "Hello"); + IMessage reply = await agent.GenerateReplyAsync([message]); + #endregion ChatWithAnAgent_GenerateReplyAsync + + #region ChatWithAnAgent_SendAsync + reply = await agent.SendAsync("Hello"); + #endregion ChatWithAnAgent_SendAsync + + #region ChatWithAnAgent_GenerateStreamingReplyAsync + var textMessage = new TextMessage(Role.User, "Hello"); + await foreach (var streamingReply in await agent.GenerateStreamingReplyAsync([message])) + { + if (streamingReply is TextMessageUpdate update) + { + Console.Write(update.Content); + } + } + #endregion ChatWithAnAgent_GenerateStreamingReplyAsync + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/BuildInMessageCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/BuildInMessageCodeSnippet.cs new file mode 100644 index 00000000000..b272ba23a03 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/BuildInMessageCodeSnippet.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// BuildInMessageCodeSnippet.cs + +using AutoGen.Core; +namespace AutoGen.BasicSample.CodeSnippet; + +internal class BuildInMessageCodeSnippet +{ + public async Task StreamingCallCodeSnippetAsync() + { + IStreamingAgent agent = default; + #region StreamingCallCodeSnippet + var helloTextMessage = new TextMessage(Role.User, "Hello"); + var reply = await agent.GenerateStreamingReplyAsync([helloTextMessage]); + var finalTextMessage = new TextMessage(Role.Assistant, string.Empty, from: agent.Name); + await foreach (var message in reply) + { + if (message is TextMessageUpdate textMessage) + { + Console.Write(textMessage.Content); + finalTextMessage.Update(textMessage); + } + } + #endregion StreamingCallCodeSnippet + + #region StreamingCallWithFinalMessage + reply = await agent.GenerateStreamingReplyAsync([helloTextMessage]); + TextMessage finalMessage = null; + await foreach (var message in reply) + { + if (message is TextMessageUpdate textMessage) + { + Console.Write(textMessage.Content); + } + else if (message is TextMessage txtMessage) + { + finalMessage = txtMessage; + } + } + #endregion StreamingCallWithFinalMessage + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/CreateAnAgent.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/CreateAnAgent.cs new file mode 100644 index 00000000000..4833c6195c9 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/CreateAnAgent.cs @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// CreateAnAgent.cs + +using AutoGen; +using AutoGen.Core; +using AutoGen.OpenAI; +using FluentAssertions; + +public partial class AssistantCodeSnippet +{ + public void CodeSnippet1() + { + #region code_snippet_1 + // get OpenAI Key and create config + var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); + var llmConfig = new OpenAIConfig(openAIKey, "gpt-3.5-turbo"); + + // create assistant agent + var assistantAgent = new AssistantAgent( + name: "assistant", + systemMessage: "You are an assistant that help user to do some tasks.", + llmConfig: new ConversableAgentConfig + { + Temperature = 0, + ConfigList = new[] { llmConfig }, + }); + #endregion code_snippet_1 + + } + + public void CodeSnippet2() + { + #region code_snippet_2 + // get OpenAI Key and create config + var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY"); + string endPoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"); // change to your endpoint + + var llmConfig = new AzureOpenAIConfig( + endpoint: endPoint, + deploymentName: "gpt-3.5-turbo-16k", // change to your deployment name + apiKey: apiKey); + + // create assistant agent + var assistantAgent = new AssistantAgent( + name: "assistant", + systemMessage: "You are an assistant that help user to do some tasks.", + llmConfig: new ConversableAgentConfig + { + Temperature = 0, + ConfigList = new[] { llmConfig }, + }); + #endregion code_snippet_2 + } + + #region code_snippet_3 + /// + /// convert input to upper case + /// + /// input + [Function] + public async Task UpperCase(string input) + { + var result = input.ToUpper(); + return result; + } + + #endregion code_snippet_3 + + public async Task CodeSnippet4() + { + // get OpenAI Key and create config + var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY"); + string endPoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"); // change to your endpoint + + var llmConfig = new AzureOpenAIConfig( + endpoint: endPoint, + deploymentName: "gpt-3.5-turbo-16k", // change to your deployment name + apiKey: apiKey); + #region code_snippet_4 + var assistantAgent = new AssistantAgent( + name: "assistant", + systemMessage: "You are an assistant that convert user input to upper case.", + llmConfig: new ConversableAgentConfig + { + Temperature = 0, + ConfigList = new[] + { + llmConfig + }, + FunctionContracts = new[] + { + this.UpperCaseFunctionContract, // The FunctionDefinition object for the UpperCase function + }, + }); + + var response = await assistantAgent.SendAsync("hello"); + response.Should().BeOfType(); + var toolCallMessage = (ToolCallMessage)response; + toolCallMessage.ToolCalls.Count().Should().Be(1); + toolCallMessage.ToolCalls.First().FunctionName.Should().Be("UpperCase"); + #endregion code_snippet_4 + } + + public async Task CodeSnippet5() + { + // get OpenAI Key and create config + var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY"); + string endPoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"); // change to your endpoint + + var llmConfig = new AzureOpenAIConfig( + endpoint: endPoint, + deploymentName: "gpt-3.5-turbo-16k", // change to your deployment name + apiKey: apiKey); + #region code_snippet_5 + var assistantAgent = new AssistantAgent( + name: "assistant", + systemMessage: "You are an assistant that convert user input to upper case.", + llmConfig: new ConversableAgentConfig + { + Temperature = 0, + ConfigList = new[] + { + llmConfig + }, + FunctionContracts = new[] + { + this.UpperCaseFunctionContract, // The FunctionDefinition object for the UpperCase function + }, + }, + functionMap: new Dictionary>> + { + { this.UpperCaseFunction.Name, this.UpperCaseWrapper }, // The wrapper function for the UpperCase function + }); + + var response = await assistantAgent.SendAsync("hello"); + response.Should().BeOfType(); + response.From.Should().Be("assistant"); + var textMessage = (TextMessage)response; + textMessage.Content.Should().Be("HELLO"); + #endregion code_snippet_5 + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/FunctionCallCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/FunctionCallCodeSnippet.cs new file mode 100644 index 00000000000..2b7e25fee0c --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/FunctionCallCodeSnippet.cs @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// FunctionCallCodeSnippet.cs + +using AutoGen; +using AutoGen.Core; +using AutoGen.OpenAI; +using FluentAssertions; + +public partial class FunctionCallCodeSnippet +{ + public async Task CodeSnippet4() + { + // get OpenAI Key and create config + var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY"); + string endPoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"); // change to your endpoint + + var llmConfig = new AzureOpenAIConfig( + endpoint: endPoint, + deploymentName: "gpt-3.5-turbo-16k", // change to your deployment name + apiKey: apiKey); + #region code_snippet_4 + var function = new TypeSafeFunctionCall(); + var assistantAgent = new AssistantAgent( + name: "assistant", + systemMessage: "You are an assistant that convert user input to upper case.", + llmConfig: new ConversableAgentConfig + { + Temperature = 0, + ConfigList = new[] + { + llmConfig + }, + FunctionContracts = new[] + { + function.WeatherReportFunctionContract, + }, + }); + + var response = await assistantAgent.SendAsync("hello What's the weather in Seattle today? today is 2024-01-01"); + response.Should().BeOfType(); + var toolCallMessage = (ToolCallMessage)response; + toolCallMessage.ToolCalls.Count().Should().Be(1); + toolCallMessage.ToolCalls[0].FunctionName.Should().Be("WeatherReport"); + toolCallMessage.ToolCalls[0].FunctionArguments.Should().Be(@"{""location"":""Seattle"",""date"":""2024-01-01""}"); + #endregion code_snippet_4 + } + + + public async Task CodeSnippet6() + { + // get OpenAI Key and create config + var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY"); + string endPoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"); // change to your endpoint + + var llmConfig = new AzureOpenAIConfig( + endpoint: endPoint, + deploymentName: "gpt-3.5-turbo-16k", // change to your deployment name + apiKey: apiKey); + #region code_snippet_6 + var function = new TypeSafeFunctionCall(); + var assistantAgent = new AssistantAgent( + name: "assistant", + llmConfig: new ConversableAgentConfig + { + Temperature = 0, + ConfigList = new[] + { + llmConfig + }, + FunctionContracts = new[] + { + function.WeatherReportFunctionContract, + }, + }, + functionMap: new Dictionary>> + { + { function.WeatherReportFunctionContract.Name, function.WeatherReportWrapper }, // The function wrapper for the weather report function + }); + + #endregion code_snippet_6 + + #region code_snippet_6_1 + var response = await assistantAgent.SendAsync("What's the weather in Seattle today? today is 2024-01-01"); + response.Should().BeOfType(); + var textMessage = (TextMessage)response; + textMessage.Content.Should().Be("Weather report for Seattle on 2024-01-01 is sunny"); + #endregion code_snippet_6_1 + } + + public async Task OverriderFunctionContractAsync() + { + IAgent agent = default; + IEnumerable messages = new List(); + #region overrider_function_contract + var function = new TypeSafeFunctionCall(); + var reply = agent.GenerateReplyAsync(messages, new GenerateReplyOptions + { + Functions = new[] { function.WeatherReportFunctionContract }, + }); + #endregion overrider_function_contract + } + + public async Task RegisterFunctionCallMiddlewareAsync() + { + IAgent agent = default; + #region register_function_call_middleware + var function = new TypeSafeFunctionCall(); + var functionCallMiddleware = new FunctionCallMiddleware( + functions: new[] { function.WeatherReportFunctionContract }, + functionMap: new Dictionary>> + { + { function.WeatherReportFunctionContract.Name, function.WeatherReportWrapper }, + }); + + agent = agent!.RegisterMiddleware(functionCallMiddleware); + var reply = await agent.SendAsync("What's the weather in Seattle today? today is 2024-01-01"); + #endregion register_function_call_middleware + } + + public async Task TwoAgentWeatherChatTestAsync() + { + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new ArgumentException("AZURE_OPENAI_API_KEY is not set"); + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new ArgumentException("AZURE_OPENAI_ENDPOINT is not set"); + var deploymentName = "gpt-35-turbo-16k"; + var config = new AzureOpenAIConfig(endpoint, deploymentName, key); + #region two_agent_weather_chat + var function = new TypeSafeFunctionCall(); + var assistant = new AssistantAgent( + "assistant", + llmConfig: new ConversableAgentConfig + { + ConfigList = new[] { config }, + FunctionContracts = new[] + { + function.WeatherReportFunctionContract, + }, + }); + + var user = new UserProxyAgent( + name: "user", + functionMap: new Dictionary>> + { + { function.WeatherReportFunctionContract.Name, function.WeatherReportWrapper }, + }); + + await user.InitiateChatAsync(assistant, "what's weather in Seattle today, today is 2024-01-01", 10); + #endregion two_agent_weather_chat + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/GetStartCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/GetStartCodeSnippet.cs new file mode 100644 index 00000000000..fe97152183a --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/GetStartCodeSnippet.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GetStartCodeSnippet.cs + +#region snippet_GetStartCodeSnippet +using AutoGen; +using AutoGen.Core; +using AutoGen.OpenAI; +#endregion snippet_GetStartCodeSnippet + +public class GetStartCodeSnippet +{ + public async Task CodeSnippet1() + { + #region code_snippet_1 + var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); + var gpt35Config = new OpenAIConfig(openAIKey, "gpt-3.5-turbo"); + + var assistantAgent = new AssistantAgent( + name: "assistant", + systemMessage: "You are an assistant that help user to do some tasks.", + llmConfig: new ConversableAgentConfig + { + Temperature = 0, + ConfigList = [gpt35Config], + }) + .RegisterPrintMessage(); // register a hook to print message nicely to console + + // set human input mode to ALWAYS so that user always provide input + var userProxyAgent = new UserProxyAgent( + name: "user", + humanInputMode: HumanInputMode.ALWAYS) + .RegisterPrintMessage(); + + // start the conversation + await userProxyAgent.InitiateChatAsync( + receiver: assistantAgent, + message: "Hey assistant, please do me a favor.", + maxRound: 10); + #endregion code_snippet_1 + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MiddlewareAgentCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MiddlewareAgentCodeSnippet.cs new file mode 100644 index 00000000000..8be026552e3 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MiddlewareAgentCodeSnippet.cs @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MiddlewareAgentCodeSnippet.cs + +using AutoGen.Core; +using System.Text.Json; +using AutoGen.OpenAI; +using FluentAssertions; + +namespace AutoGen.BasicSample.CodeSnippet; + +public class MiddlewareAgentCodeSnippet +{ + public async Task CreateMiddlewareAgentAsync() + { + #region create_middleware_agent_with_original_agent + // Create an agent that always replies "Hello World" + IAgent agent = new DefaultReplyAgent(name: "assistant", defaultReply: "Hello World"); + + // Create a middleware agent on top of default reply agent + var middlewareAgent = new MiddlewareAgent(innerAgent: agent); + middlewareAgent.Use(async (messages, options, agent, ct) => + { + var lastMessage = messages.Last() as TextMessage; + lastMessage.Content = $"[middleware 0] {lastMessage.Content}"; + return await agent.GenerateReplyAsync(messages, options, ct); + }); + + var reply = await middlewareAgent.SendAsync("Hello World"); + reply.GetContent().Should().Be("[middleware 0] Hello World"); + #endregion create_middleware_agent_with_original_agent + + #region register_middleware_agent + middlewareAgent = agent.RegisterMiddleware(async (messages, options, agent, ct) => + { + var lastMessage = messages.Last() as TextMessage; + lastMessage.Content = $"[middleware 0] {lastMessage.Content}"; + return await agent.GenerateReplyAsync(messages, options, ct); + }); + #endregion register_middleware_agent + + #region short_circuit_middleware_agent + // This middleware will short circuit the agent and return the last message directly. + middlewareAgent.Use(async (messages, options, agent, ct) => + { + var lastMessage = messages.Last() as TextMessage; + lastMessage.Content = $"[middleware shortcut]"; + return lastMessage; + }); + #endregion short_circuit_middleware_agent + } + + public async Task RegisterStreamingMiddlewareAsync() + { + IStreamingAgent streamingAgent = default; + #region register_streaming_middleware + var connector = new OpenAIChatRequestMessageConnector(); + var agent = streamingAgent! + .RegisterStreamingMiddleware(connector); + #endregion register_streaming_middleware + } + + public async Task CodeSnippet1() + { + #region code_snippet_1 + // Create an agent that always replies "Hello World" + IAgent agent = new DefaultReplyAgent(name: "assistant", defaultReply: "Hello World"); + + // Create a middleware agent on top of default reply agent + var middlewareAgent = new MiddlewareAgent(innerAgent: agent); + + // Since no middleware is added, middlewareAgent will simply proxy into the inner agent to generate reply. + var reply = await middlewareAgent.SendAsync("Hello World"); + reply.From.Should().Be("assistant"); + reply.GetContent().Should().Be("Hello World"); + #endregion code_snippet_1 + + #region code_snippet_2 + middlewareAgent.Use(async (messages, options, agent, ct) => + { + var lastMessage = messages.Last() as TextMessage; + lastMessage.Content = $"[middleware 0] {lastMessage.Content}"; + return await agent.GenerateReplyAsync(messages, options, ct); + }); + + reply = await middlewareAgent.SendAsync("Hello World"); + reply.Should().BeOfType(); + var textReply = (TextMessage)reply; + textReply.Content.Should().Be("[middleware 0] Hello World"); + #endregion code_snippet_2 + #region code_snippet_2_1 + middlewareAgent = agent.RegisterMiddleware(async (messages, options, agnet, ct) => + { + var lastMessage = messages.Last() as TextMessage; + lastMessage.Content = $"[middleware 0] {lastMessage.Content}"; + return await agent.GenerateReplyAsync(messages, options, ct); + }); + + reply = await middlewareAgent.SendAsync("Hello World"); + reply.GetContent().Should().Be("[middleware 0] Hello World"); + #endregion code_snippet_2_1 + #region code_snippet_3 + middlewareAgent.Use(async (messages, options, agent, ct) => + { + var lastMessage = messages.Last() as TextMessage; + lastMessage.Content = $"[middleware 1] {lastMessage.Content}"; + return await agent.GenerateReplyAsync(messages, options, ct); + }); + + reply = await middlewareAgent.SendAsync("Hello World"); + reply.GetContent().Should().Be("[middleware 0] [middleware 1] Hello World"); + #endregion code_snippet_3 + + #region code_snippet_4 + middlewareAgent.Use(async (messages, options, next, ct) => + { + var lastMessage = messages.Last() as TextMessage; + lastMessage.Content = $"[middleware shortcut]"; + + return lastMessage; + }); + + reply = await middlewareAgent.SendAsync("Hello World"); + reply.GetContent().Should().Be("[middleware shortcut]"); + #endregion code_snippet_4 + + #region retrieve_inner_agent + var innerAgent = middlewareAgent.Agent; + #endregion retrieve_inner_agent + + #region code_snippet_logging_to_console + var agentWithLogging = middlewareAgent.RegisterMiddleware(async (messages, options, agent, ct) => + { + var reply = await agent.GenerateReplyAsync(messages, options, ct); + var formattedMessage = reply.FormatMessage(); + Console.WriteLine(formattedMessage); + + return reply; + }); + #endregion code_snippet_logging_to_console + + #region code_snippet_response_format_forcement + var jsonAgent = middlewareAgent.RegisterMiddleware(async (messages, options, agent, ct) => + { + var maxAttempt = 5; + var reply = await agent.GenerateReplyAsync(messages, options, ct); + while (maxAttempt-- > 0) + { + if (JsonSerializer.Deserialize>(reply.GetContent()) is { } dict) + { + return reply; + } + else + { + await Task.Delay(1000); + var reviewPrompt = @"The format is not json, please modify your response to json format + -- ORIGINAL MESSAGE -- + {reply.Content} + -- END OF ORIGINAL MESSAGE -- + + Reply again with json format."; + reply = await agent.SendAsync(reviewPrompt, messages, ct); + } + } + + throw new Exception("agent fails to generate json response"); + }); + #endregion code_snippet_response_format_forcement + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs new file mode 100644 index 00000000000..6bb9e910730 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MistralAICodeSnippet.cs + +#region using_statement +using AutoGen.Mistral; +using AutoGen.Core; +using AutoGen.Mistral.Extension; +using FluentAssertions; +#endregion using_statement + +namespace AutoGen.BasicSample.CodeSnippet; + +#region weather_function +public partial class MistralAgentFunction +{ + [Function] + public async Task GetWeather(string location) + { + return "The weather in " + location + " is sunny."; + } +} +#endregion weather_function + +internal class MistralAICodeSnippet +{ + public async Task CreateMistralAIClientAsync() + { + #region create_mistral_agent + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new Exception("Missing MISTRAL_API_KEY environment variable"); + var client = new MistralClient(apiKey: apiKey); + var agent = new MistralClientAgent( + client: client, + name: "MistralAI", + model: MistralAIModelID.OPEN_MISTRAL_7B) + .RegisterMessageConnector(); // support more AutoGen built-in message types. + + await agent.SendAsync("Hello, how are you?"); + #endregion create_mistral_agent + + #region streaming_chat + var reply = await agent.GenerateStreamingReplyAsync( + messages: [new TextMessage(Role.User, "Hello, how are you?")] + ); + + await foreach (var message in reply) + { + if (message is TextMessageUpdate textMessageUpdate && textMessageUpdate.Content is string content) + { + Console.WriteLine(content); + } + } + #endregion streaming_chat + } + + public async Task MistralAIChatAgentGetWeatherToolUsageAsync() + { + #region create_mistral_function_call_agent + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new Exception("Missing MISTRAL_API_KEY environment variable"); + var client = new MistralClient(apiKey: apiKey); + var agent = new MistralClientAgent( + client: client, + name: "MistralAI", + model: MistralAIModelID.MISTRAL_SMALL_LATEST) + .RegisterMessageConnector(); // support more AutoGen built-in message types like ToolCallMessage and ToolCallResultMessage + #endregion create_mistral_function_call_agent + + #region create_get_weather_function_call_middleware + var mistralFunctions = new MistralAgentFunction(); + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [mistralFunctions.GetWeatherFunctionContract], + functionMap: new Dictionary>> // with functionMap, the function will be automatically triggered if the tool name matches one of the keys. + { + { mistralFunctions.GetWeatherFunctionContract.Name, mistralFunctions.GetWeather } + }); + #endregion create_get_weather_function_call_middleware + + #region register_function_call_middleware + agent = agent.RegisterMiddleware(functionCallMiddleware); + #endregion register_function_call_middleware + + #region send_message_with_function_call + var reply = await agent.SendAsync("What is the weather in Seattle?"); + reply.GetContent().Should().Be("The weather in Seattle is sunny."); + #endregion send_message_with_function_call + } +} \ No newline at end of file diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs new file mode 100644 index 00000000000..8d129e75157 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAICodeSnippet.cs + +#region using_statement +using AutoGen.Core; +using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; +using Azure.AI.OpenAI; +#endregion using_statement +using FluentAssertions; + +namespace AutoGen.BasicSample.CodeSnippet; +#region weather_function +public partial class Functions +{ + [Function] + public async Task GetWeather(string location) + { + return "The weather in " + location + " is sunny."; + } +} +#endregion weather_function +public partial class OpenAICodeSnippet +{ + [Function] + public async Task GetWeather(string location) + { + return "The weather in " + location + " is sunny."; + } + + public async Task CreateOpenAIChatAgentAsync() + { + #region create_openai_chat_agent + var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); + var modelId = "gpt-3.5-turbo"; + var openAIClient = new OpenAIClient(openAIKey); + + // create an open ai chat agent + var openAIChatAgent = new OpenAIChatAgent( + openAIClient: openAIClient, + name: "assistant", + modelName: modelId, + systemMessage: "You are an assistant that help user to do some tasks."); + + // OpenAIChatAgent supports the following message types: + // - IMessage where ChatRequestMessage is from Azure.AI.OpenAI + + var helloMessage = new ChatRequestUserMessage("Hello"); + + // Use MessageEnvelope.Create to create an IMessage + var chatMessageContent = MessageEnvelope.Create(helloMessage); + var reply = await openAIChatAgent.SendAsync(chatMessageContent); + + // The type of reply is MessageEnvelope where ChatResponseMessage is from Azure.AI.OpenAI + reply.Should().BeOfType>(); + + // You can un-envelop the reply to get the ChatResponseMessage + ChatResponseMessage response = reply.As>().Content; + response.Role.Should().Be(ChatRole.Assistant); + #endregion create_openai_chat_agent + + #region create_openai_chat_agent_streaming + var streamingReply = await openAIChatAgent.GenerateStreamingReplyAsync(new[] { chatMessageContent }); + + await foreach (var streamingMessage in streamingReply) + { + streamingMessage.Should().BeOfType>(); + streamingMessage.As>().Content.Role.Should().Be(ChatRole.Assistant); + } + #endregion create_openai_chat_agent_streaming + + #region register_openai_chat_message_connector + // register message connector to support more message types + var agentWithConnector = openAIChatAgent + .RegisterMessageConnector(); + + // now the agentWithConnector supports more message types + var messages = new IMessage[] + { + MessageEnvelope.Create(new ChatRequestUserMessage("Hello")), + new TextMessage(Role.Assistant, "Hello", from: "user"), + new MultiModalMessage(Role.Assistant, + [ + new TextMessage(Role.Assistant, "Hello", from: "user"), + ], + from: "user"), + new Message(Role.Assistant, "Hello", from: "user"), // Message type is going to be deprecated, please use TextMessage instead + }; + + foreach (var message in messages) + { + reply = await agentWithConnector.SendAsync(message); + + reply.Should().BeOfType(); + reply.As().From.Should().Be("assistant"); + } + #endregion register_openai_chat_message_connector + } + + public async Task OpenAIChatAgentGetWeatherFunctionCallAsync() + { + #region openai_chat_agent_get_weather_function_call + var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); + var modelId = "gpt-3.5-turbo"; + var openAIClient = new OpenAIClient(openAIKey); + + // create an open ai chat agent + var openAIChatAgent = new OpenAIChatAgent( + openAIClient: openAIClient, + name: "assistant", + modelName: modelId, + systemMessage: "You are an assistant that help user to do some tasks.") + .RegisterMessageConnector(); + + #endregion openai_chat_agent_get_weather_function_call + + #region create_function_call_middleware + var functions = new Functions(); + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [functions.GetWeatherFunctionContract], // GetWeatherFunctionContract is auto-generated from the GetWeather function + functionMap: new Dictionary>> + { + { functions.GetWeatherFunctionContract.Name, functions.GetWeatherWrapper } // GetWeatherWrapper is a wrapper function for GetWeather, which is also auto-generated + }); + + openAIChatAgent = openAIChatAgent.RegisterMiddleware(functionCallMiddleware); + #endregion create_function_call_middleware + + #region chat_agent_send_function_call + var reply = await openAIChatAgent.SendAsync("what is the weather in Seattle?"); + reply.GetContent().Should().Be("The weather in Seattle is sunny."); + reply.GetToolCalls().Count.Should().Be(1); + reply.GetToolCalls().First().Should().Be(this.GetWeatherFunctionContract.Name); + #endregion chat_agent_send_function_call + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/PrintMessageMiddlewareCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/PrintMessageMiddlewareCodeSnippet.cs new file mode 100644 index 00000000000..bf4f9c976e2 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/PrintMessageMiddlewareCodeSnippet.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// PrintMessageMiddlewareCodeSnippet.cs + +using AutoGen.Core; +using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; +using Azure; +using Azure.AI.OpenAI; + +namespace AutoGen.BasicSample.CodeSnippet; + +internal class PrintMessageMiddlewareCodeSnippet +{ + public async Task PrintMessageMiddlewareAsync() + { + var config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + var endpoint = new Uri(config.Endpoint); + var openaiClient = new OpenAIClient(endpoint, new AzureKeyCredential(config.ApiKey)); + var agent = new OpenAIChatAgent(openaiClient, "assistant", config.DeploymentName) + .RegisterMessageConnector(); + + #region PrintMessageMiddleware + var agentWithPrintMessageMiddleware = agent + .RegisterPrintMessage(); + + await agentWithPrintMessageMiddleware.SendAsync("write a long poem"); + #endregion PrintMessageMiddleware + } + + public async Task PrintMessageStreamingMiddlewareAsync() + { + var config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + var endpoint = new Uri(config.Endpoint); + var openaiClient = new OpenAIClient(endpoint, new AzureKeyCredential(config.ApiKey)); + + #region print_message_streaming + var streamingAgent = new OpenAIChatAgent(openaiClient, "assistant", config.DeploymentName) + .RegisterMessageConnector() + .RegisterPrintMessage(); + + await streamingAgent.SendAsync("write a long poem"); + #endregion print_message_streaming + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs new file mode 100644 index 00000000000..e498650b6aa --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// RunCodeSnippetCodeSnippet.cs + +#region code_snippet_0_1 +using AutoGen.Core; +using AutoGen.DotnetInteractive; +#endregion code_snippet_0_1 + +namespace AutoGen.BasicSample.CodeSnippet; +public class RunCodeSnippetCodeSnippet +{ + public async Task CodeSnippet1() + { + IAgent agent = default; + + #region code_snippet_1_1 + var workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(workingDirectory); + var interactiveService = new InteractiveService(installingDirectory: workingDirectory); + await interactiveService.StartAsync(workingDirectory: workingDirectory); + #endregion code_snippet_1_1 + + #region code_snippet_1_2 + // register dotnet code block execution hook to an arbitrary agent + var dotnetCodeAgent = agent.RegisterDotnetCodeBlockExectionHook(interactiveService: interactiveService); + + var codeSnippet = @" + ```csharp + Console.WriteLine(""Hello World""); + ```"; + + await dotnetCodeAgent.SendAsync(codeSnippet); + // output: Hello World + #endregion code_snippet_1_2 + + #region code_snippet_1_3 + var content = @" + ```csharp + // This is csharp code snippet + ``` + + ```python + // This is python code snippet + ``` + "; + #endregion code_snippet_1_3 + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/SemanticKernelCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/SemanticKernelCodeSnippet.cs new file mode 100644 index 00000000000..77f93fdf4aa --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/SemanticKernelCodeSnippet.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SemanticKernelCodeSnippet.cs + +using AutoGen.Core; +using AutoGen.SemanticKernel; +using AutoGen.SemanticKernel.Extension; +using FluentAssertions; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace AutoGen.BasicSample.CodeSnippet; + +public class SemanticKernelCodeSnippet +{ + public async Task GetWeather(string location) + { + return "The weather in " + location + " is sunny."; + } + public async Task CreateSemanticKernelAgentAsync() + { + #region create_semantic_kernel_agent + var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); + var modelId = "gpt-3.5-turbo"; + var builder = Kernel.CreateBuilder() + .AddOpenAIChatCompletion(modelId: modelId, apiKey: openAIKey); + var kernel = builder.Build(); + + // create a semantic kernel agent + var semanticKernelAgent = new SemanticKernelAgent( + kernel: kernel, + name: "assistant", + systemMessage: "You are an assistant that help user to do some tasks."); + + // SemanticKernelAgent supports the following message types: + // - IMessage where ChatMessageContent is from Azure.AI.OpenAI + + var helloMessage = new ChatMessageContent(AuthorRole.User, "Hello"); + + // Use MessageEnvelope.Create to create an IMessage + var chatMessageContent = MessageEnvelope.Create(helloMessage); + var reply = await semanticKernelAgent.SendAsync(chatMessageContent); + + // The type of reply is MessageEnvelope where ChatResponseMessage is from Azure.AI.OpenAI + reply.Should().BeOfType>(); + + // You can un-envelop the reply to get the ChatResponseMessage + ChatMessageContent response = reply.As>().Content; + response.Role.Should().Be(AuthorRole.Assistant); + #endregion create_semantic_kernel_agent + + #region create_semantic_kernel_agent_streaming + var streamingReply = await semanticKernelAgent.GenerateStreamingReplyAsync(new[] { chatMessageContent }); + + await foreach (var streamingMessage in streamingReply) + { + streamingMessage.Should().BeOfType>(); + streamingMessage.As>().From.Should().Be("assistant"); + } + #endregion create_semantic_kernel_agent_streaming + } + + public async Task SemanticKernelChatMessageContentConnector() + { + #region register_semantic_kernel_chat_message_content_connector + var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); + var modelId = "gpt-3.5-turbo"; + var builder = Kernel.CreateBuilder() + .AddOpenAIChatCompletion(modelId: modelId, apiKey: openAIKey); + var kernel = builder.Build(); + + // create a semantic kernel agent + var semanticKernelAgent = new SemanticKernelAgent( + kernel: kernel, + name: "assistant", + systemMessage: "You are an assistant that help user to do some tasks."); + + // Register the connector middleware to the kernel agent + var semanticKernelAgentWithConnector = semanticKernelAgent + .RegisterMessageConnector(); + + // now semanticKernelAgentWithConnector supports more message types + IMessage[] messages = [ + MessageEnvelope.Create(new ChatMessageContent(AuthorRole.User, "Hello")), + new TextMessage(Role.Assistant, "Hello", from: "user"), + new MultiModalMessage(Role.Assistant, + [ + new TextMessage(Role.Assistant, "Hello", from: "user"), + ], + from: "user"), + ]; + + foreach (var message in messages) + { + var reply = await semanticKernelAgentWithConnector.SendAsync(message); + + // SemanticKernelChatMessageContentConnector will convert the reply message to TextMessage + reply.Should().BeOfType(); + } + #endregion register_semantic_kernel_chat_message_content_connector + } + +} diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/TypeSafeFunctionCallCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/TypeSafeFunctionCallCodeSnippet.cs new file mode 100644 index 00000000000..50bcd8a8048 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/TypeSafeFunctionCallCodeSnippet.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// TypeSafeFunctionCallCodeSnippet.cs + +using System.Text.Json; +using AutoGen.OpenAI.Extension; +using Azure.AI.OpenAI; +#region weather_report_using_statement +using AutoGen.Core; +#endregion weather_report_using_statement + +#region weather_report +public partial class TypeSafeFunctionCall +{ + /// + /// Get weather report + /// + /// city + /// date + [Function] + public async Task WeatherReport(string city, string date) + { + return $"Weather report for {city} on {date} is sunny"; + } +} +#endregion weather_report + +public partial class TypeSafeFunctionCall +{ + public async Task Consume() + { + #region weather_report_consume + var functionInstance = new TypeSafeFunctionCall(); + + // Get the generated function definition + FunctionDefinition functionDefiniton = functionInstance.WeatherReportFunctionContract.ToOpenAIFunctionDefinition(); + + // Get the generated function wrapper + Func> functionWrapper = functionInstance.WeatherReportWrapper; + + // ... + #endregion weather_report_consume + } +} +#region code_snippet_3 +// file: FunctionCall.cs + +public partial class TypeSafeFunctionCall +{ + /// + /// convert input to upper case + /// + /// input + [Function] + public async Task UpperCase(string input) + { + var result = input.ToUpper(); + return result; + } +} +#endregion code_snippet_3 + +public class TypeSafeFunctionCallCodeSnippet +{ + public async Task UpperCase(string input) + { + var result = input.ToUpper(); + return result; + } + + #region code_snippet_1 + // file: FunctionDefinition.generated.cs + public FunctionDefinition UpperCaseFunction + { + get => new FunctionDefinition + { + Name = @"UpperCase", + Description = "convert input to upper case", + Parameters = BinaryData.FromObjectAsJson(new + { + Type = "object", + Properties = new + { + input = new + { + Type = @"string", + Description = @"input", + }, + }, + Required = new[] + { + "input", + }, + }, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }) + }; + } + #endregion code_snippet_1 + + #region code_snippet_2 + // file: FunctionDefinition.generated.cs + private class UpperCaseSchema + { + public string input { get; set; } + } + + public Task UpperCaseWrapper(string arguments) + { + var schema = JsonSerializer.Deserialize( + arguments, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }); + + return UpperCase(schema.input); + } + #endregion code_snippet_2 +} diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/UserProxyAgentCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/UserProxyAgentCodeSnippet.cs new file mode 100644 index 00000000000..85aecae959e --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/UserProxyAgentCodeSnippet.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// UserProxyAgentCodeSnippet.cs +using AutoGen.Core; + +namespace AutoGen.BasicSample.CodeSnippet; + +public class UserProxyAgentCodeSnippet +{ + public async Task CodeSnippet1() + { + #region code_snippet_1 + // create a user proxy agent which always ask user for input + var agent = new UserProxyAgent( + name: "user", + humanInputMode: HumanInputMode.ALWAYS); + + await agent.SendAsync("hello"); + #endregion code_snippet_1 + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/Example01_AssistantAgent.cs b/dotnet/sample/AutoGen.BasicSamples/Example01_AssistantAgent.cs new file mode 100644 index 00000000000..8797bda8313 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/Example01_AssistantAgent.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Example01_AssistantAgent.cs + +using AutoGen.Core; +using AutoGen; +using AutoGen.BasicSample; +using FluentAssertions; + +/// +/// This example shows the basic usage of class. +/// +public static class Example01_AssistantAgent +{ + public static async Task RunAsync() + { + var gpt35 = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + var config = new ConversableAgentConfig + { + Temperature = 0, + ConfigList = [gpt35], + }; + + // create assistant agent + var assistantAgent = new AssistantAgent( + name: "assistant", + systemMessage: "You convert what user said to all uppercase.", + llmConfig: config) + .RegisterPrintMessage(); + + // talk to the assistant agent + var reply = await assistantAgent.SendAsync("hello world"); + reply.Should().BeOfType(); + reply.GetContent().Should().Be("HELLO WORLD"); + + // to carry on the conversation, pass the previous conversation history to the next call + var conversationHistory = new List + { + new TextMessage(Role.User, "hello world"), // first message + reply, // reply from assistant agent + }; + + reply = await assistantAgent.SendAsync("hello world again", conversationHistory); + reply.Should().BeOfType(); + reply.GetContent().Should().Be("HELLO WORLD AGAIN"); + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/Example02_TwoAgent_MathChat.cs b/dotnet/sample/AutoGen.BasicSamples/Example02_TwoAgent_MathChat.cs new file mode 100644 index 00000000000..8d42b9d0504 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/Example02_TwoAgent_MathChat.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Example02_TwoAgent_MathChat.cs + +using AutoGen.Core; +using AutoGen; +using AutoGen.BasicSample; +using FluentAssertions; +public static class Example02_TwoAgent_MathChat +{ + public static async Task RunAsync() + { + #region code_snippet_1 + // get gpt-3.5-turbo config + var gpt35 = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + + // create teacher agent + // teacher agent will create math questions + var teacher = new AssistantAgent( + name: "teacher", + systemMessage: @"You are a teacher that create pre-school math question for student and check answer. + If the answer is correct, you terminate conversation by saying [TERMINATE]. + If the answer is wrong, you ask student to fix it.", + llmConfig: new ConversableAgentConfig + { + Temperature = 0, + ConfigList = [gpt35], + }) + .RegisterPostProcess(async (_, reply, _) => + { + if (reply.GetContent()?.ToLower().Contains("terminate") is true) + { + return new TextMessage(Role.Assistant, GroupChatExtension.TERMINATE, from: reply.From); + } + + return reply; + }) + .RegisterPrintMessage(); + + // create student agent + // student agent will answer the math questions + var student = new AssistantAgent( + name: "student", + systemMessage: "You are a student that answer question from teacher", + llmConfig: new ConversableAgentConfig + { + Temperature = 0, + ConfigList = [gpt35], + }) + .RegisterPrintMessage(); + + // start the conversation + var conversation = await student.InitiateChatAsync( + receiver: teacher, + message: "Hey teacher, please create math question for me.", + maxRound: 10); + + // output + // Message from teacher + // -------------------- + // content: Of course!Here's a math question for you: + // + // What is 2 + 3 ? + // -------------------- + // + // Message from student + // -------------------- + // content: The sum of 2 and 3 is 5. + // -------------------- + // + // Message from teacher + // -------------------- + // content: [GROUPCHAT_TERMINATE] + // -------------------- + #endregion code_snippet_1 + + conversation.Count().Should().BeLessThan(10); + conversation.Last().IsGroupChatTerminateMessage().Should().BeTrue(); + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/Example03_Agent_FunctionCall.cs b/dotnet/sample/AutoGen.BasicSamples/Example03_Agent_FunctionCall.cs new file mode 100644 index 00000000000..bfb8d71095b --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/Example03_Agent_FunctionCall.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Example03_Agent_FunctionCall.cs + +using AutoGen; +using AutoGen.Core; +using AutoGen.BasicSample; +using FluentAssertions; + +/// +/// This example shows how to add type-safe function call to an agent. +/// +public partial class Example03_Agent_FunctionCall +{ + /// + /// upper case the message when asked. + /// + /// + [Function] + public async Task UpperCase(string message) + { + return message.ToUpper(); + } + + /// + /// Concatenate strings. + /// + /// strings to concatenate + [Function] + public async Task ConcatString(string[] strings) + { + return string.Join(" ", strings); + } + + /// + /// calculate tax + /// + /// price, should be an integer + /// tax rate, should be in range (0, 1) + [FunctionAttribute] + public async Task CalculateTax(int price, float taxRate) + { + return $"tax is {price * taxRate}"; + } + + public static async Task RunAsync() + { + var instance = new Example03_Agent_FunctionCall(); + var gpt35 = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + + // AutoGen makes use of AutoGen.SourceGenerator to automatically generate FunctionDefinition and FunctionCallWrapper for you. + // The FunctionDefinition will be created based on function signature and XML documentation. + // The return type of type-safe function needs to be Task. And to get the best performance, please try only use primitive types and arrays of primitive types as parameters. + var config = new ConversableAgentConfig + { + Temperature = 0, + ConfigList = [gpt35], + FunctionContracts = new[] + { + instance.ConcatStringFunctionContract, + instance.UpperCaseFunctionContract, + instance.CalculateTaxFunctionContract, + }, + }; + + var agent = new AssistantAgent( + name: "agent", + systemMessage: "You are a helpful AI assistant", + llmConfig: config, + functionMap: new Dictionary>> + { + { nameof(ConcatString), instance.ConcatStringWrapper }, + { nameof(UpperCase), instance.UpperCaseWrapper }, + { nameof(CalculateTax), instance.CalculateTaxWrapper }, + }) + .RegisterPrintMessage(); + + // talk to the assistant agent + var upperCase = await agent.SendAsync("convert to upper case: hello world"); + upperCase.GetContent()?.Should().Be("HELLO WORLD"); + upperCase.Should().BeOfType>(); + upperCase.GetToolCalls().Should().HaveCount(1); + upperCase.GetToolCalls().First().FunctionName.Should().Be(nameof(UpperCase)); + + var concatString = await agent.SendAsync("concatenate strings: a, b, c, d, e"); + concatString.GetContent()?.Should().Be("a b c d e"); + concatString.Should().BeOfType>(); + concatString.GetToolCalls().Should().HaveCount(1); + concatString.GetToolCalls().First().FunctionName.Should().Be(nameof(ConcatString)); + + var calculateTax = await agent.SendAsync("calculate tax: 100, 0.1"); + calculateTax.GetContent().Should().Be("tax is 10"); + calculateTax.Should().BeOfType>(); + calculateTax.GetToolCalls().Should().HaveCount(1); + calculateTax.GetToolCalls().First().FunctionName.Should().Be(nameof(CalculateTax)); + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/Example04_Dynamic_GroupChat_Coding_Task.cs b/dotnet/sample/AutoGen.BasicSamples/Example04_Dynamic_GroupChat_Coding_Task.cs new file mode 100644 index 00000000000..d9489e522e6 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/Example04_Dynamic_GroupChat_Coding_Task.cs @@ -0,0 +1,263 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Example04_Dynamic_GroupChat_Coding_Task.cs + +using AutoGen; +using AutoGen.Core; +using AutoGen.BasicSample; +using AutoGen.DotnetInteractive; +using AutoGen.OpenAI; +using FluentAssertions; + +public partial class Example04_Dynamic_GroupChat_Coding_Task +{ + public static async Task RunAsync() + { + var instance = new Example04_Dynamic_GroupChat_Coding_Task(); + + // setup dotnet interactive + var workDir = Path.Combine(Path.GetTempPath(), "InteractiveService"); + if (!Directory.Exists(workDir)) + Directory.CreateDirectory(workDir); + + using var service = new InteractiveService(workDir); + var dotnetInteractiveFunctions = new DotnetInteractiveFunction(service); + + var result = Path.Combine(workDir, "result.txt"); + if (File.Exists(result)) + File.Delete(result); + + await service.StartAsync(workDir, default); + + var gptConfig = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + + var helperAgent = new GPTAgent( + name: "helper", + systemMessage: "You are a helpful AI assistant", + temperature: 0f, + config: gptConfig); + + var groupAdmin = new GPTAgent( + name: "groupAdmin", + systemMessage: "You are the admin of the group chat", + temperature: 0f, + config: gptConfig); + + var userProxy = new UserProxyAgent(name: "user", defaultReply: GroupChatExtension.TERMINATE, humanInputMode: HumanInputMode.NEVER) + .RegisterPrintMessage(); + + // Create admin agent + var admin = new AssistantAgent( + name: "admin", + systemMessage: """ + You are a manager who takes coding problem from user and resolve problem by splitting them into small tasks and assign each task to the most appropriate agent. + Here's available agents who you can assign task to: + - coder: write dotnet code to resolve task + - runner: run dotnet code from coder + + The workflow is as follows: + - You take the coding problem from user + - You break the problem into small tasks. For each tasks you first ask coder to write code to resolve the task. Once the code is written, you ask runner to run the code. + - Once a small task is resolved, you summarize the completed steps and create the next step. + - You repeat the above steps until the coding problem is resolved. + + You can use the following json format to assign task to agents: + ```task + { + "to": "{agent_name}", + "task": "{a short description of the task}", + "context": "{previous context from scratchpad}" + } + ``` + + If you need to ask user for extra information, you can use the following format: + ```ask + { + "question": "{question}" + } + ``` + + Once the coding problem is resolved, summarize each steps and results and send the summary to the user using the following format: + ```summary + { + "problem": "{coding problem}", + "steps": [ + { + "step": "{step}", + "result": "{result}" + } + ] + } + ``` + + Your reply must contain one of [task|ask|summary] to indicate the type of your message. + """, + llmConfig: new ConversableAgentConfig + { + Temperature = 0, + ConfigList = [gptConfig], + }) + .RegisterPrintMessage(); + + // create coder agent + // The coder agent is a composite agent that contains dotnet coder, code reviewer and nuget agent. + // The dotnet coder write dotnet code to resolve the task. + // The code reviewer review the code block from coder's reply. + // The nuget agent install nuget packages if there's any. + var coderAgent = new GPTAgent( + name: "coder", + systemMessage: @"You act as dotnet coder, you write dotnet code to resolve task. Once you finish writing code, ask runner to run the code for you. + +Here're some rules to follow on writing dotnet code: +- put code between ```csharp and ``` +- When creating http client, use `var httpClient = new HttpClient()`. Don't use `using var httpClient = new HttpClient()` because it will cause error when running the code. +- Try to use `var` instead of explicit type. +- Try avoid using external library, use .NET Core library instead. +- Use top level statement to write code. +- Always print out the result to console. Don't write code that doesn't print out anything. + +If you need to install nuget packages, put nuget packages in the following format: +```nuget +nuget_package_name +``` + +If your code is incorrect, Fix the error and send the code again. + +Here's some externel information +- The link to mlnet repo is: https://github.com/dotnet/machinelearning. you don't need a token to use github pr api. Make sure to include a User-Agent header, otherwise github will reject it. +", + config: gptConfig, + temperature: 0.4f) + .RegisterPrintMessage(); + + // code reviewer agent will review if code block from coder's reply satisfy the following conditions: + // - There's only one code block + // - The code block is csharp code block + // - The code block is top level statement + // - The code block is not using declaration + var codeReviewAgent = new GPTAgent( + name: "reviewer", + systemMessage: """ + You are a code reviewer who reviews code from coder. You need to check if the code satisfy the following conditions: + - The reply from coder contains at least one code block, e.g ```csharp and ``` + - There's only one code block and it's csharp code block + - The code block is not inside a main function. a.k.a top level statement + - The code block is not using declaration when creating http client + + You don't check the code style, only check if the code satisfy the above conditions. + + Put your comment between ```review and ```, if the code satisfies all conditions, put APPROVED in review.result field. Otherwise, put REJECTED along with comments. make sure your comment is clear and easy to understand. + + ## Example 1 ## + ```review + comment: The code satisfies all conditions. + result: APPROVED + ``` + + ## Example 2 ## + ```review + comment: The code is inside main function. Please rewrite the code in top level statement. + result: REJECTED + ``` + + """, + config: gptConfig, + temperature: 0f) + .RegisterPrintMessage(); + + // create runner agent + // The runner agent will run the code block from coder's reply. + // It runs dotnet code using dotnet interactive service hook. + // It also truncate the output if the output is too long. + var runner = new AssistantAgent( + name: "runner", + defaultReply: "No code available, coder, write code please") + .RegisterDotnetCodeBlockExectionHook(interactiveService: service) + .RegisterMiddleware(async (msgs, option, agent, ct) => + { + var mostRecentCoderMessage = msgs.LastOrDefault(x => x.From == "coder") ?? throw new Exception("No coder message found"); + return await agent.GenerateReplyAsync(new[] { mostRecentCoderMessage }, option, ct); + }) + .RegisterPrintMessage(); + + var adminToCoderTransition = Transition.Create(admin, coderAgent, async (from, to, messages) => + { + // the last message should be from admin + var lastMessage = messages.Last(); + if (lastMessage.From != admin.Name) + { + return false; + } + + return true; + }); + var coderToReviewerTransition = Transition.Create(coderAgent, codeReviewAgent); + var adminToRunnerTransition = Transition.Create(admin, runner, async (from, to, messages) => + { + // the last message should be from admin + var lastMessage = messages.Last(); + if (lastMessage.From != admin.Name) + { + return false; + } + + // the previous messages should contain a message from coder + var coderMessage = messages.FirstOrDefault(x => x.From == coderAgent.Name); + if (coderMessage is null) + { + return false; + } + + return true; + }); + + var runnerToAdminTransition = Transition.Create(runner, admin); + + var reviewerToAdminTransition = Transition.Create(codeReviewAgent, admin); + + var adminToUserTransition = Transition.Create(admin, userProxy, async (from, to, messages) => + { + // the last message should be from admin + var lastMessage = messages.Last(); + if (lastMessage.From != admin.Name) + { + return false; + } + + return true; + }); + + var userToAdminTransition = Transition.Create(userProxy, admin); + + var workflow = new Graph( + [ + adminToCoderTransition, + coderToReviewerTransition, + reviewerToAdminTransition, + adminToRunnerTransition, + runnerToAdminTransition, + adminToUserTransition, + userToAdminTransition, + ]); + + // create group chat + var groupChat = new GroupChat( + admin: groupAdmin, + members: [admin, coderAgent, runner, codeReviewAgent, userProxy], + workflow: workflow); + + // task 1: retrieve the most recent pr from mlnet and save it in result.txt + var groupChatManager = new GroupChatManager(groupChat); + await userProxy.SendAsync(groupChatManager, "Retrieve the most recent pr from mlnet and save it in result.txt", maxRound: 30); + File.Exists(result).Should().BeTrue(); + + // task 2: calculate the 39th fibonacci number + var answer = 63245986; + // clear the result file + File.Delete(result); + + var conversationHistory = await userProxy.InitiateChatAsync(groupChatManager, "What's the 39th of fibonacci number? Save the result in result.txt", maxRound: 10); + File.Exists(result).Should().BeTrue(); + var resultContent = File.ReadAllText(result); + resultContent.Should().Contain(answer.ToString()); + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/Example05_Dalle_And_GPT4V.cs b/dotnet/sample/AutoGen.BasicSamples/Example05_Dalle_And_GPT4V.cs new file mode 100644 index 00000000000..9fccd7ab385 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/Example05_Dalle_And_GPT4V.cs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Example05_Dalle_And_GPT4V.cs + +using AutoGen; +using AutoGen.Core; +using Azure.AI.OpenAI; +using FluentAssertions; +using autogen = AutoGen.LLMConfigAPI; + +public partial class Example05_Dalle_And_GPT4V +{ + private readonly OpenAIClient openAIClient; + + public Example05_Dalle_And_GPT4V(OpenAIClient openAIClient) + { + this.openAIClient = openAIClient; + } + + /// + /// Generate image from prompt using DALL-E. + /// + /// prompt with feedback + /// + [Function] + public async Task GenerateImage(string prompt) + { + // TODO + // generate image from prompt using DALL-E + // and return url. + var option = new ImageGenerationOptions + { + Size = ImageSize.Size1024x1024, + Style = ImageGenerationStyle.Vivid, + ImageCount = 1, + Prompt = prompt, + Quality = ImageGenerationQuality.Standard, + DeploymentName = "dall-e-3", + }; + + var imageResponse = await openAIClient.GetImageGenerationsAsync(option); + var imageUrl = imageResponse.Value.Data.First().Url.OriginalString; + + return $@"// ignore this line [IMAGE_GENERATION] +The image is generated from prompt {prompt} + +{imageUrl}"; + } + + public static async Task RunAsync() + { + // This example shows how to use DALL-E and GPT-4V to generate image from prompt and feedback. + // The DALL-E agent will generate image from prompt. + // The GPT-4V agent will provide feedback to DALL-E agent to help it generate better image. + // The conversation will be terminated when the image satisfies the condition. + // The image will be saved to image.jpg in current directory. + + // get OpenAI Key and create config + var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); + var gpt35Config = autogen.GetOpenAIConfigList(openAIKey, new[] { "gpt-3.5-turbo" }); + var gpt4vConfig = autogen.GetOpenAIConfigList(openAIKey, new[] { "gpt-4-vision-preview" }); + var openAIClient = new OpenAIClient(openAIKey); + var instance = new Example05_Dalle_And_GPT4V(openAIClient); + var imagePath = Path.Combine(Environment.CurrentDirectory, "image.jpg"); + if (File.Exists(imagePath)) + { + File.Delete(imagePath); + } + + var dalleAgent = new AssistantAgent( + name: "dalle", + systemMessage: "You are a DALL-E agent that generate image from prompt, when conversation is terminated, return the most recent image url", + llmConfig: new ConversableAgentConfig + { + Temperature = 0, + ConfigList = gpt35Config, + FunctionContracts = new[] + { + instance.GenerateImageFunctionContract, + }, + }, + functionMap: new Dictionary>> + { + { nameof(GenerateImage), instance.GenerateImageWrapper }, + }) + .RegisterMiddleware(async (msgs, option, agent, ct) => + { + // if last message contains [TERMINATE], then find the last image url and terminate the conversation + if (msgs.Last().GetContent()?.Contains("TERMINATE") is true) + { + var lastMessageWithImage = msgs.Last(msg => msg is ImageMessage) as ImageMessage; + var lastImageUrl = lastMessageWithImage.Url; + Console.WriteLine($"download image from {lastImageUrl} to {imagePath}"); + var httpClient = new HttpClient(); + var imageBytes = await httpClient.GetByteArrayAsync(lastImageUrl); + File.WriteAllBytes(imagePath, imageBytes); + + var messageContent = $@"{GroupChatExtension.TERMINATE} + +{lastImageUrl}"; + return new TextMessage(Role.Assistant, messageContent) + { + From = "dalle", + }; + } + + var reply = await agent.GenerateReplyAsync(msgs, option, ct); + + if (reply.GetContent() is string content && content.Contains("IMAGE_GENERATION")) + { + var imageUrl = content.Split("\n").Last(); + var imageMessage = new ImageMessage(Role.Assistant, imageUrl, from: reply.From); + + return imageMessage; + } + else + { + return reply; + } + }) + .RegisterPrintMessage(); + + var gpt4VAgent = new AssistantAgent( + name: "gpt4v", + systemMessage: @"You are a critism that provide feedback to DALL-E agent. +Carefully check the image generated by DALL-E agent and provide feedback. +If the image satisfies the condition, then terminate the conversation by saying [TERMINATE]. +Otherwise, provide detailed feedback to DALL-E agent so it can generate better image. + +The image should satisfy the following conditions: +- There should be a cat and a mouse in the image +- The cat should be chasing after the mouse +", + llmConfig: new ConversableAgentConfig + { + Temperature = 0, + ConfigList = gpt4vConfig, + }) + .RegisterPrintMessage(); + + IEnumerable conversation = new List() + { + new TextMessage(Role.User, "Hey dalle, please generate image from prompt: English short hair blue cat chase after a mouse") + }; + var maxRound = 20; + await gpt4VAgent.InitiateChatAsync( + receiver: dalleAgent, + message: "Hey dalle, please generate image from prompt: English short hair blue cat chase after a mouse", + maxRound: maxRound); + + File.Exists(imagePath).Should().BeTrue(); + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/Example06_UserProxyAgent.cs b/dotnet/sample/AutoGen.BasicSamples/Example06_UserProxyAgent.cs new file mode 100644 index 00000000000..dd3b5a67192 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/Example06_UserProxyAgent.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Example06_UserProxyAgent.cs +using AutoGen.Core; +using AutoGen.OpenAI; + +namespace AutoGen.BasicSample; + +public static class Example06_UserProxyAgent +{ + public static async Task RunAsync() + { + var gpt35 = LLMConfiguration.GetOpenAIGPT3_5_Turbo(); + + var assistantAgent = new GPTAgent( + name: "assistant", + systemMessage: "You are an assistant that help user to do some tasks.", + config: gpt35) + .RegisterPrintMessage(); + + // set human input mode to ALWAYS so that user always provide input + var userProxyAgent = new UserProxyAgent( + name: "user", + humanInputMode: HumanInputMode.ALWAYS) + .RegisterPrintMessage(); + + // start the conversation + await userProxyAgent.InitiateChatAsync( + receiver: assistantAgent, + message: "Hey assistant, please help me to do some tasks.", + maxRound: 10); + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs b/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs new file mode 100644 index 00000000000..6b1dc0965ee --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs @@ -0,0 +1,377 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs + +using System.Text; +using System.Text.Json; +using AutoGen; +using AutoGen.BasicSample; +using AutoGen.DotnetInteractive; +using AutoGen.Core; +using AutoGen.OpenAI; +using FluentAssertions; + +public partial class Example07_Dynamic_GroupChat_Calculate_Fibonacci +{ + #region reviewer_function + public struct CodeReviewResult + { + public bool HasMultipleCodeBlocks { get; set; } + public bool IsTopLevelStatement { get; set; } + public bool IsDotnetCodeBlock { get; set; } + public bool IsPrintResultToConsole { get; set; } + } + + /// + /// review code block + /// + /// true if there're multipe csharp code blocks + /// true if the code is in top level statement + /// true if the code block is csharp code block + /// true if the code block print out result to console + [Function] + public async Task ReviewCodeBlock( + bool hasMultipleCodeBlocks, + bool isTopLevelStatement, + bool isDotnetCodeBlock, + bool isPrintResultToConsole) + { + var obj = new CodeReviewResult + { + HasMultipleCodeBlocks = hasMultipleCodeBlocks, + IsTopLevelStatement = isTopLevelStatement, + IsDotnetCodeBlock = isDotnetCodeBlock, + IsPrintResultToConsole = isPrintResultToConsole, + }; + + return JsonSerializer.Serialize(obj); + } + #endregion reviewer_function + + #region create_coder + public static async Task CreateCoderAgentAsync() + { + var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + var coder = new GPTAgent( + name: "coder", + systemMessage: @"You act as dotnet coder, you write dotnet code to resolve task. Once you finish writing code, ask runner to run the code for you. + + Here're some rules to follow on writing dotnet code: + - put code between ```csharp and ``` + - Avoid adding `using` keyword when creating disposable object. e.g `var httpClient = new HttpClient()` + - Try to use `var` instead of explicit type. + - Try avoid using external library, use .NET Core library instead. + - Use top level statement to write code. + - Always print out the result to console. Don't write code that doesn't print out anything. + + If you need to install nuget packages, put nuget packages in the following format: + ```nuget + nuget_package_name + ``` + + If your code is incorrect, runner will tell you the error message. Fix the error and send the code again.", + config: gpt3Config, + temperature: 0.4f) + .RegisterPrintMessage(); + + return coder; + } + #endregion create_coder + + #region create_runner + public static async Task CreateRunnerAgentAsync(InteractiveService service) + { + var runner = new AssistantAgent( + name: "runner", + systemMessage: "You run dotnet code", + defaultReply: "No code available.") + .RegisterDotnetCodeBlockExectionHook(interactiveService: service) + .RegisterReply(async (msgs, _) => + { + if (msgs.Count() == 0) + { + return new TextMessage(Role.Assistant, "No code available. Coder please write code"); + } + + return null; + }) + .RegisterPreProcess(async (msgs, _) => + { + // retrieve the most recent message from coder + var coderMsg = msgs.LastOrDefault(msg => msg.From == "coder"); + if (coderMsg is null) + { + return Enumerable.Empty(); + } + else + { + return new[] { coderMsg }; + } + }) + .RegisterPrintMessage(); + + return runner; + } + #endregion create_runner + + #region create_admin + public static async Task CreateAdminAsync() + { + var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + var admin = new GPTAgent( + name: "admin", + systemMessage: "You are group admin, terminate the group chat once task is completed by saying [TERMINATE] plus the final answer", + temperature: 0, + config: gpt3Config) + .RegisterPostProcess(async (_, reply, _) => + { + if (reply is TextMessage textMessage && textMessage.Content.Contains("TERMINATE") is true) + { + var content = $"{textMessage.Content}\n\n {GroupChatExtension.TERMINATE}"; + + return new TextMessage(Role.Assistant, content, from: reply.From); + } + + return reply; + }); + + return admin; + } + #endregion create_admin + + #region create_reviewer + public static async Task CreateReviewerAgentAsync() + { + var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + var functions = new Example07_Dynamic_GroupChat_Calculate_Fibonacci(); + var reviewer = new GPTAgent( + name: "code_reviewer", + systemMessage: @"You review code block from coder", + config: gpt3Config, + functions: [functions.ReviewCodeBlockFunction], + functionMap: new Dictionary>>() + { + { nameof(ReviewCodeBlock), functions.ReviewCodeBlockWrapper }, + }) + .RegisterMiddleware(async (msgs, option, innerAgent, ct) => + { + var maxRetry = 3; + var reply = await innerAgent.GenerateReplyAsync(msgs, option, ct); + while (maxRetry-- > 0) + { + if (reply.GetToolCalls() is var toolCalls && toolCalls.Count() == 1 && toolCalls[0].FunctionName == nameof(ReviewCodeBlock)) + { + var toolCallResult = reply.GetContent(); + var reviewResultObj = JsonSerializer.Deserialize(toolCallResult); + var reviews = new List(); + if (reviewResultObj.HasMultipleCodeBlocks) + { + var fixCodeBlockPrompt = @"There're multiple code blocks, please combine them into one code block"; + reviews.Add(fixCodeBlockPrompt); + } + + if (reviewResultObj.IsDotnetCodeBlock is false) + { + var fixCodeBlockPrompt = @"The code block is not csharp code block, please write dotnet code only"; + reviews.Add(fixCodeBlockPrompt); + } + + if (reviewResultObj.IsTopLevelStatement is false) + { + var fixCodeBlockPrompt = @"The code is not top level statement, please rewrite your dotnet code using top level statement"; + reviews.Add(fixCodeBlockPrompt); + } + + if (reviewResultObj.IsPrintResultToConsole is false) + { + var fixCodeBlockPrompt = @"The code doesn't print out result to console, please print out result to console"; + reviews.Add(fixCodeBlockPrompt); + } + + if (reviews.Count > 0) + { + var sb = new StringBuilder(); + sb.AppendLine("There're some comments from code reviewer, please fix these comments"); + foreach (var review in reviews) + { + sb.AppendLine($"- {review}"); + } + + return new TextMessage(Role.Assistant, sb.ToString(), from: "code_reviewer"); + } + else + { + var msg = new TextMessage(Role.Assistant, "The code looks good, please ask runner to run the code for you.") + { + From = "code_reviewer", + }; + + return msg; + } + } + else + { + var originalContent = reply.GetContent(); + var prompt = $@"Please convert the content to ReviewCodeBlock function arguments. + + ## Original Content + {originalContent}"; + + reply = await innerAgent.SendAsync(prompt, msgs, ct); + } + } + + throw new Exception("Failed to review code block"); + }) + .RegisterPrintMessage(); + + return reviewer; + } + #endregion create_reviewer + + public static async Task RunWorkflowAsync() + { + long the39thFibonacciNumber = 63245986; + var workDir = Path.Combine(Path.GetTempPath(), "InteractiveService"); + if (!Directory.Exists(workDir)) + Directory.CreateDirectory(workDir); + + using var service = new InteractiveService(workDir); + var dotnetInteractiveFunctions = new DotnetInteractiveFunction(service); + + await service.StartAsync(workDir, default); + + #region create_workflow + var reviewer = await CreateReviewerAgentAsync(); + var coder = await CreateCoderAgentAsync(); + var runner = await CreateRunnerAgentAsync(service); + var admin = await CreateAdminAsync(); + + var admin2CoderTransition = Transition.Create(admin, coder); + var coder2ReviewerTransition = Transition.Create(coder, reviewer); + var reviewer2RunnerTransition = Transition.Create( + from: reviewer, + to: runner, + canTransitionAsync: async (from, to, messages) => + { + var lastMessage = messages.Last(); + if (lastMessage is TextMessage textMessage && textMessage.Content.ToLower().Contains("the code looks good, please ask runner to run the code for you.") is true) + { + // ask runner to run the code + return true; + } + + return false; + }); + var reviewer2CoderTransition = Transition.Create( + from: reviewer, + to: coder, + canTransitionAsync: async (from, to, messages) => + { + var lastMessage = messages.Last(); + if (lastMessage is TextMessage textMessage && textMessage.Content.ToLower().Contains("there're some comments from code reviewer, please fix these comments") is true) + { + // ask coder to fix the code based on reviewer's comments + return true; + } + + return false; + }); + + var runner2CoderTransition = Transition.Create( + from: runner, + to: coder, + canTransitionAsync: async (from, to, messages) => + { + var lastMessage = messages.Last(); + if (lastMessage is TextMessage textMessage && textMessage.Content.ToLower().Contains("error") is true) + { + // ask coder to fix the error + return true; + } + + return false; + }); + var runner2AdminTransition = Transition.Create(runner, admin); + + var workflow = new Graph( + [ + admin2CoderTransition, + coder2ReviewerTransition, + reviewer2RunnerTransition, + reviewer2CoderTransition, + runner2CoderTransition, + runner2AdminTransition, + ]); + #endregion create_workflow + + #region create_group_chat_with_workflow + var groupChat = new GroupChat( + admin: admin, + workflow: workflow, + members: + [ + admin, + coder, + runner, + reviewer, + ]); + + admin.SendIntroduction("Welcome to my group, work together to resolve my task", groupChat); + coder.SendIntroduction("I will write dotnet code to resolve task", groupChat); + reviewer.SendIntroduction("I will review dotnet code", groupChat); + runner.SendIntroduction("I will run dotnet code once the review is done", groupChat); + + var groupChatManager = new GroupChatManager(groupChat); + var conversationHistory = await admin.InitiateChatAsync(groupChatManager, "What's the 39th of fibonacci number?", maxRound: 10); + #endregion create_group_chat_with_workflow + // the last message is from admin, which is the termination message + var lastMessage = conversationHistory.Last(); + lastMessage.From.Should().Be("admin"); + lastMessage.IsGroupChatTerminateMessage().Should().BeTrue(); + lastMessage.Should().BeOfType(); + lastMessage.GetContent().Should().Contain(the39thFibonacciNumber.ToString()); + } + + public static async Task RunAsync() + { + long the39thFibonacciNumber = 63245986; + var workDir = Path.Combine(Path.GetTempPath(), "InteractiveService"); + if (!Directory.Exists(workDir)) + Directory.CreateDirectory(workDir); + + using var service = new InteractiveService(workDir); + var dotnetInteractiveFunctions = new DotnetInteractiveFunction(service); + + await service.StartAsync(workDir, default); + #region create_group_chat + var reviewer = await CreateReviewerAgentAsync(); + var coder = await CreateCoderAgentAsync(); + var runner = await CreateRunnerAgentAsync(service); + var admin = await CreateAdminAsync(); + var groupChat = new GroupChat( + admin: admin, + members: + [ + admin, + coder, + runner, + reviewer, + ]); + + admin.SendIntroduction("Welcome to my group, work together to resolve my task", groupChat); + coder.SendIntroduction("I will write dotnet code to resolve task", groupChat); + reviewer.SendIntroduction("I will review dotnet code", groupChat); + runner.SendIntroduction("I will run dotnet code once the review is done", groupChat); + + var groupChatManager = new GroupChatManager(groupChat); + var conversationHistory = await admin.InitiateChatAsync(groupChatManager, "What's the 39th of fibonacci number?", maxRound: 10); + + // the last message is from admin, which is the termination message + var lastMessage = conversationHistory.Last(); + lastMessage.From.Should().Be("admin"); + lastMessage.IsGroupChatTerminateMessage().Should().BeTrue(); + lastMessage.Should().BeOfType(); + lastMessage.GetContent().Should().Contain(the39thFibonacciNumber.ToString()); + #endregion create_group_chat + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/Example08_LMStudio.cs b/dotnet/sample/AutoGen.BasicSamples/Example08_LMStudio.cs new file mode 100644 index 00000000000..cce33011762 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/Example08_LMStudio.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Example08_LMStudio.cs + +#region lmstudio_using_statements +using AutoGen.Core; +using AutoGen.LMStudio; +#endregion lmstudio_using_statements + +namespace AutoGen.BasicSample; + +public class Example08_LMStudio +{ + public static async Task RunAsync() + { + #region lmstudio_example_1 + var config = new LMStudioConfig("localhost", 1234); + var lmAgent = new LMStudioAgent("asssistant", config: config) + .RegisterPrintMessage(); + + await lmAgent.SendAsync("Can you write a piece of C# code to calculate 100th of fibonacci?"); + + // output from assistant (the output below is generated using llama-2-chat-7b, the output may vary depending on the model used) + // + // Of course! To calculate the 100th number in the Fibonacci sequence using C#, you can use the following code:``` + // using System; + // class FibonacciSequence { + // static int Fibonacci(int n) { + // if (n <= 1) { + // return 1; + // } else { + // return Fibonacci(n - 1) + Fibonacci(n - 2); + // } + // } + // static void Main() { + // Console.WriteLine("The 100th number in the Fibonacci sequence is: " + Fibonacci(100)); + // } + // } + // ``` + // In this code, we define a function `Fibonacci` that takes an integer `n` as input and returns the `n`-th number in the Fibonacci sequence. The function uses a recursive approach to calculate the value of the sequence. + // The `Main` method simply calls the `Fibonacci` function with the argument `100`, and prints the result to the console. + // Note that this code will only work for positive integers `n`. If you want to calculate the Fibonacci sequence for other types of numbers, such as real or complex numbers, you will need to modify the code accordingly. + #endregion lmstudio_example_1 + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/Example09_LMStudio_FunctionCall.cs b/dotnet/sample/AutoGen.BasicSamples/Example09_LMStudio_FunctionCall.cs new file mode 100644 index 00000000000..9a62144df2b --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/Example09_LMStudio_FunctionCall.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Example09_LMStudio_FunctionCall.cs + +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoGen.Core; +using AutoGen.LMStudio; +using Azure.AI.OpenAI; + +namespace AutoGen.BasicSample; + +public class LLaMAFunctionCall +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("arguments")] + public JsonElement Arguments { get; set; } +} + +public partial class Example09_LMStudio_FunctionCall +{ + /// + /// Get weather from location. + /// + /// location + /// date. type is string + [Function] + public async Task GetWeather(string location, string date) + { + return $"[Function] The weather on {date} in {location} is sunny."; + } + + + /// + /// Search query on Google and return the results. + /// + /// search query + [Function] + public async Task GoogleSearch(string query) + { + return $"[Function] Here are the search results for {query}."; + } + + private static object SerializeFunctionDefinition(FunctionDefinition functionDefinition) + { + return new + { + type = "function", + function = new + { + name = functionDefinition.Name, + description = functionDefinition.Description, + parameters = functionDefinition.Parameters.ToObjectFromJson(), + } + }; + } + + public static async Task RunAsync() + { + #region lmstudio_function_call_example + // This example has been verified to work with Trelis-Llama-2-7b-chat-hf-function-calling-v3 + var instance = new Example09_LMStudio_FunctionCall(); + var config = new LMStudioConfig("localhost", 1234); + var systemMessage = @$"You are a helpful AI assistant."; + + // Because the LM studio server doesn't support openai function call yet + // To simulate the function call, we can put the function call details in the system message + // And ask agent to response in function call object format using few-shot example + object[] functionList = + [ + SerializeFunctionDefinition(instance.GetWeatherFunction), + SerializeFunctionDefinition(instance.GoogleSearchFunction) + ]; + var functionListString = JsonSerializer.Serialize(functionList, new JsonSerializerOptions { WriteIndented = true }); + var lmAgent = new LMStudioAgent( + name: "assistant", + systemMessage: @$" +You are a helpful AI assistant +You have access to the following functions. Use them if required: + +{functionListString}", + config: config) + .RegisterMiddleware(async (msgs, option, innerAgent, ct) => + { + // inject few-shot example to the message + var exampleGetWeather = new TextMessage(Role.User, "Get weather in London"); + var exampleAnswer = new TextMessage(Role.Assistant, "{\n \"name\": \"GetWeather\",\n \"arguments\": {\n \"city\": \"London\"\n }\n}", from: innerAgent.Name); + + msgs = new[] { exampleGetWeather, exampleAnswer }.Concat(msgs).ToArray(); + var reply = await innerAgent.GenerateReplyAsync(msgs, option, ct); + + // if reply is a function call, invoke function + var content = reply.GetContent(); + try + { + if (JsonSerializer.Deserialize(content) is { } functionCall) + { + var arguments = JsonSerializer.Serialize(functionCall.Arguments); + // invoke function wrapper + if (functionCall.Name == instance.GetWeatherFunction.Name) + { + var result = await instance.GetWeatherWrapper(arguments); + return new TextMessage(Role.Assistant, result); + } + else if (functionCall.Name == instance.GoogleSearchFunction.Name) + { + var result = await instance.GoogleSearchWrapper(arguments); + return new TextMessage(Role.Assistant, result); + } + else + { + throw new Exception($"Unknown function call: {functionCall.Name}"); + } + } + } + catch (JsonException) + { + // ignore + } + + return reply; + }) + .RegisterPrintMessage(); + + var userProxyAgent = new UserProxyAgent( + name: "user", + humanInputMode: HumanInputMode.ALWAYS); + + await userProxyAgent.SendAsync( + receiver: lmAgent, + "Search the names of the five largest stocks in the US by market cap "); + #endregion lmstudio_function_call_example + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/Example10_SemanticKernel.cs b/dotnet/sample/AutoGen.BasicSamples/Example10_SemanticKernel.cs new file mode 100644 index 00000000000..e4ef7de9df7 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/Example10_SemanticKernel.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Example10_SemanticKernel.cs + +using System.ComponentModel; +using AutoGen.Core; +using AutoGen.SemanticKernel.Extension; +using FluentAssertions; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +namespace AutoGen.BasicSample; + +public class LightPlugin +{ + public bool IsOn { get; set; } = false; + + [KernelFunction] + [Description("Gets the state of the light.")] + public string GetState() => this.IsOn ? "on" : "off"; + + [KernelFunction] + [Description("Changes the state of the light.'")] + public string ChangeState(bool newState) + { + this.IsOn = newState; + var state = this.GetState(); + + // Print the state to the console + Console.ForegroundColor = ConsoleColor.DarkBlue; + Console.WriteLine($"[Light is now {state}]"); + Console.ResetColor(); + + return state; + } +} + +public class Example10_SemanticKernel +{ + public static async Task RunAsync() + { + var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); + var modelId = "gpt-3.5-turbo"; + var builder = Kernel.CreateBuilder() + .AddOpenAIChatCompletion(modelId: modelId, apiKey: openAIKey); + var kernel = builder.Build(); + var settings = new OpenAIPromptExecutionSettings + { + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions, + }; + + kernel.Plugins.AddFromObject(new LightPlugin()); + var skAgent = kernel + .ToSemanticKernelAgent(name: "assistant", systemMessage: "You control the light", settings); + + // Send a message to the skAgent, the skAgent supports the following message types: + // - IMessage + // - (streaming) IMessage + // You can create an IMessage using MessageEnvelope.Create + var chatMessageContent = MessageEnvelope.Create(new ChatMessageContent(AuthorRole.User, "Toggle the light")); + var reply = await skAgent.SendAsync(chatMessageContent); + reply.Should().BeOfType>(); + Console.WriteLine((reply as IMessage).Content.Items[0].As().Text); + + var skAgentWithMiddleware = skAgent + .RegisterMessageConnector() + .RegisterPrintMessage(); + + // Now the skAgentWithMiddleware supports more IMessage types like TextMessage, ImageMessage or MultiModalMessage + // It also register a print format message hook to print the message in a human readable format to the console + await skAgent.SendAsync(chatMessageContent); + await skAgentWithMiddleware.SendAsync(new TextMessage(Role.User, "Toggle the light")); + + // The more message type an agent support, the more flexible it is to be used in different scenarios + // For example, since the TextMessage is supported, the skAgentWithMiddleware can be used with user proxy. + var userProxy = new UserProxyAgent("user"); + + await skAgentWithMiddleware.InitiateChatAsync(userProxy, "how can I help you today"); + } + +} diff --git a/dotnet/sample/AutoGen.BasicSamples/Example11_Sequential_GroupChat_Example.cs b/dotnet/sample/AutoGen.BasicSamples/Example11_Sequential_GroupChat_Example.cs new file mode 100644 index 00000000000..00ff321082a --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/Example11_Sequential_GroupChat_Example.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Example11_Sequential_GroupChat_Example.cs + +#region using_statement +using AutoGen.Core; +using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; +using AutoGen.SemanticKernel; +using AutoGen.SemanticKernel.Extension; +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Plugins.Web; +using Microsoft.SemanticKernel.Plugins.Web.Bing; +#endregion using_statement + +namespace AutoGen.BasicSample; + +public partial class Sequential_GroupChat_Example +{ + public static async Task CreateBingSearchAgentAsync() + { + #region CreateBingSearchAgent + var config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + var apiKey = config.ApiKey; + var kernelBuilder = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(config.DeploymentName, config.Endpoint, apiKey); + var bingApiKey = Environment.GetEnvironmentVariable("BING_API_KEY") ?? throw new Exception("BING_API_KEY environment variable is not set"); + var bingSearch = new BingConnector(bingApiKey); + var webSearchPlugin = new WebSearchEnginePlugin(bingSearch); + kernelBuilder.Plugins.AddFromObject(webSearchPlugin); + + var kernel = kernelBuilder.Build(); + var kernelAgent = new SemanticKernelAgent( + kernel: kernel, + name: "bing-search", + systemMessage: """ + You search results from Bing and return it as-is. + You put the original search result between ```bing and ``` + + e.g. + ```bing + xxx + ``` + """) + .RegisterMessageConnector() + .RegisterPrintMessage(); // pretty print the message + + return kernelAgent; + #endregion CreateBingSearchAgent + } + + public static async Task CreateSummarizerAgentAsync() + { + #region CreateSummarizerAgent + var config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + var apiKey = config.ApiKey; + var endPoint = new Uri(config.Endpoint); + + var openAIClient = new OpenAIClient(endPoint, new Azure.AzureKeyCredential(apiKey)); + var openAIClientAgent = new OpenAIChatAgent( + openAIClient: openAIClient, + name: "summarizer", + modelName: config.DeploymentName, + systemMessage: "You summarize search result from bing in a short and concise manner"); + + return openAIClientAgent + .RegisterMessageConnector() + .RegisterPrintMessage(); // pretty print the message + #endregion CreateSummarizerAgent + } + + public static async Task RunAsync() + { + #region Sequential_GroupChat_Example + var userProxyAgent = new UserProxyAgent( + name: "user", + humanInputMode: HumanInputMode.ALWAYS) + .RegisterPrintMessage(); + + var bingSearchAgent = await CreateBingSearchAgentAsync(); + var summarizerAgent = await CreateSummarizerAgentAsync(); + + var groupChat = new RoundRobinGroupChat( + agents: [userProxyAgent, bingSearchAgent, summarizerAgent]); + + var groupChatAgent = new GroupChatManager(groupChat); + + var history = await userProxyAgent.InitiateChatAsync( + receiver: groupChatAgent, + message: "How to deploy an openai resource on azure", + maxRound: 10); + #endregion Sequential_GroupChat_Example + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/Example12_TwoAgent_Fill_Application.cs b/dotnet/sample/AutoGen.BasicSamples/Example12_TwoAgent_Fill_Application.cs new file mode 100644 index 00000000000..c5e6773d01e --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/Example12_TwoAgent_Fill_Application.cs @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Example11_TwoAgent_Fill_Application.cs + +using System.Text; +using AutoGen.OpenAI; +using AutoGen.Core; +using Azure.AI.OpenAI; +using AutoGen.OpenAI.Extension; + +namespace AutoGen.BasicSample; + +public partial class TwoAgent_Fill_Application +{ + private string? name = null; + private string? email = null; + private string? phone = null; + private string? address = null; + private bool? receiveUpdates = null; + + [Function] + public async Task SaveProgress( + string name, + string email, + string phone, + string address, + bool? receiveUpdates) + { + this.name = !string.IsNullOrEmpty(name) ? name : this.name; + this.email = !string.IsNullOrEmpty(email) ? email : this.email; + this.phone = !string.IsNullOrEmpty(phone) ? phone : this.phone; + this.address = !string.IsNullOrEmpty(address) ? address : this.address; + this.receiveUpdates = receiveUpdates ?? this.receiveUpdates; + + var missingInformationStringBuilder = new StringBuilder(); + if (string.IsNullOrEmpty(this.name)) + { + missingInformationStringBuilder.AppendLine("Name is missing."); + } + + if (string.IsNullOrEmpty(this.email)) + { + missingInformationStringBuilder.AppendLine("Email is missing."); + } + + if (string.IsNullOrEmpty(this.phone)) + { + missingInformationStringBuilder.AppendLine("Phone is missing."); + } + + if (string.IsNullOrEmpty(this.address)) + { + missingInformationStringBuilder.AppendLine("Address is missing."); + } + + if (this.receiveUpdates == null) + { + missingInformationStringBuilder.AppendLine("ReceiveUpdates is missing."); + } + + if (missingInformationStringBuilder.Length > 0) + { + return missingInformationStringBuilder.ToString(); + } + else + { + return "Application information is saved to database."; + } + } + + public static async Task CreateSaveProgressAgent() + { + var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + var endPoint = gpt3Config.Endpoint ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + var apiKey = gpt3Config.ApiKey ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + var openaiClient = new OpenAIClient(new Uri(endPoint), new Azure.AzureKeyCredential(apiKey)); + + var instance = new TwoAgent_Fill_Application(); + var functionCallConnector = new FunctionCallMiddleware( + functions: [instance.SaveProgressFunctionContract], + functionMap: new Dictionary>> + { + { instance.SaveProgressFunctionContract.Name, instance.SaveProgressWrapper }, + }); + + var chatAgent = new OpenAIChatAgent( + openAIClient: openaiClient, + name: "application", + modelName: gpt3Config.DeploymentName, + systemMessage: """You are a helpful application form assistant who saves progress while user fills application.""") + .RegisterMessageConnector() + .RegisterMiddleware(functionCallConnector) + .RegisterMiddleware(async (msgs, option, agent, ct) => + { + var lastUserMessage = msgs.Last() ?? throw new Exception("No user message found."); + var prompt = $""" + Save progress according to the most recent information provided by user. + + ```user + {lastUserMessage.GetContent()} + ``` + """; + + return await agent.GenerateReplyAsync([lastUserMessage], option, ct); + + }); + + return chatAgent; + } + + public static async Task CreateAssistantAgent() + { + var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + var endPoint = gpt3Config.Endpoint ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + var apiKey = gpt3Config.ApiKey ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + var openaiClient = new OpenAIClient(new Uri(endPoint), new Azure.AzureKeyCredential(apiKey)); + + var chatAgent = new OpenAIChatAgent( + openAIClient: openaiClient, + name: "assistant", + modelName: gpt3Config.DeploymentName, + systemMessage: """You create polite prompt to ask user provide missing information""") + .RegisterMessageConnector() + .RegisterPrintMessage() + .RegisterMiddleware(async (msgs, option, agent, ct) => + { + var lastReply = msgs.Last() ?? throw new Exception("No reply found."); + var reply = await agent.GenerateReplyAsync(msgs, option, ct); + + // if application is complete, exit conversation by sending termination message + if (lastReply.GetContent().Contains("Application information is saved to database.")) + { + return new TextMessage(Role.Assistant, GroupChatExtension.TERMINATE, from: agent.Name); + } + else + { + return reply; + } + }); + + return chatAgent; + } + + public static async Task CreateUserAgent() + { + var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + var endPoint = gpt3Config.Endpoint ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + var apiKey = gpt3Config.ApiKey ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + var openaiClient = new OpenAIClient(new Uri(endPoint), new Azure.AzureKeyCredential(apiKey)); + + var chatAgent = new OpenAIChatAgent( + openAIClient: openaiClient, + name: "user", + modelName: gpt3Config.DeploymentName, + systemMessage: """ + You are a user who is filling an application form. Simply provide the information as requested and answer the questions, don't do anything else. + + here's some personal information about you: + - name: John Doe + - email: 1234567@gmail.com + - phone: 123-456-7890 + - address: 1234 Main St, Redmond, WA 98052 + - want to receive update? true + """) + .RegisterMessageConnector() + .RegisterPrintMessage(); + + return chatAgent; + } + + public static async Task RunAsync() + { + var applicationAgent = await CreateSaveProgressAgent(); + var assistantAgent = await CreateAssistantAgent(); + var userAgent = await CreateUserAgent(); + + var userToApplicationTransition = Transition.Create(userAgent, applicationAgent); + var applicationToAssistantTransition = Transition.Create(applicationAgent, assistantAgent); + var assistantToUserTransition = Transition.Create(assistantAgent, userAgent); + + var workflow = new Graph( + [ + userToApplicationTransition, + applicationToAssistantTransition, + assistantToUserTransition, + ]); + + var groupChat = new GroupChat( + members: [userAgent, applicationAgent, assistantAgent], + workflow: workflow); + + var groupChatManager = new GroupChatManager(groupChat); + var initialMessage = await assistantAgent.SendAsync("Generate a greeting meesage for user and start the conversation by asking what's their name."); + + var chatHistory = await userAgent.SendAsync(groupChatManager, [initialMessage], maxRound: 30); + + var lastMessage = chatHistory.Last(); + Console.WriteLine(lastMessage.GetContent()); + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/Example13_OpenAIAgent_JsonMode.cs b/dotnet/sample/AutoGen.BasicSamples/Example13_OpenAIAgent_JsonMode.cs new file mode 100644 index 00000000000..2591ab23016 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/Example13_OpenAIAgent_JsonMode.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Example13_OpenAIAgent_JsonMode.cs + +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoGen.Core; +using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; +using Azure.AI.OpenAI; +using FluentAssertions; + +namespace AutoGen.BasicSample; + +public class Example13_OpenAIAgent_JsonMode +{ + public static async Task RunAsync() + { + #region create_agent + var config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(deployName: "gpt-35-turbo-0125"); // json mode only works with 0125 and later model. + var apiKey = config.ApiKey; + var endPoint = new Uri(config.Endpoint); + + var openAIClient = new OpenAIClient(endPoint, new Azure.AzureKeyCredential(apiKey)); + var openAIClientAgent = new OpenAIChatAgent( + openAIClient: openAIClient, + name: "assistant", + modelName: config.DeploymentName, + systemMessage: "You are a helpful assistant designed to output JSON.", + seed: 0, // explicitly set a seed to enable deterministic output + responseFormat: ChatCompletionsResponseFormat.JsonObject) // set response format to JSON object to enable JSON mode + .RegisterMessageConnector(); + #endregion create_agent + + #region chat_with_agent + var reply = await openAIClientAgent.SendAsync("My name is John, I am 25 years old, and I live in Seattle."); + + var person = JsonSerializer.Deserialize(reply.GetContent()); + Console.WriteLine($"Name: {person.Name}"); + Console.WriteLine($"Age: {person.Age}"); + + if (!string.IsNullOrEmpty(person.Address)) + { + Console.WriteLine($"Address: {person.Address}"); + } + + Console.WriteLine("Done."); + #endregion chat_with_agent + + person.Name.Should().Be("John"); + person.Age.Should().Be(25); + person.Address.Should().BeNullOrEmpty(); + } +} + +#region person_class +public class Person +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("age")] + public int Age { get; set; } + + [JsonPropertyName("address")] + public string Address { get; set; } +} +#endregion person_class diff --git a/dotnet/sample/AutoGen.BasicSamples/Example14_MistralClientAgent_TokenCount.cs b/dotnet/sample/AutoGen.BasicSamples/Example14_MistralClientAgent_TokenCount.cs new file mode 100644 index 00000000000..8b20dbf33a8 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/Example14_MistralClientAgent_TokenCount.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Example14_MistralClientAgent_TokenCount.cs + +#region using_statements +using AutoGen.Core; +using AutoGen.Mistral; +#endregion using_statements +using FluentAssertions; + +namespace AutoGen.BasicSample; + +public class Example14_MistralClientAgent_TokenCount +{ + #region token_counter_middleware + public class MistralAITokenCounterMiddleware : IMiddleware + { + private readonly List responses = new List(); + public string? Name => nameof(MistralAITokenCounterMiddleware); + + public async Task InvokeAsync(MiddlewareContext context, IAgent agent, CancellationToken cancellationToken = default) + { + var reply = await agent.GenerateReplyAsync(context.Messages, context.Options, cancellationToken); + + if (reply is IMessage message) + { + responses.Add(message.Content); + } + + return reply; + } + + public int GetCompletionTokenCount() + { + return responses.Sum(r => r.Usage.CompletionTokens); + } + } + #endregion token_counter_middleware + + public static async Task RunAsync() + { + #region create_mistral_client_agent + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new Exception("Missing MISTRAL_API_KEY environment variable."); + var mistralClient = new MistralClient(apiKey); + var agent = new MistralClientAgent( + client: mistralClient, + name: "assistant", + model: MistralAIModelID.OPEN_MISTRAL_7B); + #endregion create_mistral_client_agent + + #region register_middleware + var tokenCounterMiddleware = new MistralAITokenCounterMiddleware(); + var mistralMessageConnector = new MistralChatMessageConnector(); + var agentWithTokenCounter = agent + .RegisterMiddleware(tokenCounterMiddleware) + .RegisterMiddleware(mistralMessageConnector) + .RegisterPrintMessage(); + #endregion register_middleware + + #region chat_with_agent + await agentWithTokenCounter.SendAsync("write a long, tedious story"); + Console.WriteLine($"Completion token count: {tokenCounterMiddleware.GetCompletionTokenCount()}"); + tokenCounterMiddleware.GetCompletionTokenCount().Should().BeGreaterThan(0); + #endregion chat_with_agent + } +} \ No newline at end of file diff --git a/dotnet/sample/AutoGen.BasicSamples/GlobalUsing.cs b/dotnet/sample/AutoGen.BasicSamples/GlobalUsing.cs new file mode 100644 index 00000000000..87b4ee0ab4c --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/GlobalUsing.cs @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GlobalUsing.cs + diff --git a/dotnet/sample/AutoGen.BasicSamples/LLMConfiguration.cs b/dotnet/sample/AutoGen.BasicSamples/LLMConfiguration.cs new file mode 100644 index 00000000000..37c9b0d7ade --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/LLMConfiguration.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// LLMConfiguration.cs + +using AutoGen.OpenAI; + +namespace AutoGen.BasicSample; + +internal static class LLMConfiguration +{ + public static OpenAIConfig GetOpenAIGPT3_5_Turbo() + { + var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); + var modelId = "gpt-3.5-turbo"; + return new OpenAIConfig(openAIKey, modelId); + } + + public static OpenAIConfig GetOpenAIGPT4() + { + var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); + var modelId = "gpt-4"; + + return new OpenAIConfig(openAIKey, modelId); + } + + public static AzureOpenAIConfig GetAzureOpenAIGPT3_5_Turbo(string deployName = "gpt-35-turbo-16k") + { + var azureOpenAIKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + + return new AzureOpenAIConfig(endpoint, deployName, azureOpenAIKey); + } + + public static AzureOpenAIConfig GetAzureOpenAIGPT4(string deployName = "gpt-4") + { + var azureOpenAIKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + + return new AzureOpenAIConfig(endpoint, deployName, azureOpenAIKey); + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/Program.cs b/dotnet/sample/AutoGen.BasicSamples/Program.cs new file mode 100644 index 00000000000..bddbb68bf48 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/Program.cs @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Program.cs + +using AutoGen.BasicSample; +await Example14_MistralClientAgent_TokenCount.RunAsync(); diff --git a/dotnet/src/AutoGen.Core/Agent/DefaultReplyAgent.cs b/dotnet/src/AutoGen.Core/Agent/DefaultReplyAgent.cs new file mode 100644 index 00000000000..647a2ece79d --- /dev/null +++ b/dotnet/src/AutoGen.Core/Agent/DefaultReplyAgent.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// DefaultReplyAgent.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +public class DefaultReplyAgent : IAgent +{ + public DefaultReplyAgent( + string name, + string? defaultReply) + { + Name = name; + DefaultReply = defaultReply ?? string.Empty; + } + + public string Name { get; } + + public string DefaultReply { get; } = string.Empty; + + public async Task GenerateReplyAsync( + IEnumerable _, + GenerateReplyOptions? __ = null, + CancellationToken ___ = default) + { + return new TextMessage(Role.Assistant, DefaultReply, from: this.Name); + } +} diff --git a/dotnet/src/AutoGen.Core/Agent/GroupChatManager.cs b/dotnet/src/AutoGen.Core/Agent/GroupChatManager.cs new file mode 100644 index 00000000000..db40f801dea --- /dev/null +++ b/dotnet/src/AutoGen.Core/Agent/GroupChatManager.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GroupChatManager.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +public class GroupChatManager : IAgent +{ + public GroupChatManager(IGroupChat groupChat) + { + GroupChat = groupChat; + } + public string Name => throw new ArgumentException("GroupChatManager does not have a name"); + + public IEnumerable? Messages { get; private set; } + + public IGroupChat GroupChat { get; } + + public async Task GenerateReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options, + CancellationToken cancellationToken = default) + { + var response = await GroupChat.CallAsync(messages, ct: cancellationToken); + Messages = response; + + return response.Last(); + } +} diff --git a/dotnet/src/AutoGen.Core/Agent/IAgent.cs b/dotnet/src/AutoGen.Core/Agent/IAgent.cs new file mode 100644 index 00000000000..b9149008480 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Agent/IAgent.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IAgent.cs + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; +public interface IAgent +{ + public string Name { get; } + + /// + /// Generate reply + /// + /// conversation history + /// completion option. If provided, it should override existing option if there's any + public Task GenerateReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + CancellationToken cancellationToken = default); +} + +public class GenerateReplyOptions +{ + public GenerateReplyOptions() + { + } + + /// + /// Copy constructor + /// + /// other option to copy from + public GenerateReplyOptions(GenerateReplyOptions other) + { + this.Temperature = other.Temperature; + this.MaxToken = other.MaxToken; + this.StopSequence = other.StopSequence?.Select(s => s)?.ToArray(); + this.Functions = other.Functions?.Select(f => f)?.ToArray(); + } + + public float? Temperature { get; set; } + + public int? MaxToken { get; set; } + + public string[]? StopSequence { get; set; } + + public FunctionContract[]? Functions { get; set; } +} diff --git a/dotnet/src/AutoGen.Core/Agent/IMiddlewareAgent.cs b/dotnet/src/AutoGen.Core/Agent/IMiddlewareAgent.cs new file mode 100644 index 00000000000..7b318183d52 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Agent/IMiddlewareAgent.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IMiddlewareAgent.cs + +using System.Collections.Generic; + +namespace AutoGen.Core; + +public interface IMiddlewareAgent : IAgent +{ + /// + /// Get the inner agent. + /// + IAgent Agent { get; } + + /// + /// Get the middlewares. + /// + IEnumerable Middlewares { get; } + + /// + /// Use middleware. + /// + void Use(IMiddleware middleware); +} + +public interface IMiddlewareStreamAgent : IMiddlewareAgent, IStreamingAgent +{ + /// + /// Get the inner agent. + /// + IStreamingAgent StreamingAgent { get; } + + IEnumerable StreamingMiddlewares { get; } + + void UseStreaming(IStreamingMiddleware middleware); +} + +public interface IMiddlewareAgent : IMiddlewareAgent + where T : IAgent +{ + /// + /// Get the typed inner agent. + /// + T TAgent { get; } +} + +public interface IMiddlewareStreamAgent : IMiddlewareStreamAgent, IMiddlewareAgent + where T : IStreamingAgent +{ +} diff --git a/dotnet/src/AutoGen.Core/Agent/IStreamingAgent.cs b/dotnet/src/AutoGen.Core/Agent/IStreamingAgent.cs new file mode 100644 index 00000000000..f4004b1397b --- /dev/null +++ b/dotnet/src/AutoGen.Core/Agent/IStreamingAgent.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IStreamingAgent.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +/// +/// agent that supports streaming reply +/// +public interface IStreamingAgent : IAgent +{ + public Task> GenerateStreamingReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/AutoGen.Core/Agent/MiddlewareAgent.cs b/dotnet/src/AutoGen.Core/Agent/MiddlewareAgent.cs new file mode 100644 index 00000000000..307e0da79ae --- /dev/null +++ b/dotnet/src/AutoGen.Core/Agent/MiddlewareAgent.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MiddlewareAgent.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +/// +/// An agent that allows you to add middleware and modify the behavior of an existing agent. +/// +public class MiddlewareAgent : IMiddlewareAgent +{ + private readonly IAgent _agent; + private readonly List middlewares = new(); + + /// + /// Create a new instance of + /// + /// the inner agent where middleware will be added. + /// the name of the agent if provided. Otherwise, the name of will be used. + public MiddlewareAgent(IAgent innerAgent, string? name = null) + { + this.Name = name ?? innerAgent.Name; + this._agent = innerAgent; + } + + /// + /// Create a new instance of by copying the middlewares from another . + /// + public MiddlewareAgent(MiddlewareAgent other) + { + this.Name = other.Name; + this._agent = other._agent; + this.middlewares.AddRange(other.middlewares); + } + + public string Name { get; } + + /// + /// Get the inner agent. + /// + public IAgent Agent => this._agent; + + /// + /// Get the middlewares. + /// + public IEnumerable Middlewares => this.middlewares; + + public Task GenerateReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + CancellationToken cancellationToken = default) + { + IAgent agent = this._agent; + foreach (var middleware in this.middlewares) + { + agent = new DelegateAgent(middleware, agent); + } + + return agent.GenerateReplyAsync(messages, options, cancellationToken); + } + + /// + /// Add a middleware to the agent. If multiple middlewares are added, they will be executed in the LIFO order. + /// Call into the next function to continue the execution of the next middleware. + /// Short cut middleware execution by not calling into the next function. + /// + public void Use(Func, GenerateReplyOptions?, IAgent, CancellationToken, Task> func, string? middlewareName = null) + { + this.middlewares.Add(new DelegateMiddleware(middlewareName, async (context, agent, cancellationToken) => + { + return await func(context.Messages, context.Options, agent, cancellationToken); + })); + } + + public void Use(IMiddleware middleware) + { + this.middlewares.Add(middleware); + } + + public override string ToString() + { + var names = this.Middlewares.Select(m => m.Name ?? "[Unknown middleware]"); + var namesPlusAgentName = names.Append(this.Name); + + return namesPlusAgentName.Aggregate((a, b) => $"{a} -> {b}"); + } + + private class DelegateAgent : IAgent + { + private readonly IAgent innerAgent; + private readonly IMiddleware middleware; + + public DelegateAgent(IMiddleware middleware, IAgent innerAgent) + { + this.middleware = middleware; + this.innerAgent = innerAgent; + } + + public string Name { get => this.innerAgent.Name; } + + public Task GenerateReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + CancellationToken cancellationToken = default) + { + var context = new MiddlewareContext(messages, options); + return this.middleware.InvokeAsync(context, this.innerAgent, cancellationToken); + } + } +} + +public sealed class MiddlewareAgent : MiddlewareAgent, IMiddlewareAgent + where T : IAgent +{ + public MiddlewareAgent(T innerAgent, string? name = null) + : base(innerAgent, name) + { + this.TAgent = innerAgent; + } + + public MiddlewareAgent(MiddlewareAgent other) + : base(other) + { + this.TAgent = other.TAgent; + } + + /// + /// Get the inner agent of type . + /// + public T TAgent { get; } +} diff --git a/dotnet/src/AutoGen.Core/Agent/MiddlewareStreamingAgent.cs b/dotnet/src/AutoGen.Core/Agent/MiddlewareStreamingAgent.cs new file mode 100644 index 00000000000..b83922227b7 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Agent/MiddlewareStreamingAgent.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MiddlewareStreamingAgent.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +public class MiddlewareStreamingAgent : MiddlewareAgent, IMiddlewareStreamAgent +{ + private readonly IStreamingAgent _agent; + private readonly List _streamingMiddlewares = new(); + private readonly List _middlewares = new(); + + public MiddlewareStreamingAgent( + IStreamingAgent agent, + string? name = null, + IEnumerable? streamingMiddlewares = null, + IEnumerable? middlewares = null) + : base(agent, name) + { + _agent = agent; + if (streamingMiddlewares != null) + { + _streamingMiddlewares.AddRange(streamingMiddlewares); + } + + if (middlewares != null) + { + _middlewares.AddRange(middlewares); + } + } + + /// + /// Get the inner agent. + /// + public IStreamingAgent StreamingAgent => _agent; + + /// + /// Get the streaming middlewares. + /// + public IEnumerable StreamingMiddlewares => _streamingMiddlewares; + + public Task> GenerateStreamingReplyAsync(IEnumerable messages, GenerateReplyOptions? options = null, CancellationToken cancellationToken = default) + { + var agent = _agent; + foreach (var middleware in _streamingMiddlewares) + { + agent = new DelegateStreamingAgent(middleware, agent); + } + + return agent.GenerateStreamingReplyAsync(messages, options, cancellationToken); + } + + public void UseStreaming(IStreamingMiddleware middleware) + { + _streamingMiddlewares.Add(middleware); + } + + private class DelegateStreamingAgent : IStreamingAgent + { + private IStreamingMiddleware? streamingMiddleware; + private IMiddleware? middleware; + private IStreamingAgent innerAgent; + + public string Name => innerAgent.Name; + + public DelegateStreamingAgent(IStreamingMiddleware middleware, IStreamingAgent next) + { + this.streamingMiddleware = middleware; + this.innerAgent = next; + } + + public DelegateStreamingAgent(IMiddleware middleware, IStreamingAgent next) + { + this.middleware = middleware; + this.innerAgent = next; + } + + public async Task GenerateReplyAsync(IEnumerable messages, GenerateReplyOptions? options = null, CancellationToken cancellationToken = default) + { + if (middleware is null) + { + return await innerAgent.GenerateReplyAsync(messages, options, cancellationToken); + } + + var context = new MiddlewareContext(messages, options); + return await middleware.InvokeAsync(context, innerAgent, cancellationToken); + } + + public Task> GenerateStreamingReplyAsync(IEnumerable messages, GenerateReplyOptions? options = null, CancellationToken cancellationToken = default) + { + if (streamingMiddleware is null) + { + return innerAgent.GenerateStreamingReplyAsync(messages, options, cancellationToken); + } + + var context = new MiddlewareContext(messages, options); + return streamingMiddleware.InvokeAsync(context, innerAgent, cancellationToken); + } + } +} + +public sealed class MiddlewareStreamingAgent : MiddlewareStreamingAgent, IMiddlewareStreamAgent + where T : IStreamingAgent +{ + public MiddlewareStreamingAgent(T innerAgent, string? name = null) + : base(innerAgent, name) + { + TAgent = innerAgent; + } + + public MiddlewareStreamingAgent(MiddlewareStreamingAgent other) + : base(other) + { + TAgent = other.TAgent; + } + + /// + /// Get the inner agent. + /// + public T TAgent { get; } +} diff --git a/dotnet/src/AutoGen.Core/AutoGen.Core.csproj b/dotnet/src/AutoGen.Core/AutoGen.Core.csproj new file mode 100644 index 00000000000..018cd23a446 --- /dev/null +++ b/dotnet/src/AutoGen.Core/AutoGen.Core.csproj @@ -0,0 +1,21 @@ + + + netstandard2.0 + AutoGen.Core + + + + + + + AutoGen.Core + + Core library for AutoGen. This package provides contracts and core functionalities for AutoGen. + + + + + + + + diff --git a/dotnet/src/AutoGen.Core/Extension/AgentExtension.cs b/dotnet/src/AutoGen.Core/Extension/AgentExtension.cs new file mode 100644 index 00000000000..44ce8838b73 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Extension/AgentExtension.cs @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AgentExtension.cs + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +public static class AgentExtension +{ + /// + /// Send message to an agent. + /// + /// message to send. will be added to the end of if provided + /// sender agent. + /// chat history. + /// conversation history + public static async Task SendAsync( + this IAgent agent, + IMessage? message = null, + IEnumerable? chatHistory = null, + CancellationToken ct = default) + { + var messages = new List(); + + if (chatHistory != null) + { + messages.AddRange(chatHistory); + } + + if (message != null) + { + messages.Add(message); + } + + + var result = await agent.GenerateReplyAsync(messages, cancellationToken: ct); + + return result; + } + + /// + /// Send message to an agent. + /// + /// sender agent. + /// message to send. will be added to the end of if provided + /// chat history. + /// conversation history + public static async Task SendAsync( + this IAgent agent, + string message, + IEnumerable? chatHistory = null, + CancellationToken ct = default) + { + var msg = new TextMessage(Role.User, message); + + return await agent.SendAsync(msg, chatHistory, ct); + } + + /// + /// Send message to another agent. + /// + /// sender agent. + /// receiver agent. + /// chat history. + /// max conversation round. + /// conversation history + public static async Task> SendAsync( + this IAgent agent, + IAgent receiver, + IEnumerable chatHistory, + int maxRound = 10, + CancellationToken ct = default) + { + if (receiver is GroupChatManager manager) + { + var gc = manager.GroupChat; + + return await agent.SendMessageToGroupAsync(gc, chatHistory, maxRound, ct); + } + + var groupChat = new RoundRobinGroupChat( + agents: new[] + { + agent, + receiver, + }); + + return await groupChat.CallAsync(chatHistory, maxRound, ct: ct); + } + + /// + /// Send message to another agent. + /// + /// sender agent. + /// message to send. will be added to the end of if provided + /// receiver agent. + /// chat history. + /// max conversation round. + /// conversation history + public static async Task> SendAsync( + this IAgent agent, + IAgent receiver, + string message, + IEnumerable? chatHistory = null, + int maxRound = 10, + CancellationToken ct = default) + { + var msg = new TextMessage(Role.User, message) + { + From = agent.Name, + }; + + chatHistory = chatHistory ?? new List(); + chatHistory = chatHistory.Append(msg); + + return await agent.SendAsync(receiver, chatHistory, maxRound, ct); + } + + /// + /// Shortcut API to send message to another agent. + /// + /// sender agent + /// receiver agent + /// message to send + /// max round + public static async Task> InitiateChatAsync( + this IAgent agent, + IAgent receiver, + string? message = null, + int maxRound = 10, + CancellationToken ct = default) + { + var chatHistory = new List(); + if (message != null) + { + var msg = new TextMessage(Role.User, message) + { + From = agent.Name, + }; + + chatHistory.Add(msg); + } + + return await agent.SendAsync(receiver, chatHistory, maxRound, ct); + } + + public static async Task> SendMessageToGroupAsync( + this IAgent agent, + IGroupChat groupChat, + string msg, + IEnumerable? chatHistory = null, + int maxRound = 10, + CancellationToken ct = default) + { + var chatMessage = new TextMessage(Role.Assistant, msg, from: agent.Name); + chatHistory = chatHistory ?? Enumerable.Empty(); + chatHistory = chatHistory.Append(chatMessage); + + return await agent.SendMessageToGroupAsync(groupChat, chatHistory, maxRound, ct); + } + + public static async Task> SendMessageToGroupAsync( + this IAgent _, + IGroupChat groupChat, + IEnumerable? chatHistory = null, + int maxRound = 10, + CancellationToken ct = default) + { + return await groupChat.CallAsync(chatHistory, maxRound, ct); + } +} diff --git a/dotnet/src/AutoGen.Core/Extension/GroupChatExtension.cs b/dotnet/src/AutoGen.Core/Extension/GroupChatExtension.cs new file mode 100644 index 00000000000..e3e44622c81 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Extension/GroupChatExtension.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GroupChatExtension.cs + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AutoGen.Core; + +public static class GroupChatExtension +{ + public const string TERMINATE = "[GROUPCHAT_TERMINATE]"; + public const string CLEAR_MESSAGES = "[GROUPCHAT_CLEAR_MESSAGES]"; + + [Obsolete("please use SendIntroduction")] + public static void AddInitializeMessage(this IAgent agent, string message, IGroupChat groupChat) + { + var msg = new TextMessage(Role.User, message) + { + From = agent.Name + }; + + groupChat.SendIntroduction(msg); + } + + /// + /// Send an instruction message to the group chat. + /// + public static void SendIntroduction(this IAgent agent, string message, IGroupChat groupChat) + { + var msg = new TextMessage(Role.User, message) + { + From = agent.Name + }; + + groupChat.SendIntroduction(msg); + } + + public static IEnumerable MessageToKeep( + this IGroupChat _, + IEnumerable messages) + { + var lastCLRMessageIndex = messages.ToList() + .FindLastIndex(x => x.IsGroupChatClearMessage()); + + // if multiple clr messages, e.g [msg, clr, msg, clr, msg, clr, msg] + // only keep the the messages after the second last clr message. + if (messages.Count(m => m.IsGroupChatClearMessage()) > 1) + { + lastCLRMessageIndex = messages.ToList() + .FindLastIndex(lastCLRMessageIndex - 1, lastCLRMessageIndex - 1, x => x.IsGroupChatClearMessage()); + messages = messages.Skip(lastCLRMessageIndex); + } + + lastCLRMessageIndex = messages.ToList() + .FindLastIndex(x => x.IsGroupChatClearMessage()); + + if (lastCLRMessageIndex != -1 && messages.Count() - lastCLRMessageIndex >= 2) + { + messages = messages.Skip(lastCLRMessageIndex); + } + + return messages; + } + + /// + /// Return true if contains , otherwise false. + /// + /// + /// + public static bool IsGroupChatTerminateMessage(this IMessage message) + { + return message.GetContent()?.Contains(TERMINATE) ?? false; + } + + public static bool IsGroupChatClearMessage(this IMessage message) + { + return message.GetContent()?.Contains(CLEAR_MESSAGES) ?? false; + } + + public static IEnumerable ProcessConversationForAgent( + this IGroupChat groupChat, + IEnumerable initialMessages, + IEnumerable messages) + { + messages = groupChat.MessageToKeep(messages); + return initialMessages.Concat(messages); + } + + internal static IEnumerable ProcessConversationsForRolePlay( + this IGroupChat groupChat, + IEnumerable initialMessages, + IEnumerable messages) + { + messages = groupChat.MessageToKeep(messages); + var messagesToKeep = initialMessages.Concat(messages); + + return messagesToKeep.Select((x, i) => + { + var msg = @$"From {x.From}: +{x.GetContent()} + +round # + {i}"; + + return new TextMessage(Role.User, content: msg); + }); + } +} diff --git a/dotnet/src/AutoGen.Core/Extension/MessageExtension.cs b/dotnet/src/AutoGen.Core/Extension/MessageExtension.cs new file mode 100644 index 00000000000..47dbad55e30 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Extension/MessageExtension.cs @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MessageExtension.cs + +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace AutoGen.Core; + +public static class MessageExtension +{ + private static string separator = new string('-', 20); + + public static string FormatMessage(this IMessage message) + { + return message switch + { + Message msg => msg.FormatMessage(), + TextMessage textMessage => textMessage.FormatMessage(), + ImageMessage imageMessage => imageMessage.FormatMessage(), + ToolCallMessage toolCallMessage => toolCallMessage.FormatMessage(), + ToolCallResultMessage toolCallResultMessage => toolCallResultMessage.FormatMessage(), + AggregateMessage aggregateMessage => aggregateMessage.FormatMessage(), + _ => message.ToString(), + }; + } + + public static string FormatMessage(this TextMessage message) + { + var sb = new StringBuilder(); + // write from + sb.AppendLine($"TextMessage from {message.From}"); + // write a seperator + sb.AppendLine(separator); + sb.AppendLine(message.Content); + // write a seperator + sb.AppendLine(separator); + + return sb.ToString(); + } + + public static string FormatMessage(this ImageMessage message) + { + var sb = new StringBuilder(); + // write from + sb.AppendLine($"ImageMessage from {message.From}"); + // write a seperator + sb.AppendLine(separator); + sb.AppendLine($"Image: {message.Url}"); + // write a seperator + sb.AppendLine(separator); + + return sb.ToString(); + } + + public static string FormatMessage(this ToolCallMessage message) + { + var sb = new StringBuilder(); + // write from + sb.AppendLine($"ToolCallMessage from {message.From}"); + + // write a seperator + sb.AppendLine(separator); + + foreach (var toolCall in message.ToolCalls) + { + sb.AppendLine($"- {toolCall.FunctionName}: {toolCall.FunctionArguments}"); + } + + sb.AppendLine(separator); + + return sb.ToString(); + } + + public static string FormatMessage(this ToolCallResultMessage message) + { + var sb = new StringBuilder(); + // write from + sb.AppendLine($"ToolCallResultMessage from {message.From}"); + + // write a seperator + sb.AppendLine(separator); + + foreach (var toolCall in message.ToolCalls) + { + sb.AppendLine($"- {toolCall.FunctionName}: {toolCall.Result}"); + } + + sb.AppendLine(separator); + + return sb.ToString(); + } + + public static string FormatMessage(this AggregateMessage message) + { + var sb = new StringBuilder(); + // write from + sb.AppendLine($"AggregateMessage from {message.From}"); + + // write a seperator + sb.AppendLine(separator); + + sb.AppendLine("ToolCallMessage:"); + sb.AppendLine(message.Message1.FormatMessage()); + + sb.AppendLine("ToolCallResultMessage:"); + sb.AppendLine(message.Message2.FormatMessage()); + + sb.AppendLine(separator); + + return sb.ToString(); + } + public static string FormatMessage(this Message message) + { + var sb = new StringBuilder(); + // write from + sb.AppendLine($"Message from {message.From}"); + // write a seperator + sb.AppendLine(separator); + + // write content + sb.AppendLine($"content: {message.Content}"); + + // write function name if exists + if (!string.IsNullOrEmpty(message.FunctionName)) + { + sb.AppendLine($"function name: {message.FunctionName}"); + sb.AppendLine($"function arguments: {message.FunctionArguments}"); + } + + // write metadata + if (message.Metadata is { Count: > 0 }) + { + sb.AppendLine($"metadata:"); + foreach (var item in message.Metadata) + { + sb.AppendLine($"{item.Key}: {item.Value}"); + } + } + + // write a seperator + sb.AppendLine(separator); + + return sb.ToString(); + } + + public static bool IsSystemMessage(this IMessage message) + { + return message switch + { + TextMessage textMessage => textMessage.Role == Role.System, + Message msg => msg.Role == Role.System, + _ => false, + }; + } + + /// + /// Get the content from the message + /// if the message is a or , return the content + /// if the message is a and only contains one function call, return the result of that function call + /// if the message is a where TMessage1 is and TMessage2 is and the second message only contains one function call, return the result of that function call + /// for all other situation, return null. + /// + /// + public static string? GetContent(this IMessage message) + { + return message switch + { + TextMessage textMessage => textMessage.Content, + Message msg => msg.Content, + ToolCallResultMessage toolCallResultMessage => toolCallResultMessage.ToolCalls.Count == 1 ? toolCallResultMessage.ToolCalls.First().Result : null, + AggregateMessage aggregateMessage => aggregateMessage.Message2.ToolCalls.Count == 1 ? aggregateMessage.Message2.ToolCalls.First().Result : null, + _ => null, + }; + } + + /// + /// Get the role from the message if it's available. + /// + public static Role? GetRole(this IMessage message) + { + return message switch + { + TextMessage textMessage => textMessage.Role, + Message msg => msg.Role, + ImageMessage img => img.Role, + MultiModalMessage multiModal => multiModal.Role, + _ => null, + }; + } + + /// + /// Return the tool calls from the message if it's available. + /// if the message is a , return its tool calls + /// if the message is a and the function name and function arguments are available, return a list of tool call with one item + /// if the message is a where TMessage1 is and TMessage2 is , return the tool calls from the first message + /// + /// + /// + public static IList? GetToolCalls(this IMessage message) + { + return message switch + { + ToolCallMessage toolCallMessage => toolCallMessage.ToolCalls, + Message msg => msg.FunctionName is not null && msg.FunctionArguments is not null + ? msg.Content is not null ? new List { new ToolCall(msg.FunctionName, msg.FunctionArguments, result: msg.Content) } + : new List { new ToolCall(msg.FunctionName, msg.FunctionArguments) } + : null, + AggregateMessage aggregateMessage => aggregateMessage.Message1.ToolCalls, + _ => null, + }; + } +} diff --git a/dotnet/src/AutoGen.Core/Extension/MiddlewareExtension.cs b/dotnet/src/AutoGen.Core/Extension/MiddlewareExtension.cs new file mode 100644 index 00000000000..c522c78f506 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Extension/MiddlewareExtension.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MiddlewareExtension.cs + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +public static class MiddlewareExtension +{ + /// + /// Register a auto reply hook to an agent. The hook will be called before the agent generate the reply. + /// If the hook return a non-null reply, then that non-null reply will be returned directly without calling the agent. + /// Otherwise, the agent will generate the reply. + /// This is useful when you want to override the agent reply in some cases. + /// + /// + /// + /// + /// throw when agent name is null. + public static MiddlewareAgent RegisterReply( + this TAgent agent, + Func, CancellationToken, Task> replyFunc) + where TAgent : IAgent + { + return agent.RegisterMiddleware(async (messages, options, agent, ct) => + { + var reply = await replyFunc(messages, ct); + + if (reply != null) + { + return reply; + } + + return await agent.GenerateReplyAsync(messages, options, ct); + }); + } + + /// + /// Register a post process hook to an agent. The hook will be called before the agent return the reply and after the agent generate the reply. + /// This is useful when you want to customize arbitrary behavior before the agent return the reply. + /// + /// One example is , which print the formatted message to console before the agent return the reply. + /// + /// throw when agent name is null. + public static MiddlewareAgent RegisterPostProcess( + this TAgent agent, + Func, IMessage, CancellationToken, Task> postprocessFunc) + where TAgent : IAgent + { + return agent.RegisterMiddleware(async (messages, options, agent, ct) => + { + var reply = await agent.GenerateReplyAsync(messages, options, ct); + + return await postprocessFunc(messages, reply, ct); + }); + } + + /// + /// Register a pre process hook to an agent. The hook will be called before the agent generate the reply. This is useful when you want to modify the conversation history before the agent generate the reply. + /// + /// throw when agent name is null. + public static MiddlewareAgent RegisterPreProcess( + this TAgent agent, + Func, CancellationToken, Task>> preprocessFunc) + where TAgent : IAgent + { + return agent.RegisterMiddleware(async (messages, options, agent, ct) => + { + var newMessages = await preprocessFunc(messages, ct); + + return await agent.GenerateReplyAsync(newMessages, options, ct); + }); + } + + /// + /// Register a middleware to an existing agent and return a new agent with the middleware. + /// + public static MiddlewareAgent RegisterMiddleware( + this TAgent agent, + Func, GenerateReplyOptions?, IAgent, CancellationToken, Task> func, + string? middlewareName = null) + where TAgent : IAgent + { + var middleware = new DelegateMiddleware(middlewareName, async (context, agent, cancellationToken) => + { + return await func(context.Messages, context.Options, agent, cancellationToken); + }); + + return agent.RegisterMiddleware(middleware); + } + + /// + /// Register a middleware to an existing agent and return a new agent with the middleware. + /// + public static MiddlewareAgent RegisterMiddleware( + this TAgent agent, + IMiddleware middleware) + where TAgent : IAgent + { + var middlewareAgent = new MiddlewareAgent(agent); + + return middlewareAgent.RegisterMiddleware(middleware); + } + + /// + /// Register a middleware to an existing agent and return a new agent with the middleware. + /// + public static MiddlewareAgent RegisterMiddleware( + this MiddlewareAgent agent, + Func, GenerateReplyOptions?, IAgent, CancellationToken, Task> func, + string? middlewareName = null) + where TAgent : IAgent + { + var delegateMiddleware = new DelegateMiddleware(middlewareName, async (context, agent, cancellationToken) => + { + return await func(context.Messages, context.Options, agent, cancellationToken); + }); + + return agent.RegisterMiddleware(delegateMiddleware); + } + + /// + /// Register a middleware to an existing agent and return a new agent with the middleware. + /// + public static MiddlewareAgent RegisterMiddleware( + this MiddlewareAgent agent, + IMiddleware middleware) + where TAgent : IAgent + { + var copyAgent = new MiddlewareAgent(agent); + copyAgent.Use(middleware); + + return copyAgent; + } +} diff --git a/dotnet/src/AutoGen.Core/Extension/PrintMessageMiddlewareExtension.cs b/dotnet/src/AutoGen.Core/Extension/PrintMessageMiddlewareExtension.cs new file mode 100644 index 00000000000..deb196ca324 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Extension/PrintMessageMiddlewareExtension.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// PrintMessageMiddlewareExtension.cs + +using System; + +namespace AutoGen.Core; + +public static class PrintMessageMiddlewareExtension +{ + [Obsolete("This API will be removed in v0.1.0, Use RegisterPrintMessage instead.")] + public static MiddlewareAgent RegisterPrintFormatMessageHook(this TAgent agent) + where TAgent : IAgent + { + return RegisterPrintMessage(agent); + } + + [Obsolete("This API will be removed in v0.1.0, Use RegisterPrintMessage instead.")] + public static MiddlewareAgent RegisterPrintFormatMessageHook(this MiddlewareAgent agent) + where TAgent : IAgent + { + return RegisterPrintMessage(agent); + } + + [Obsolete("This API will be removed in v0.1.0, Use RegisterPrintMessage instead.")] + public static MiddlewareStreamingAgent RegisterPrintFormatMessageHook(this MiddlewareStreamingAgent agent) + where TAgent : IStreamingAgent + { + return RegisterPrintMessage(agent); + } + + /// + /// Register a to which print formatted message to console. + /// + public static MiddlewareAgent RegisterPrintMessage(this TAgent agent) + where TAgent : IAgent + { + var middleware = new PrintMessageMiddleware(); + var middlewareAgent = new MiddlewareAgent(agent); + middlewareAgent.Use(middleware); + + return middlewareAgent; + } + + /// + /// Register a to which print formatted message to console. + /// + public static MiddlewareAgent RegisterPrintMessage(this MiddlewareAgent agent) + where TAgent : IAgent + { + var middleware = new PrintMessageMiddleware(); + var middlewareAgent = new MiddlewareAgent(agent); + middlewareAgent.Use(middleware); + + return middlewareAgent; + } + + /// + /// Register a to which print formatted message to console. + /// + public static MiddlewareStreamingAgent RegisterPrintMessage(this MiddlewareStreamingAgent agent) + where TAgent : IStreamingAgent + { + var middleware = new PrintMessageMiddleware(); + var middlewareAgent = new MiddlewareStreamingAgent(agent); + middlewareAgent.Use(middleware); + + return middlewareAgent; + } +} diff --git a/dotnet/src/AutoGen.Core/Extension/StreamingMiddlewareExtension.cs b/dotnet/src/AutoGen.Core/Extension/StreamingMiddlewareExtension.cs new file mode 100644 index 00000000000..901d7f2492a --- /dev/null +++ b/dotnet/src/AutoGen.Core/Extension/StreamingMiddlewareExtension.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// StreamingMiddlewareExtension.cs + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +public static class StreamingMiddlewareExtension +{ + /// + /// Register a middleware to an existing agent and return a new agent with the middleware. + /// + public static MiddlewareStreamingAgent RegisterStreamingMiddleware( + this TStreamingAgent agent, + IStreamingMiddleware middleware) + where TStreamingAgent : IStreamingAgent + { + var middlewareAgent = new MiddlewareStreamingAgent(agent); + middlewareAgent.UseStreaming(middleware); + + if (middleware is IMiddleware middlewareBase) + { + middlewareAgent.Use(middlewareBase); + } + + return middlewareAgent; + } + + /// + /// Register a middleware to an existing agent and return a new agent with the middleware. + /// + public static MiddlewareStreamingAgent RegisterStreamingMiddleware( + this MiddlewareStreamingAgent agent, + IStreamingMiddleware middleware) + where TAgent : IStreamingAgent + { + var copyAgent = new MiddlewareStreamingAgent(agent); + copyAgent.UseStreaming(middleware); + + if (middleware is IMiddleware middlewareBase) + { + copyAgent.Use(middlewareBase); + } + + return copyAgent; + } + + + /// + /// Register a middleware to an existing agent and return a new agent with the middleware. + /// + public static MiddlewareStreamingAgent RegisterStreamingMiddleware( + this TAgent agent, + Func>> func, + string? middlewareName = null) + where TAgent : IStreamingAgent + { + var middleware = new DelegateStreamingMiddleware(middlewareName, new DelegateStreamingMiddleware.MiddlewareDelegate(func)); + + return agent.RegisterStreamingMiddleware(middleware); + } + + /// + /// Register a streaming middleware to an existing agent and return a new agent with the middleware. + /// + public static MiddlewareStreamingAgent RegisterStreamingMiddleware( + this MiddlewareStreamingAgent agent, + Func>> func, + string? middlewareName = null) + where TAgent : IStreamingAgent + { + var middleware = new DelegateStreamingMiddleware(middlewareName, new DelegateStreamingMiddleware.MiddlewareDelegate(func)); + + return agent.RegisterStreamingMiddleware(middleware); + } + + /// + /// Register a middleware to an existing streaming agent and return a new agent with the middleware. + /// + public static MiddlewareStreamingAgent RegisterMiddleware( + this MiddlewareStreamingAgent streamingAgent, + Func, GenerateReplyOptions?, IAgent, CancellationToken, Task> func, + string? middlewareName = null) + where TStreamingAgent : IStreamingAgent + { + var middleware = new DelegateMiddleware(middlewareName, async (context, agent, cancellationToken) => + { + return await func(context.Messages, context.Options, agent, cancellationToken); + }); + + return streamingAgent.RegisterMiddleware(middleware); + } + + /// + /// Register a middleware to an existing streaming agent and return a new agent with the middleware. + /// + public static MiddlewareStreamingAgent RegisterMiddleware( + this MiddlewareStreamingAgent streamingAgent, + IMiddleware middleware) + where TStreamingAgent : IStreamingAgent + { + var copyAgent = new MiddlewareStreamingAgent(streamingAgent); + copyAgent.Use(middleware); + if (middleware is IStreamingMiddleware streamingMiddleware) + { + copyAgent.UseStreaming(streamingMiddleware); + } + + return copyAgent; + } +} diff --git a/dotnet/src/AutoGen.Core/Function/FunctionAttribute.cs b/dotnet/src/AutoGen.Core/Function/FunctionAttribute.cs new file mode 100644 index 00000000000..2c828c26d89 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Function/FunctionAttribute.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// FunctionAttribute.cs + +using System; +using System.Collections.Generic; + +namespace AutoGen.Core; + +[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +public class FunctionAttribute : Attribute +{ + public string? FunctionName { get; } + + public string? Description { get; } + + public FunctionAttribute(string? functionName = null, string? description = null) + { + FunctionName = functionName; + Description = description; + } +} + +public class FunctionContract +{ + /// + /// The namespace of the function. + /// + public string? Namespace { get; set; } + + /// + /// The class name of the function. + /// + public string? ClassName { get; set; } + + /// + /// The name of the function. + /// + public string? Name { get; set; } + + /// + /// The description of the function. + /// If a structured comment is available, the description will be extracted from the summary section. + /// Otherwise, the description will be null. + /// + public string? Description { get; set; } + + /// + /// The parameters of the function. + /// + public IEnumerable? Parameters { get; set; } + + /// + /// The return type of the function. + /// + public Type? ReturnType { get; set; } + + /// + /// The description of the return section. + /// If a structured comment is available, the description will be extracted from the return section. + /// Otherwise, the description will be null. + /// + public string? ReturnDescription { get; set; } +} + +public class FunctionParameterContract +{ + /// + /// The name of the parameter. + /// + public string? Name { get; set; } + + /// + /// The description of the parameter. + /// This will be extracted from the param section of the structured comment if available. + /// Otherwise, the description will be null. + /// + public string? Description { get; set; } + + /// + /// The type of the parameter. + /// + public Type? ParameterType { get; set; } + + /// + /// If the parameter is a required parameter. + /// + public bool IsRequired { get; set; } + + /// + /// The default value of the parameter. + /// + public object? DefaultValue { get; set; } +} diff --git a/dotnet/src/AutoGen.Core/GroupChat/Graph.cs b/dotnet/src/AutoGen.Core/GroupChat/Graph.cs new file mode 100644 index 00000000000..483ce63bebc --- /dev/null +++ b/dotnet/src/AutoGen.Core/GroupChat/Graph.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Workflow.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +/// +/// Obsolete: please use +/// +[Obsolete("please use Graph")] +public class Workflow : Graph +{ + [Obsolete("please use Graph")] + public Workflow(IEnumerable transitions) + : base(transitions) + { + } +} + +public class Graph +{ + private readonly List transitions = new List(); + + public Graph(IEnumerable transitions) + { + this.transitions.AddRange(transitions); + } + + public void AddTransition(Transition transition) + { + transitions.Add(transition); + } + + /// + /// Get the transitions of the workflow. + /// + public IEnumerable Transitions => transitions; + + /// + /// Get the next available agents that the messages can be transit to. + /// + /// the from agent + /// messages + /// A list of agents that the messages can be transit to + public async Task> TransitToNextAvailableAgentsAsync(IAgent fromAgent, IEnumerable messages) + { + var nextAgents = new List(); + var availableTransitions = transitions.FindAll(t => t.From == fromAgent) ?? Enumerable.Empty(); + foreach (var transition in availableTransitions) + { + if (await transition.CanTransitionAsync(messages)) + { + nextAgents.Add(transition.To); + } + } + + return nextAgents; + } +} + +/// +/// Represents a transition between two agents. +/// +public class Transition +{ + private readonly IAgent _from; + private readonly IAgent _to; + private readonly Func, Task>? _canTransition; + + /// + /// Create a new instance of . + /// This constructor is used for testing purpose only. + /// To create a new instance of , use . + /// + /// from agent + /// to agent + /// detect if the transition is allowed, default to be always true + internal Transition(IAgent from, IAgent to, Func, Task>? canTransitionAsync = null) + { + _from = from; + _to = to; + _canTransition = canTransitionAsync; + } + + /// + /// Create a new instance of . + /// + /// " + public static Transition Create(TFromAgent from, TToAgent to, Func, Task>? canTransitionAsync = null) + where TFromAgent : IAgent + where TToAgent : IAgent + { + return new Transition(from, to, (fromAgent, toAgent, messages) => canTransitionAsync?.Invoke((TFromAgent)fromAgent, (TToAgent)toAgent, messages) ?? Task.FromResult(true)); + } + + public IAgent From => _from; + + public IAgent To => _to; + + /// + /// Check if the transition is allowed. + /// + /// messages + public Task CanTransitionAsync(IEnumerable messages) + { + if (_canTransition == null) + { + return Task.FromResult(true); + } + + return _canTransition(this.From, this.To, messages); + } +} diff --git a/dotnet/src/AutoGen.Core/GroupChat/GroupChat.cs b/dotnet/src/AutoGen.Core/GroupChat/GroupChat.cs new file mode 100644 index 00000000000..3b6288ca0a7 --- /dev/null +++ b/dotnet/src/AutoGen.Core/GroupChat/GroupChat.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GroupChat.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +public class GroupChat : IGroupChat +{ + private IAgent? admin; + private List agents = new List(); + private IEnumerable initializeMessages = new List(); + private Graph? workflow = null; + + public IEnumerable? Messages { get; private set; } + + /// + /// Create a group chat. The next speaker will be decided by a combination effort of the admin and the workflow. + /// + /// admin agent. If provided, the admin will be invoked to decide the next speaker. + /// workflow of the group chat. If provided, the next speaker will be decided by the workflow. + /// group members. + /// + public GroupChat( + IEnumerable members, + IAgent? admin = null, + IEnumerable? initializeMessages = null, + Graph? workflow = null) + { + this.admin = admin; + this.agents = members.ToList(); + this.initializeMessages = initializeMessages ?? new List(); + this.workflow = workflow; + + this.Validation(); + } + + private void Validation() + { + // check if all agents has a name + if (this.agents.Any(x => string.IsNullOrEmpty(x.Name))) + { + throw new Exception("All agents must have a name."); + } + + // check if any agents has the same name + var names = this.agents.Select(x => x.Name).ToList(); + if (names.Distinct().Count() != names.Count) + { + throw new Exception("All agents must have a unique name."); + } + + // if there's a workflow + // check if the agents in that workflow are in the group chat + if (this.workflow != null) + { + var agentNamesInWorkflow = this.workflow.Transitions.Select(x => x.From.Name!).Concat(this.workflow.Transitions.Select(x => x.To.Name!)).Distinct(); + if (agentNamesInWorkflow.Any(x => !this.agents.Select(a => a.Name).Contains(x))) + { + throw new Exception("All agents in the workflow must be in the group chat."); + } + } + + // must provide one of admin or workflow + if (this.admin == null && this.workflow == null) + { + throw new Exception("Must provide one of admin or workflow."); + } + } + + /// + /// Select the next speaker based on the conversation history. + /// The next speaker will be decided by a combination effort of the admin and the workflow. + /// Firstly, a group of candidates will be selected by the workflow. If there's only one candidate, then that candidate will be the next speaker. + /// Otherwise, the admin will be invoked to decide the next speaker using role-play prompt. + /// + /// current speaker + /// conversation history + /// next speaker. + public async Task SelectNextSpeakerAsync(IAgent currentSpeaker, IEnumerable conversationHistory) + { + var agentNames = this.agents.Select(x => x.Name).ToList(); + if (this.workflow != null) + { + var nextAvailableAgents = await this.workflow.TransitToNextAvailableAgentsAsync(currentSpeaker, conversationHistory); + agentNames = nextAvailableAgents.Select(x => x.Name).ToList(); + if (agentNames.Count() == 0) + { + throw new Exception("No next available agents found in the current workflow"); + } + + if (agentNames.Count() == 1) + { + return this.agents.FirstOrDefault(x => x.Name == agentNames.First()); + } + } + + if (this.admin == null) + { + throw new Exception("No admin is provided."); + } + + var systemMessage = new TextMessage(Role.System, + content: $@"You are in a role play game. Carefully read the conversation history and carry on the conversation. +The available roles are: +{string.Join(",", agentNames)} + +Each message will start with 'From name:', e.g: +From admin: +//your message//."); + + var conv = this.ProcessConversationsForRolePlay(this.initializeMessages, conversationHistory); + + var messages = new IMessage[] { systemMessage }.Concat(conv); + var response = await this.admin.GenerateReplyAsync( + messages: messages, + options: new GenerateReplyOptions + { + Temperature = 0, + MaxToken = 128, + StopSequence = [":"], + Functions = [], + }); + + var name = response?.GetContent() ?? throw new Exception("No name is returned."); + + // remove From + name = name!.Substring(5); + return this.agents.First(x => x.Name!.ToLower() == name.ToLower()); + } + + /// + public void AddInitializeMessage(IMessage message) + { + this.SendIntroduction(message); + } + + public async Task> CallAsync( + IEnumerable? conversationWithName = null, + int maxRound = 10, + CancellationToken ct = default) + { + var conversationHistory = new List(); + if (conversationWithName != null) + { + conversationHistory.AddRange(conversationWithName); + } + + var lastSpeaker = conversationHistory.LastOrDefault()?.From switch + { + null => this.agents.First(), + _ => this.agents.FirstOrDefault(x => x.Name == conversationHistory.Last().From) ?? throw new Exception("The agent is not in the group chat"), + }; + var round = 0; + while (round < maxRound) + { + var currentSpeaker = await this.SelectNextSpeakerAsync(lastSpeaker, conversationHistory); + var processedConversation = this.ProcessConversationForAgent(this.initializeMessages, conversationHistory); + var result = await currentSpeaker.GenerateReplyAsync(processedConversation) ?? throw new Exception("No result is returned."); + conversationHistory.Add(result); + + // if message is terminate message, then terminate the conversation + if (result?.IsGroupChatTerminateMessage() ?? false) + { + break; + } + + lastSpeaker = currentSpeaker; + round++; + } + + return conversationHistory; + } + + public void SendIntroduction(IMessage message) + { + this.initializeMessages = this.initializeMessages.Append(message); + } +} diff --git a/dotnet/src/AutoGen.Core/GroupChat/RoundRobinGroupChat.cs b/dotnet/src/AutoGen.Core/GroupChat/RoundRobinGroupChat.cs new file mode 100644 index 00000000000..b8de89b834f --- /dev/null +++ b/dotnet/src/AutoGen.Core/GroupChat/RoundRobinGroupChat.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// RoundRobinGroupChat.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +/// +/// Obsolete: please use +/// +[Obsolete("please use RoundRobinGroupChat")] +public class SequentialGroupChat : RoundRobinGroupChat +{ + [Obsolete("please use RoundRobinGroupChat")] + public SequentialGroupChat(IEnumerable agents, List? initializeMessages = null) + : base(agents, initializeMessages) + { + } +} + +/// +/// A group chat that allows agents to talk in a round-robin manner. +/// +public class RoundRobinGroupChat : IGroupChat +{ + private readonly List agents = new List(); + private readonly List initializeMessages = new List(); + + public RoundRobinGroupChat( + IEnumerable agents, + List? initializeMessages = null) + { + this.agents.AddRange(agents); + this.initializeMessages = initializeMessages ?? new List(); + } + + /// + public void AddInitializeMessage(IMessage message) + { + this.SendIntroduction(message); + } + + public async Task> CallAsync( + IEnumerable? conversationWithName = null, + int maxRound = 10, + CancellationToken ct = default) + { + var conversationHistory = new List(); + if (conversationWithName != null) + { + conversationHistory.AddRange(conversationWithName); + } + + var lastSpeaker = conversationHistory.LastOrDefault()?.From switch + { + null => this.agents.First(), + _ => this.agents.FirstOrDefault(x => x.Name == conversationHistory.Last().From) ?? throw new Exception("The agent is not in the group chat"), + }; + var round = 0; + while (round < maxRound) + { + var currentSpeaker = this.SelectNextSpeaker(lastSpeaker); + var processedConversation = this.ProcessConversationForAgent(this.initializeMessages, conversationHistory); + var result = await currentSpeaker.GenerateReplyAsync(processedConversation) ?? throw new Exception("No result is returned."); + conversationHistory.Add(result); + + // if message is terminate message, then terminate the conversation + if (result?.IsGroupChatTerminateMessage() ?? false) + { + break; + } + + lastSpeaker = currentSpeaker; + round++; + } + + return conversationHistory; + } + + public void SendIntroduction(IMessage message) + { + this.initializeMessages.Add(message); + } + + private IAgent SelectNextSpeaker(IAgent currentSpeaker) + { + var index = this.agents.IndexOf(currentSpeaker); + if (index == -1) + { + throw new ArgumentException("The agent is not in the group chat", nameof(currentSpeaker)); + } + + var nextIndex = (index + 1) % this.agents.Count; + return this.agents[nextIndex]; + } +} diff --git a/dotnet/src/AutoGen.Core/IGroupChat.cs b/dotnet/src/AutoGen.Core/IGroupChat.cs new file mode 100644 index 00000000000..a8c948cf58a --- /dev/null +++ b/dotnet/src/AutoGen.Core/IGroupChat.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IGroupChat.cs + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +public interface IGroupChat +{ + /// + /// Send an introduction message to the group chat. + /// + void SendIntroduction(IMessage message); + + [Obsolete("please use SendIntroduction")] + void AddInitializeMessage(IMessage message); + + Task> CallAsync(IEnumerable? conversation = null, int maxRound = 10, CancellationToken ct = default); +} diff --git a/dotnet/src/AutoGen.Core/ILLMConfig.cs b/dotnet/src/AutoGen.Core/ILLMConfig.cs new file mode 100644 index 00000000000..fd2a90db02a --- /dev/null +++ b/dotnet/src/AutoGen.Core/ILLMConfig.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ILLMConfig.cs + +namespace AutoGen.Core; + +public interface ILLMConfig +{ +} diff --git a/dotnet/src/AutoGen.Core/Message/AggregateMessage.cs b/dotnet/src/AutoGen.Core/Message/AggregateMessage.cs new file mode 100644 index 00000000000..c7eee1316ee --- /dev/null +++ b/dotnet/src/AutoGen.Core/Message/AggregateMessage.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AggregateMessage.cs + +using System; +using System.Collections.Generic; + +namespace AutoGen.Core; + +public class AggregateMessage : IMessage + where TMessage1 : IMessage + where TMessage2 : IMessage +{ + public AggregateMessage(TMessage1 message1, TMessage2 message2, string? from = null) + { + this.From = from; + this.Message1 = message1; + this.Message2 = message2; + this.Validate(); + } + + public TMessage1 Message1 { get; } + + public TMessage2 Message2 { get; } + + public string? From { get; set; } + + private void Validate() + { + var messages = new List { this.Message1, this.Message2 }; + // the from property of all messages should be the same with the from property of the aggregate message + + foreach (var message in messages) + { + if (message.From != this.From) + { + throw new ArgumentException($"The from property of the message {message} is different from the from property of the aggregate message {this}"); + } + } + } + + public override string ToString() + { + var stringBuilder = new System.Text.StringBuilder(); + var messages = new List { this.Message1, this.Message2 }; + stringBuilder.Append($"AggregateMessage({this.From})"); + foreach (var message in messages) + { + stringBuilder.Append($"\n\t{message}"); + } + + return stringBuilder.ToString(); + } +} diff --git a/dotnet/src/AutoGen.Core/Message/IMessage.cs b/dotnet/src/AutoGen.Core/Message/IMessage.cs new file mode 100644 index 00000000000..7b48f4f0d63 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Message/IMessage.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IMessage.cs + +namespace AutoGen.Core; + +/// +/// The universal message interface for all message types in AutoGen. +/// Related PR: https://github.com/microsoft/autogen/pull/1676 +/// Built-in message types +/// +/// +/// : plain text message. +/// +/// +/// : image message. +/// +/// +/// : message type for multimodal message. The current support message items are and . +/// +/// +/// : message type for tool call. This message supports both single and parallel tool call. +/// +/// +/// : message type for tool call result. +/// +/// +/// : This type is used by previous version of AutoGen. And it's reserved for backward compatibility. +/// +/// +/// : an aggregate message type that contains two message types. +/// This type is useful when you want to combine two message types into one unique message type. One example is when invoking a tool call and you want to return both and . +/// One example of how this type is used in AutoGen is +/// +/// +/// +public interface IMessage : IStreamingMessage +{ +} + +public interface IMessage : IMessage, IStreamingMessage +{ +} + +public interface IStreamingMessage +{ + string? From { get; set; } +} + +public interface IStreamingMessage : IStreamingMessage +{ + T Content { get; } +} diff --git a/dotnet/src/AutoGen.Core/Message/ImageMessage.cs b/dotnet/src/AutoGen.Core/Message/ImageMessage.cs new file mode 100644 index 00000000000..18ceea0d111 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Message/ImageMessage.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ImageMessage.cs + +using System; + +namespace AutoGen.Core; + +public class ImageMessage : IMessage +{ + public ImageMessage(Role role, string url, string? from = null) + { + this.Role = role; + this.From = from; + this.Url = url; + } + + public ImageMessage(Role role, Uri uri, string? from = null) + { + this.Role = role; + this.From = from; + this.Url = uri.ToString(); + } + + public Role Role { get; set; } + + public string Url { get; set; } + + public string? From { get; set; } + + public override string ToString() + { + return $"ImageMessage({this.Role}, {this.Url}, {this.From})"; + } +} diff --git a/dotnet/src/AutoGen.Core/Message/Message.cs b/dotnet/src/AutoGen.Core/Message/Message.cs new file mode 100644 index 00000000000..ec4751b9344 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Message/Message.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Message.cs + +using System.Collections.Generic; + +namespace AutoGen.Core; + +public class Message : IMessage +{ + public Message( + Role role, + string? content, + string? from = null, + ToolCall? toolCall = null) + { + this.Role = role; + this.Content = content; + this.From = from; + this.FunctionName = toolCall?.FunctionName; + this.FunctionArguments = toolCall?.FunctionArguments; + } + + public Message(Message other) + : this(other.Role, other.Content, other.From) + { + this.FunctionName = other.FunctionName; + this.FunctionArguments = other.FunctionArguments; + this.Value = other.Value; + this.Metadata = other.Metadata; + } + + public Role Role { get; set; } + + public string? Content { get; set; } + + public string? From { get; set; } + + public string? FunctionName { get; set; } + + public string? FunctionArguments { get; set; } + + /// + /// raw message + /// + public object? Value { get; set; } + + public IList> Metadata { get; set; } = new List>(); + + public override string ToString() + { + return $"Message({this.Role}, {this.Content}, {this.From}, {this.FunctionName}, {this.FunctionArguments})"; + } +} diff --git a/dotnet/src/AutoGen.Core/Message/MessageEnvelope.cs b/dotnet/src/AutoGen.Core/Message/MessageEnvelope.cs new file mode 100644 index 00000000000..f83bea27926 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Message/MessageEnvelope.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MessageEnvelope.cs + +using System.Collections.Generic; + +namespace AutoGen.Core; + +public abstract class MessageEnvelope : IMessage, IStreamingMessage +{ + public MessageEnvelope(string? from = null, IDictionary? metadata = null) + { + this.From = from; + this.Metadata = metadata ?? new Dictionary(); + } + + public static MessageEnvelope Create(TContent content, string? from = null, IDictionary? metadata = null) + { + return new MessageEnvelope(content, from, metadata); + } + + public string? From { get; set; } + + public IDictionary Metadata { get; set; } +} + +public class MessageEnvelope : MessageEnvelope, IMessage, IStreamingMessage +{ + public MessageEnvelope(T content, string? from = null, IDictionary? metadata = null) + : base(from, metadata) + { + this.Content = content; + this.From = from; + this.Metadata = metadata ?? new Dictionary(); + } + + public T Content { get; } +} diff --git a/dotnet/src/AutoGen.Core/Message/MultiModalMessage.cs b/dotnet/src/AutoGen.Core/Message/MultiModalMessage.cs new file mode 100644 index 00000000000..9dd2a37af0b --- /dev/null +++ b/dotnet/src/AutoGen.Core/Message/MultiModalMessage.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MultiModalMessage.cs + +using System; +using System.Collections.Generic; + +namespace AutoGen.Core; + +public class MultiModalMessage : IMessage +{ + public MultiModalMessage(Role role, IEnumerable content, string? from = null) + { + this.Role = role; + this.Content = content; + this.From = from; + this.Validate(); + } + + public Role Role { get; set; } + + public IEnumerable Content { get; set; } + + public string? From { get; set; } + + private void Validate() + { + foreach (var message in this.Content) + { + if (message.From != this.From) + { + var reason = $"The from property of the message {message} is different from the from property of the aggregate message {this}"; + throw new ArgumentException($"Invalid aggregate message {reason}"); + } + } + + // all message must be either text or image + foreach (var message in this.Content) + { + if (message is not TextMessage && message is not ImageMessage) + { + var reason = $"The message {message} is not a text or image message"; + throw new ArgumentException($"Invalid aggregate message {reason}"); + } + } + } + + public override string ToString() + { + var stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append($"MultiModalMessage({this.Role}, {this.From})"); + foreach (var message in this.Content) + { + stringBuilder.Append($"\n\t{message}"); + } + + return stringBuilder.ToString(); + } +} diff --git a/dotnet/src/AutoGen.Core/Message/Role.cs b/dotnet/src/AutoGen.Core/Message/Role.cs new file mode 100644 index 00000000000..8253543a81c --- /dev/null +++ b/dotnet/src/AutoGen.Core/Message/Role.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Role.cs + +using System; + +namespace AutoGen.Core; + +public readonly struct Role : IEquatable +{ + private readonly string label; + + internal Role(string name) + { + label = name; + } + + public static Role User { get; } = new Role("user"); + + public static Role Assistant { get; } = new Role("assistant"); + + public static Role System { get; } = new Role("system"); + + public static Role Function { get; } = new Role("function"); + + public bool Equals(Role other) + { + return label.Equals(other.label, StringComparison.OrdinalIgnoreCase); + } + + public override string ToString() + { + return label; + } + + public override bool Equals(object? obj) + { + return obj is Role other && Equals(other); + } + + public override int GetHashCode() + { + return label.GetHashCode(); + } + + public static bool operator ==(Role left, Role right) + { + return left.Equals(right); + } + + public static bool operator !=(Role left, Role right) + { + return !(left == right); + } +} diff --git a/dotnet/src/AutoGen.Core/Message/TextMessage.cs b/dotnet/src/AutoGen.Core/Message/TextMessage.cs new file mode 100644 index 00000000000..ed4d7436dde --- /dev/null +++ b/dotnet/src/AutoGen.Core/Message/TextMessage.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// TextMessage.cs + +namespace AutoGen.Core; + +public class TextMessage : IMessage, IStreamingMessage +{ + public TextMessage(Role role, string content, string? from = null) + { + this.Content = content; + this.Role = role; + this.From = from; + } + + public TextMessage(TextMessageUpdate update) + { + this.Content = update.Content ?? string.Empty; + this.Role = update.Role; + this.From = update.From; + } + + public void Update(TextMessageUpdate update) + { + if (update.Role != this.Role) + { + throw new System.ArgumentException("Role mismatch", nameof(update)); + } + + if (update.From != this.From) + { + throw new System.ArgumentException("From mismatch", nameof(update)); + } + + this.Content = this.Content + update.Content ?? string.Empty; + } + + public Role Role { get; set; } + + public string Content { get; set; } + + public string? From { get; set; } + + public override string ToString() + { + return $"TextMessage({this.Role}, {this.Content}, {this.From})"; + } +} + +public class TextMessageUpdate : IStreamingMessage +{ + public TextMessageUpdate(Role role, string? content, string? from = null) + { + this.Content = content; + this.From = from; + this.Role = role; + } + + public string? Content { get; set; } + + public string? From { get; set; } + + public Role Role { get; set; } +} diff --git a/dotnet/src/AutoGen.Core/Message/ToolCallMessage.cs b/dotnet/src/AutoGen.Core/Message/ToolCallMessage.cs new file mode 100644 index 00000000000..8dcd98ea0ec --- /dev/null +++ b/dotnet/src/AutoGen.Core/Message/ToolCallMessage.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ToolCallMessage.cs + +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace AutoGen.Core; + +public class ToolCall +{ + public ToolCall(string functionName, string functionArgs) + { + this.FunctionName = functionName; + this.FunctionArguments = functionArgs; + } + + public ToolCall(string functionName, string functionArgs, string result) + { + this.FunctionName = functionName; + this.FunctionArguments = functionArgs; + this.Result = result; + } + + public string FunctionName { get; set; } + + public string FunctionArguments { get; set; } + + public string? Result { get; set; } + + public override string ToString() + { + return $"ToolCall({this.FunctionName}, {this.FunctionArguments}, {this.Result})"; + } +} + +public class ToolCallMessage : IMessage +{ + public ToolCallMessage(IEnumerable toolCalls, string? from = null) + { + this.From = from; + this.ToolCalls = toolCalls.ToList(); + } + + public ToolCallMessage(string functionName, string functionArgs, string? from = null) + { + this.From = from; + this.ToolCalls = new List { new ToolCall(functionName, functionArgs) }; + } + + public ToolCallMessage(ToolCallMessageUpdate update) + { + this.From = update.From; + this.ToolCalls = new List { new ToolCall(update.FunctionName, update.FunctionArgumentUpdate) }; + } + + public void Update(ToolCallMessageUpdate update) + { + // firstly, valid if the update is from the same agent + if (update.From != this.From) + { + throw new System.ArgumentException("From mismatch", nameof(update)); + } + + // if update.FunctionName exists in the tool calls, update the function arguments + var toolCall = this.ToolCalls.FirstOrDefault(tc => tc.FunctionName == update.FunctionName); + if (toolCall is not null) + { + toolCall.FunctionArguments += update.FunctionArgumentUpdate; + } + else + { + this.ToolCalls.Add(new ToolCall(update.FunctionName, update.FunctionArgumentUpdate)); + } + } + + public IList ToolCalls { get; set; } + + public string? From { get; set; } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append($"ToolCallMessage({this.From})"); + foreach (var toolCall in this.ToolCalls) + { + sb.Append($"\n\t{toolCall}"); + } + + return sb.ToString(); + } +} + +public class ToolCallMessageUpdate : IStreamingMessage +{ + public ToolCallMessageUpdate(string functionName, string functionArgumentUpdate, string? from = null) + { + this.From = from; + this.FunctionName = functionName; + this.FunctionArgumentUpdate = functionArgumentUpdate; + } + + public string? From { get; set; } + + public string FunctionName { get; set; } + + public string FunctionArgumentUpdate { get; set; } +} diff --git a/dotnet/src/AutoGen.Core/Message/ToolCallResultMessage.cs b/dotnet/src/AutoGen.Core/Message/ToolCallResultMessage.cs new file mode 100644 index 00000000000..99c7740849a --- /dev/null +++ b/dotnet/src/AutoGen.Core/Message/ToolCallResultMessage.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ToolCallResultMessage.cs + +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace AutoGen.Core; + +public class ToolCallResultMessage : IMessage +{ + public ToolCallResultMessage(IEnumerable toolCalls, string? from = null) + { + this.From = from; + this.ToolCalls = toolCalls.ToList(); + } + + public ToolCallResultMessage(string result, string functionName, string functionArgs, string? from = null) + { + this.From = from; + var toolCall = new ToolCall(functionName, functionArgs); + toolCall.Result = result; + this.ToolCalls = [toolCall]; + } + + /// + /// The original tool call message + /// + public IList ToolCalls { get; set; } + + public string? From { get; set; } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append($"ToolCallResultMessage({this.From})"); + foreach (var toolCall in this.ToolCalls) + { + sb.Append($"\n\t{toolCall}"); + } + + return sb.ToString(); + } + + private void Validate() + { + // each tool call must have a result + foreach (var toolCall in this.ToolCalls) + { + if (string.IsNullOrEmpty(toolCall.Result)) + { + throw new System.ArgumentException($"The tool call {toolCall} does not have a result"); + } + } + } +} diff --git a/dotnet/src/AutoGen.Core/Middleware/DelegateMiddleware.cs b/dotnet/src/AutoGen.Core/Middleware/DelegateMiddleware.cs new file mode 100644 index 00000000000..79360e0428f --- /dev/null +++ b/dotnet/src/AutoGen.Core/Middleware/DelegateMiddleware.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// DelegateMiddleware.cs + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +internal class DelegateMiddleware : IMiddleware +{ + /// + /// middleware delegate. Call into the next function to continue the execution of the next middleware. Otherwise, short cut the middleware execution. + /// + /// cancellation token + public delegate Task MiddlewareDelegate( + MiddlewareContext context, + IAgent agent, + CancellationToken cancellationToken); + + private readonly MiddlewareDelegate middlewareDelegate; + + public DelegateMiddleware(string? name, Func> middlewareDelegate) + { + this.Name = name; + this.middlewareDelegate = async (context, agent, cancellationToken) => + { + return await middlewareDelegate(context, agent, cancellationToken); + }; + } + + public string? Name { get; } + + public Task InvokeAsync( + MiddlewareContext context, + IAgent agent, + CancellationToken cancellationToken = default) + { + var messages = context.Messages; + var options = context.Options; + + return this.middlewareDelegate(context, agent, cancellationToken); + } +} + diff --git a/dotnet/src/AutoGen.Core/Middleware/DelegateStreamingMiddleware.cs b/dotnet/src/AutoGen.Core/Middleware/DelegateStreamingMiddleware.cs new file mode 100644 index 00000000000..5499abccf4c --- /dev/null +++ b/dotnet/src/AutoGen.Core/Middleware/DelegateStreamingMiddleware.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// DelegateStreamingMiddleware.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +internal class DelegateStreamingMiddleware : IStreamingMiddleware +{ + public delegate Task> MiddlewareDelegate( + MiddlewareContext context, + IStreamingAgent agent, + CancellationToken cancellationToken); + + private readonly MiddlewareDelegate middlewareDelegate; + + public DelegateStreamingMiddleware(string? name, MiddlewareDelegate middlewareDelegate) + { + this.Name = name; + this.middlewareDelegate = middlewareDelegate; + } + + public string? Name { get; } + + public Task> InvokeAsync( + MiddlewareContext context, + IStreamingAgent agent, + CancellationToken cancellationToken = default) + { + var messages = context.Messages; + var options = context.Options; + + return this.middlewareDelegate(context, agent, cancellationToken); + } +} + diff --git a/dotnet/src/AutoGen.Core/Middleware/FunctionCallMiddleware.cs b/dotnet/src/AutoGen.Core/Middleware/FunctionCallMiddleware.cs new file mode 100644 index 00000000000..d00151b32a8 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Middleware/FunctionCallMiddleware.cs @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// FunctionCallMiddleware.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +/// +/// The middleware that process function call message that both send to an agent or reply from an agent. +/// If the last message is and the tool calls is available in this middleware's function map, +/// the tools from the last message will be invoked and a will be returned. In this situation, +/// the inner agent will be short-cut and won't be invoked. +/// Otherwise, the message will be sent to the inner agent. In this situation +/// if the reply from the inner agent is , +/// and the tool calls is available in this middleware's function map, the tools from the reply will be invoked, +/// and a where TMessage1 is and TMessage2 is "/> +/// will be returned. +/// +/// If the reply from the inner agent is but the tool calls is not available in this middleware's function map, +/// or the reply from the inner agent is not , the original reply from the inner agent will be returned. +/// +/// When used as a streaming middleware, if the streaming reply from the inner agent is or , +/// This middleware will update the message accordingly and invoke the function if the tool call is available in this middleware's function map. +/// If the streaming reply from the inner agent is other types of message, the most recent message will be used to invoke the function. +/// +/// +public class FunctionCallMiddleware : IMiddleware, IStreamingMiddleware +{ + private readonly IEnumerable? functions; + private readonly IDictionary>>? functionMap; + + public FunctionCallMiddleware( + IEnumerable? functions = null, + IDictionary>>? functionMap = null, + string? name = null) + { + this.Name = name ?? nameof(FunctionCallMiddleware); + this.functions = functions; + this.functionMap = functionMap; + } + + public string? Name { get; } + + public async Task InvokeAsync(MiddlewareContext context, IAgent agent, CancellationToken cancellationToken = default) + { + var lastMessage = context.Messages.Last(); + if (lastMessage is ToolCallMessage toolCallMessage) + { + return await this.InvokeToolCallMessagesBeforeInvokingAgentAsync(toolCallMessage, agent); + } + + // combine functions + var options = new GenerateReplyOptions(context.Options ?? new GenerateReplyOptions()); + var combinedFunctions = this.functions?.Concat(options.Functions ?? []) ?? options.Functions; + options.Functions = combinedFunctions?.ToArray(); + + var reply = await agent.GenerateReplyAsync(context.Messages, options, cancellationToken); + + // if the reply is a function call message plus the function's name is available in function map, invoke the function and return the result instead of sending to the agent. + if (reply is ToolCallMessage toolCallMsg) + { + return await this.InvokeToolCallMessagesAfterInvokingAgentAsync(toolCallMsg, agent); + } + + // for all other messages, just return the reply from the agent. + return reply; + } + + public Task> InvokeAsync(MiddlewareContext context, IStreamingAgent agent, CancellationToken cancellationToken = default) + { + return Task.FromResult(this.StreamingInvokeAsync(context, agent, cancellationToken)); + } + + private async IAsyncEnumerable StreamingInvokeAsync( + MiddlewareContext context, + IStreamingAgent agent, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var lastMessage = context.Messages.Last(); + if (lastMessage is ToolCallMessage toolCallMessage) + { + yield return await this.InvokeToolCallMessagesBeforeInvokingAgentAsync(toolCallMessage, agent); + } + + // combine functions + var options = new GenerateReplyOptions(context.Options ?? new GenerateReplyOptions()); + var combinedFunctions = this.functions?.Concat(options.Functions ?? []) ?? options.Functions; + options.Functions = combinedFunctions?.ToArray(); + + IStreamingMessage? initMessage = default; + await foreach (var message in await agent.GenerateStreamingReplyAsync(context.Messages, options, cancellationToken)) + { + if (message is ToolCallMessageUpdate toolCallMessageUpdate && this.functionMap != null) + { + if (initMessage is null) + { + initMessage = new ToolCallMessage(toolCallMessageUpdate); + } + else if (initMessage is ToolCallMessage toolCall) + { + toolCall.Update(toolCallMessageUpdate); + } + else + { + throw new InvalidOperationException("The first message is ToolCallMessage, but the update message is not ToolCallMessageUpdate"); + } + } + else + { + yield return message; + } + } + + if (initMessage is ToolCallMessage toolCallMsg) + { + yield return await this.InvokeToolCallMessagesAfterInvokingAgentAsync(toolCallMsg, agent); + } + } + + private async Task InvokeToolCallMessagesBeforeInvokingAgentAsync(ToolCallMessage toolCallMessage, IAgent agent) + { + var toolCallResult = new List(); + var toolCalls = toolCallMessage.ToolCalls; + foreach (var toolCall in toolCalls) + { + var functionName = toolCall.FunctionName; + var functionArguments = toolCall.FunctionArguments; + if (this.functionMap?.TryGetValue(functionName, out var func) is true) + { + var result = await func(functionArguments); + toolCallResult.Add(new ToolCall(functionName, functionArguments, result)); + } + else if (this.functionMap is not null) + { + var errorMessage = $"Function {functionName} is not available. Available functions are: {string.Join(", ", this.functionMap.Select(f => f.Key))}"; + + toolCallResult.Add(new ToolCall(functionName, functionArguments, errorMessage)); + } + else + { + throw new InvalidOperationException("FunctionMap is not available"); + } + } + + return new ToolCallResultMessage(toolCallResult, from: agent.Name); + } + + private async Task InvokeToolCallMessagesAfterInvokingAgentAsync(ToolCallMessage toolCallMsg, IAgent agent) + { + var toolCallsReply = toolCallMsg.ToolCalls; + var toolCallResult = new List(); + foreach (var toolCall in toolCallsReply) + { + var fName = toolCall.FunctionName; + var fArgs = toolCall.FunctionArguments; + if (this.functionMap?.TryGetValue(fName, out var func) is true) + { + var result = await func(fArgs); + toolCallResult.Add(new ToolCall(fName, fArgs, result)); + } + } + + if (toolCallResult.Count() > 0) + { + var toolCallResultMessage = new ToolCallResultMessage(toolCallResult, from: agent.Name); + return new AggregateMessage(toolCallMsg, toolCallResultMessage, from: agent.Name); + } + else + { + return toolCallMsg; + } + } +} diff --git a/dotnet/src/AutoGen.Core/Middleware/IMiddleware.cs b/dotnet/src/AutoGen.Core/Middleware/IMiddleware.cs new file mode 100644 index 00000000000..2813ee9cdb4 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Middleware/IMiddleware.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IMiddleware.cs + +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +/// +/// The middleware interface +/// +public interface IMiddleware +{ + /// + /// the name of the middleware + /// + public string? Name { get; } + + /// + /// The method to invoke the middleware + /// + public Task InvokeAsync( + MiddlewareContext context, + IAgent agent, + CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/AutoGen.Core/Middleware/IStreamingMiddleware.cs b/dotnet/src/AutoGen.Core/Middleware/IStreamingMiddleware.cs new file mode 100644 index 00000000000..b8965dcc41c --- /dev/null +++ b/dotnet/src/AutoGen.Core/Middleware/IStreamingMiddleware.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IStreamingMiddleware.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +/// +/// The streaming middleware interface +/// +public interface IStreamingMiddleware +{ + public string? Name { get; } + + public Task> InvokeAsync( + MiddlewareContext context, + IStreamingAgent agent, + CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/AutoGen.Core/Middleware/MiddlewareContext.cs b/dotnet/src/AutoGen.Core/Middleware/MiddlewareContext.cs new file mode 100644 index 00000000000..a608d0baf81 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Middleware/MiddlewareContext.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MiddlewareContext.cs + +using System.Collections.Generic; + +namespace AutoGen.Core; + +public class MiddlewareContext +{ + public MiddlewareContext( + IEnumerable messages, + GenerateReplyOptions? options) + { + this.Messages = messages; + this.Options = options; + } + + /// + /// Messages to send to the agent + /// + public IEnumerable Messages { get; } + + /// + /// Options to generate the reply + /// + public GenerateReplyOptions? Options { get; } +} diff --git a/dotnet/src/AutoGen.Core/Middleware/PrintMessageMiddleware.cs b/dotnet/src/AutoGen.Core/Middleware/PrintMessageMiddleware.cs new file mode 100644 index 00000000000..9461b697357 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Middleware/PrintMessageMiddleware.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// PrintMessageMiddleware.cs + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +/// +/// The middleware that prints the reply from agent to the console. +/// +public class PrintMessageMiddleware : IMiddleware +{ + public string? Name => nameof(PrintMessageMiddleware); + + public async Task InvokeAsync(MiddlewareContext context, IAgent agent, CancellationToken cancellationToken = default) + { + if (agent is IStreamingAgent streamingAgent) + { + IMessage? recentUpdate = null; + await foreach (var message in await streamingAgent.GenerateStreamingReplyAsync(context.Messages, context.Options, cancellationToken)) + { + if (message is TextMessageUpdate textMessageUpdate) + { + if (recentUpdate is null) + { + // Print from: xxx + Console.WriteLine($"from: {textMessageUpdate.From}"); + recentUpdate = new TextMessage(textMessageUpdate); + Console.Write(textMessageUpdate.Content); + } + else if (recentUpdate is TextMessage recentTextMessage) + { + // Print the content of the message + Console.Write(textMessageUpdate.Content); + recentTextMessage.Update(textMessageUpdate); + } + else + { + throw new InvalidOperationException("The recent update is not a TextMessage"); + } + } + else if (message is ToolCallMessageUpdate toolCallUpdate) + { + if (recentUpdate is null) + { + recentUpdate = new ToolCallMessage(toolCallUpdate); + } + else if (recentUpdate is ToolCallMessage recentToolCallMessage) + { + recentToolCallMessage.Update(toolCallUpdate); + } + else + { + throw new InvalidOperationException("The recent update is not a ToolCallMessage"); + } + } + else if (message is IMessage imessage) + { + recentUpdate = imessage; + } + else + { + throw new InvalidOperationException("The message is not a valid message"); + } + } + Console.WriteLine(); + if (recentUpdate is not null && recentUpdate is not TextMessage) + { + Console.WriteLine(recentUpdate.FormatMessage()); + } + + return recentUpdate ?? throw new InvalidOperationException("The message is not a valid message"); + } + else + { + var reply = await agent.GenerateReplyAsync(context.Messages, context.Options, cancellationToken); + + var formattedMessages = reply.FormatMessage(); + + Console.WriteLine(formattedMessages); + + return reply; + } + } +} diff --git a/dotnet/src/AutoGen.DotnetInteractive/AutoGen.DotnetInteractive.csproj b/dotnet/src/AutoGen.DotnetInteractive/AutoGen.DotnetInteractive.csproj new file mode 100644 index 00000000000..e17356994f8 --- /dev/null +++ b/dotnet/src/AutoGen.DotnetInteractive/AutoGen.DotnetInteractive.csproj @@ -0,0 +1,40 @@ + + + + netstandard2.0 + enable + enable + AutoGen.DotnetInteractive + true + + + + + + + AutoGen.DotnetInteractive + + Dotnet interactive integration for AutoGen agents + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveFunction.cs b/dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveFunction.cs new file mode 100644 index 00000000000..5587694882c --- /dev/null +++ b/dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveFunction.cs @@ -0,0 +1,278 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// DotnetInteractiveFunction.cs + +using System.Text; +using System.Text.Json; +using Azure.AI.OpenAI; +using Microsoft.DotNet.Interactive.Documents; +using Microsoft.DotNet.Interactive.Documents.Jupyter; + +namespace AutoGen.DotnetInteractive; + +public class DotnetInteractiveFunction : IDisposable +{ + private readonly InteractiveService? _interactiveService = null; + private string? _notebookPath; + private readonly KernelInfoCollection _kernelInfoCollection = new KernelInfoCollection(); + + public DotnetInteractiveFunction(InteractiveService interactiveService, string? notebookPath = null, bool continueFromExistingNotebook = false) + { + this._interactiveService = interactiveService; + this._notebookPath = notebookPath; + this._kernelInfoCollection.Add(new KernelInfo("csharp")); + this._kernelInfoCollection.Add(new KernelInfo("markdown")); + + if (this._notebookPath != null) + { + if (continueFromExistingNotebook == false) + { + // remove existing notebook + if (File.Exists(this._notebookPath)) + { + File.Delete(this._notebookPath); + } + + var document = new InteractiveDocument(); + + using var stream = File.OpenWrite(_notebookPath); + Notebook.Write(document, stream, this._kernelInfoCollection); + stream.Flush(); + stream.Dispose(); + } + else if (continueFromExistingNotebook == true && File.Exists(this._notebookPath)) + { + // load existing notebook + using var readStream = File.OpenRead(this._notebookPath); + var document = Notebook.Read(readStream, this._kernelInfoCollection); + foreach (var cell in document.Elements) + { + if (cell.KernelName == "csharp") + { + var code = cell.Contents; + this._interactiveService.SubmitCSharpCodeAsync(code, default).Wait(); + } + } + } + else + { + // create an empty notebook + var document = new InteractiveDocument(); + + using var stream = File.OpenWrite(_notebookPath); + Notebook.Write(document, stream, this._kernelInfoCollection); + stream.Flush(); + stream.Dispose(); + } + } + } + + /// + /// Run existing dotnet code from message. Don't modify the code, run it as is. + /// + /// code. + public async Task RunCode(string code) + { + if (this._interactiveService == null) + { + throw new Exception("InteractiveService is not initialized."); + } + + var result = await this._interactiveService.SubmitCSharpCodeAsync(code, default); + if (result != null) + { + // if result contains Error, return entire message + if (result.StartsWith("Error:")) + { + return result; + } + + // add cell if _notebookPath is not null + if (this._notebookPath != null) + { + await AddCellAsync(code, "csharp"); + } + + // if result is over 100 characters, only return the first 100 characters. + if (result.Length > 100) + { + result = result.Substring(0, 100) + " (...too long to present)"; + + return result; + } + + return result; + } + + // add cell if _notebookPath is not null + if (this._notebookPath != null) + { + await AddCellAsync(code, "csharp"); + } + + return "Code run successfully. no output is available."; + } + + /// + /// Install nuget packages. + /// + /// nuget package to install. + public async Task InstallNugetPackages(string[] nugetPackages) + { + if (this._interactiveService == null) + { + throw new Exception("InteractiveService is not initialized."); + } + + var codeSB = new StringBuilder(); + foreach (var nuget in nugetPackages ?? Array.Empty()) + { + var nugetInstallCommand = $"#r \"nuget:{nuget}\""; + codeSB.AppendLine(nugetInstallCommand); + await this._interactiveService.SubmitCSharpCodeAsync(nugetInstallCommand, default); + } + + var code = codeSB.ToString(); + if (this._notebookPath != null) + { + await AddCellAsync(code, "csharp"); + } + + var sb = new StringBuilder(); + sb.AppendLine("Installed nuget packages:"); + foreach (var nuget in nugetPackages ?? Array.Empty()) + { + sb.AppendLine($"- {nuget}"); + } + + return sb.ToString(); + } + + private async Task AddCellAsync(string cellContent, string kernelName) + { + if (!File.Exists(this._notebookPath)) + { + using var stream = File.OpenWrite(this._notebookPath); + Notebook.Write(new InteractiveDocument(), stream, this._kernelInfoCollection); + stream.Dispose(); + } + + using var readStream = File.OpenRead(this._notebookPath); + var document = Notebook.Read(readStream, this._kernelInfoCollection); + readStream.Dispose(); + + var cell = new InteractiveDocumentElement(cellContent, kernelName); + + document.Add(cell); + + using var writeStream = File.OpenWrite(this._notebookPath); + Notebook.Write(document, writeStream, this._kernelInfoCollection); + // sleep 3 seconds + await Task.Delay(3000); + writeStream.Flush(); + writeStream.Dispose(); + } + + private class RunCodeSchema + { + public string code { get; set; } = string.Empty; + } + + public Task RunCodeWrapper(string arguments) + { + var schema = JsonSerializer.Deserialize( + arguments, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }); + + return RunCode(schema!.code); + } + + public FunctionDefinition RunCodeFunction + { + get => new FunctionDefinition + { + Name = @"RunCode", + Description = """ +Run existing dotnet code from message. Don't modify the code, run it as is. +""", + Parameters = BinaryData.FromObjectAsJson(new + { + Type = "object", + Properties = new + { + code = new + { + Type = @"string", + Description = @"code.", + }, + }, + Required = new[] + { + "code", + }, + }, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }) + }; + } + + private class InstallNugetPackagesSchema + { + public string[] nugetPackages { get; set; } = Array.Empty(); + } + + public Task InstallNugetPackagesWrapper(string arguments) + { + var schema = JsonSerializer.Deserialize( + arguments, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }); + + return InstallNugetPackages(schema!.nugetPackages); + } + + public FunctionDefinition InstallNugetPackagesFunction + { + get => new FunctionDefinition + { + Name = @"InstallNugetPackages", + Description = """ +Install nuget packages. +""", + Parameters = BinaryData.FromObjectAsJson(new + { + Type = "object", + Properties = new + { + nugetPackages = new + { + Type = @"array", + Items = new + { + Type = @"string", + }, + Description = @"nuget package to install.", + }, + }, + Required = new[] + { + "nugetPackages", + }, + }, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }) + }; + } + public void Dispose() + { + this._interactiveService?.Dispose(); + } +} diff --git a/dotnet/src/AutoGen.DotnetInteractive/Extension/AgentExtension.cs b/dotnet/src/AutoGen.DotnetInteractive/Extension/AgentExtension.cs new file mode 100644 index 00000000000..034ca170e3d --- /dev/null +++ b/dotnet/src/AutoGen.DotnetInteractive/Extension/AgentExtension.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AgentExtension.cs + +using System.Text; +namespace AutoGen.DotnetInteractive; + +public static class AgentExtension +{ + /// + /// Register an AutoReply hook to run dotnet code block from message. + /// This hook will first detect if there's any dotnet code block (e.g. ```csharp and ```) in the most recent message. + /// if there's any, it will run the code block and send the result back as reply. + /// + /// agent + /// interactive service + /// code block prefix + /// code block suffix + /// maximum output to keep + /// + /// + /// + public static IAgent RegisterDotnetCodeBlockExectionHook( + this IAgent agent, + InteractiveService interactiveService, + string codeBlockPrefix = "```csharp", + string codeBlockSuffix = "```", + int maximumOutputToKeep = 500) + { + return agent.RegisterReply(async (msgs, ct) => + { + var lastMessage = msgs.LastOrDefault(); + if (lastMessage == null || lastMessage.GetContent() is null) + { + return null; + } + + // retrieve all code blocks from last message + var codeBlocks = lastMessage.GetContent()!.Split(new[] { codeBlockPrefix }, StringSplitOptions.RemoveEmptyEntries); + if (codeBlocks.Length <= 0) + { + return null; + } + + // run code blocks + var result = new StringBuilder(); + var i = 0; + result.AppendLine(@$"// [DOTNET_CODE_BLOCK_EXECUTION]"); + foreach (var codeBlock in codeBlocks) + { + var codeBlockIndex = codeBlock.IndexOf(codeBlockSuffix); + + if (codeBlockIndex == -1) + { + continue; + } + + // remove code block suffix + var code = codeBlock.Substring(0, codeBlockIndex).Trim(); + + if (code.Length == 0) + { + continue; + } + + var codeResult = await interactiveService.SubmitCSharpCodeAsync(code, ct); + if (codeResult != null) + { + result.AppendLine(@$"### Executing result for code block {i++}"); + result.AppendLine(codeResult); + result.AppendLine("### End of executing result ###"); + } + } + if (result.Length <= maximumOutputToKeep) + { + maximumOutputToKeep = result.Length; + } + + return new TextMessage(Role.Assistant, result.ToString().Substring(0, maximumOutputToKeep), from: agent.Name); + }); + } +} diff --git a/dotnet/src/AutoGen.DotnetInteractive/GlobalUsing.cs b/dotnet/src/AutoGen.DotnetInteractive/GlobalUsing.cs new file mode 100644 index 00000000000..d66bf001ed5 --- /dev/null +++ b/dotnet/src/AutoGen.DotnetInteractive/GlobalUsing.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GlobalUsing.cs + +global using AutoGen.Core; diff --git a/dotnet/src/AutoGen.DotnetInteractive/InteractiveService.cs b/dotnet/src/AutoGen.DotnetInteractive/InteractiveService.cs new file mode 100644 index 00000000000..0dc34f24e44 --- /dev/null +++ b/dotnet/src/AutoGen.DotnetInteractive/InteractiveService.cs @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// InteractiveService.cs + +using System.Diagnostics; +using System.Reactive.Linq; +using System.Reflection; +using Microsoft.DotNet.Interactive; +using Microsoft.DotNet.Interactive.App.Connection; +using Microsoft.DotNet.Interactive.Commands; +using Microsoft.DotNet.Interactive.Connection; +using Microsoft.DotNet.Interactive.Events; +using Microsoft.DotNet.Interactive.Utility; + +namespace AutoGen.DotnetInteractive; + +public class InteractiveService : IDisposable +{ + private Kernel? kernel = null; + private Process? process = null; + private bool disposedValue; + private const string DotnetInteractiveToolNotInstallMessage = "Cannot find a tool in the manifest file that has a command named 'dotnet-interactive'."; + //private readonly ProcessJobTracker jobTracker = new ProcessJobTracker(); + private string installingDirectory; + + public event EventHandler? DisplayEvent; + + public event EventHandler? Output; + + public event EventHandler? CommandFailed; + + public event EventHandler? HoverTextProduced; + + /// + /// Create an instance of InteractiveService + /// + /// dotnet interactive installing directory + public InteractiveService(string installingDirectory) + { + this.installingDirectory = installingDirectory; + } + + public async Task StartAsync(string workingDirectory, CancellationToken ct = default) + { + this.kernel = await this.CreateKernelAsync(workingDirectory, ct); + return true; + } + + public async Task SubmitCommandAsync(KernelCommand cmd, CancellationToken ct) + { + if (this.kernel == null) + { + throw new Exception("Kernel is not running"); + } + + try + { + var res = await this.kernel.SendAndThrowOnCommandFailedAsync(cmd, ct); + var events = res.Events; + var displayValues = events.Where(x => x is StandardErrorValueProduced || x is StandardOutputValueProduced || x is ReturnValueProduced) + .SelectMany(x => (x as DisplayEvent)!.FormattedValues); + + if (displayValues is null || displayValues.Count() == 0) + { + return null; + } + + return string.Join("\n", displayValues.Select(x => x.Value)); + } + catch (Exception ex) + { + return $"Error: {ex.Message}"; + } + } + + public async Task SubmitPowershellCodeAsync(string code, CancellationToken ct) + { + var command = new SubmitCode(code, targetKernelName: "pwsh"); + return await this.SubmitCommandAsync(command, ct); + } + + public async Task SubmitCSharpCodeAsync(string code, CancellationToken ct) + { + var command = new SubmitCode(code, targetKernelName: "csharp"); + return await this.SubmitCommandAsync(command, ct); + } + + private async Task CreateKernelAsync(string workingDirectory, CancellationToken ct = default) + { + try + { + var url = KernelHost.CreateHostUriForCurrentProcessId(); + var compositeKernel = new CompositeKernel("cbcomposite"); + var cmd = new string[] + { + "dotnet", + "tool", + "run", + "dotnet-interactive", + $"[cb-{Process.GetCurrentProcess().Id}]", + "stdio", + //"--default-kernel", + //"csharp", + "--working-dir", + $@"""{workingDirectory}""", + }; + var connector = new StdIoKernelConnector( + cmd, + "root-proxy", + url, + new DirectoryInfo(workingDirectory)); + + // Start the dotnet-interactive tool and get a proxy for the root composite kernel therein. + using var rootProxyKernel = await connector.CreateRootProxyKernelAsync().ConfigureAwait(false); + + // Get proxies for each subkernel present inside the dotnet-interactive tool. + var requestKernelInfoCommand = new RequestKernelInfo(rootProxyKernel.KernelInfo.RemoteUri); + var result = + await rootProxyKernel.SendAsync( + requestKernelInfoCommand, + ct).ConfigureAwait(false); + + var subKernels = result.Events.OfType(); + + foreach (var kernelInfoProduced in result.Events.OfType()) + { + var kernelInfo = kernelInfoProduced.KernelInfo; + if (kernelInfo is not null && !kernelInfo.IsProxy && !kernelInfo.IsComposite) + { + var proxyKernel = await connector.CreateProxyKernelAsync(kernelInfo).ConfigureAwait(false); + proxyKernel.SetUpValueSharingIfSupported(); + compositeKernel.Add(proxyKernel); + } + } + + //compositeKernel.DefaultKernelName = "csharp"; + compositeKernel.Add(rootProxyKernel); + + compositeKernel.KernelEvents.Subscribe(this.OnKernelDiagnosticEventReceived); + + return compositeKernel; + } + catch (CommandLineInvocationException ex) when (ex.Message.Contains("Cannot find a tool in the manifest file that has a command named 'dotnet-interactive'")) + { + var success = this.RestoreDotnetInteractive(); + + if (success) + { + return await this.CreateKernelAsync(workingDirectory, ct); + } + + throw; + } + } + + private void OnKernelDiagnosticEventReceived(KernelEvent ke) + { + this.WriteLine("Receive data from kernel"); + this.WriteLine(KernelEventEnvelope.Serialize(ke)); + + switch (ke) + { + case DisplayEvent de: + this.DisplayEvent?.Invoke(this, de); + break; + case CommandFailed cf: + this.CommandFailed?.Invoke(this, cf); + break; + case HoverTextProduced cf: + this.HoverTextProduced?.Invoke(this, cf); + break; + } + } + + private void WriteLine(string data) + { + this.Output?.Invoke(this, data); + } + + private bool RestoreDotnetInteractive() + { + this.WriteLine("Restore dotnet interactive tool"); + // write RestoreInteractive.config from embedded resource to this.workingDirectory + var assembly = Assembly.GetAssembly(typeof(InteractiveService))!; + var resourceName = "AutoGen.DotnetInteractive.RestoreInteractive.config"; + using (var stream = assembly.GetManifestResourceStream(resourceName)!) + using (var fileStream = File.Create(Path.Combine(this.installingDirectory, "RestoreInteractive.config"))) + { + stream.CopyTo(fileStream); + } + + // write dotnet-tool.json from embedded resource to this.workingDirectory + + resourceName = "AutoGen.DotnetInteractive.dotnet-tools.json"; + using (var stream2 = assembly.GetManifestResourceStream(resourceName)!) + using (var fileStream2 = File.Create(Path.Combine(this.installingDirectory, "dotnet-tools.json"))) + { + stream2.CopyTo(fileStream2); + } + + var psi = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"tool restore --configfile RestoreInteractive.config", + WorkingDirectory = this.installingDirectory, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + using var process = new Process { StartInfo = psi }; + process.OutputDataReceived += this.PrintProcessOutput; + process.ErrorDataReceived += this.PrintProcessOutput; + process.Start(); + process.BeginErrorReadLine(); + process.BeginOutputReadLine(); + process.WaitForExit(); + + return process.ExitCode == 0; + } + + private void PrintProcessOutput(object sender, DataReceivedEventArgs e) + { + if (!string.IsNullOrEmpty(e.Data)) + { + this.WriteLine(e.Data); + } + } + + public bool IsRunning() + { + return this.kernel != null; + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + this.kernel?.Dispose(); + + if (this.process != null) + { + this.process.Kill(); + this.process.Dispose(); + } + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/dotnet/src/AutoGen.DotnetInteractive/RestoreInteractive.config b/dotnet/src/AutoGen.DotnetInteractive/RestoreInteractive.config new file mode 100644 index 00000000000..390adb4ab6f --- /dev/null +++ b/dotnet/src/AutoGen.DotnetInteractive/RestoreInteractive.config @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/dotnet/src/AutoGen.DotnetInteractive/Utils.cs b/dotnet/src/AutoGen.DotnetInteractive/Utils.cs new file mode 100644 index 00000000000..d10208d508c --- /dev/null +++ b/dotnet/src/AutoGen.DotnetInteractive/Utils.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Utils.cs + +using System.Collections; +using System.Collections.Immutable; +using Microsoft.DotNet.Interactive; +using Microsoft.DotNet.Interactive.Commands; +using Microsoft.DotNet.Interactive.Connection; +using Microsoft.DotNet.Interactive.Events; + +public static class ObservableExtensions +{ + public static SubscribedList ToSubscribedList(this IObservable source) + { + return new SubscribedList(source); + } +} + +public static class KernelExtensions +{ + internal static void SetUpValueSharingIfSupported(this ProxyKernel proxyKernel) + { + var supportedCommands = proxyKernel.KernelInfo.SupportedKernelCommands; + if (supportedCommands.Any(d => d.Name == nameof(RequestValue)) && + supportedCommands.Any(d => d.Name == nameof(SendValue))) + { + proxyKernel.UseValueSharing(); + } + } + + internal static async Task SendAndThrowOnCommandFailedAsync( + this Kernel kernel, + KernelCommand command, + CancellationToken cancellationToken) + { + var result = await kernel.SendAsync(command, cancellationToken); + result.ThrowOnCommandFailed(); + return result; + } + + private static void ThrowOnCommandFailed(this KernelCommandResult result) + { + var failedEvents = result.Events.OfType(); + if (!failedEvents.Any()) + { + return; + } + + if (failedEvents.Skip(1).Any()) + { + var innerExceptions = failedEvents.Select(f => f.GetException()); + throw new AggregateException(innerExceptions); + } + else + { + throw failedEvents.Single().GetException(); + } + } + + private static Exception GetException(this CommandFailed commandFailedEvent) + => new Exception(commandFailedEvent.Message); +} + +public class SubscribedList : IReadOnlyList, IDisposable +{ + private ImmutableArray _list = ImmutableArray.Empty; + private readonly IDisposable _subscription; + + public SubscribedList(IObservable source) + { + _subscription = source.Subscribe(x => _list = _list.Add(x)); + } + + public IEnumerator GetEnumerator() + { + return ((IEnumerable)_list).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => _list.Length; + + public T this[int index] => _list[index]; + + public void Dispose() => _subscription.Dispose(); +} diff --git a/dotnet/src/AutoGen.DotnetInteractive/dotnet-tools.json b/dotnet/src/AutoGen.DotnetInteractive/dotnet-tools.json new file mode 100644 index 00000000000..b2677b61678 --- /dev/null +++ b/dotnet/src/AutoGen.DotnetInteractive/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "Microsoft.dotnet-interactive": { + "version": "1.0.431302", + "commands": [ + "dotnet-interactive" + ] + } + } +} \ No newline at end of file diff --git a/dotnet/src/AutoGen.LMStudio/AutoGen.LMStudio.csproj b/dotnet/src/AutoGen.LMStudio/AutoGen.LMStudio.csproj new file mode 100644 index 00000000000..b738fe02bb7 --- /dev/null +++ b/dotnet/src/AutoGen.LMStudio/AutoGen.LMStudio.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + AutoGen.LMStudio + + + + + + + AutoGen.LMStudio + + Provide support for consuming LMStudio openai-like API service in AutoGen + + + + + + + + + diff --git a/dotnet/src/AutoGen.LMStudio/GlobalUsing.cs b/dotnet/src/AutoGen.LMStudio/GlobalUsing.cs new file mode 100644 index 00000000000..d66bf001ed5 --- /dev/null +++ b/dotnet/src/AutoGen.LMStudio/GlobalUsing.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GlobalUsing.cs + +global using AutoGen.Core; diff --git a/dotnet/src/AutoGen.LMStudio/LMStudioAgent.cs b/dotnet/src/AutoGen.LMStudio/LMStudioAgent.cs new file mode 100644 index 00000000000..ac5150cc9df --- /dev/null +++ b/dotnet/src/AutoGen.LMStudio/LMStudioAgent.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// LMStudioAgent.cs + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoGen.OpenAI; +using Azure.AI.OpenAI; +using Azure.Core.Pipeline; +using Azure.Core; + +namespace AutoGen.LMStudio; + +/// +/// agent that consumes local server from LM Studio +/// +/// +/// [!code-csharp[LMStudioAgent](../../sample/AutoGen.BasicSamples/Example08_LMStudio.cs?name=lmstudio_example_1)] +/// +public class LMStudioAgent : IAgent +{ + private readonly GPTAgent innerAgent; + + public LMStudioAgent( + string name, + LMStudioConfig config, + string systemMessage = "You are a helpful AI assistant", + float temperature = 0.7f, + int maxTokens = 1024, + IEnumerable? functions = null, + IDictionary>>? functionMap = null) + { + var client = ConfigOpenAIClientForLMStudio(config); + innerAgent = new GPTAgent( + name: name, + systemMessage: systemMessage, + openAIClient: client, + modelName: "llm", // model name doesn't matter for LM Studio + temperature: temperature, + maxTokens: maxTokens, + functions: functions, + functionMap: functionMap); + } + + public string Name => innerAgent.Name; + + public Task GenerateReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + System.Threading.CancellationToken cancellationToken = default) + { + return innerAgent.GenerateReplyAsync(messages, options, cancellationToken); + } + + private OpenAIClient ConfigOpenAIClientForLMStudio(LMStudioConfig config) + { + // create uri from host and port + var uri = config.Uri; + var accessToken = new AccessToken(string.Empty, DateTimeOffset.Now.AddDays(180)); + var tokenCredential = DelegatedTokenCredential.Create((_, _) => accessToken); + var openAIClient = new OpenAIClient(uri, tokenCredential); + + // remove authenication header from pipeline + var pipeline = HttpPipelineBuilder.Build( + new OpenAIClientOptions(OpenAIClientOptions.ServiceVersion.V2022_12_01), + Array.Empty(), + [], + new ResponseClassifier()); + + // use reflection to override _pipeline field + var field = typeof(OpenAIClient).GetField("_pipeline", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + field.SetValue(openAIClient, pipeline); + + // use reflection to set _isConfiguredForAzureOpenAI to false + var isConfiguredForAzureOpenAIField = typeof(OpenAIClient).GetField("_isConfiguredForAzureOpenAI", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + isConfiguredForAzureOpenAIField.SetValue(openAIClient, false); + + return openAIClient; + } +} diff --git a/dotnet/src/AutoGen.LMStudio/LMStudioConfig.cs b/dotnet/src/AutoGen.LMStudio/LMStudioConfig.cs new file mode 100644 index 00000000000..4cf18210a43 --- /dev/null +++ b/dotnet/src/AutoGen.LMStudio/LMStudioConfig.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// LMStudioConfig.cs + +using System; + +/// +/// Add support for consuming openai-like API from LM Studio +/// +public class LMStudioConfig : ILLMConfig +{ + public LMStudioConfig(string host, int port, int version = 1) + { + this.Host = host; + this.Port = port; + this.Version = version; + } + + public string Host { get; } + + public int Port { get; } + + public int Version { get; } + + public Uri Uri => new Uri($"http://{Host}:{Port}/v{Version}"); +} diff --git a/dotnet/src/AutoGen.LMStudio/README.md b/dotnet/src/AutoGen.LMStudio/README.md new file mode 100644 index 00000000000..1e5caf4756c --- /dev/null +++ b/dotnet/src/AutoGen.LMStudio/README.md @@ -0,0 +1,31 @@ +## AutoGen.LMStudio + +This package provides support for consuming openai-like API from LMStudio local server. + +## Installation +To use `AutoGen.LMStudio`, add the following package to your `.csproj` file: + +```xml + + + +``` + +## Usage +```csharp +using AutoGen.LMStudio; +var localServerEndpoint = "localhost"; +var port = 5000; +var lmStudioConfig = new LMStudioConfig(localServerEndpoint, port); +var agent = new LMStudioAgent( + name: "agent", + systemMessage: "You are an agent that help user to do some tasks.", + lmStudioConfig: lmStudioConfig) + .RegisterPrintMessage(); // register a hook to print message nicely to console + +await agent.SendAsync("Can you write a piece of C# code to calculate 100th of fibonacci?"); +``` + +## Update history +### Update on 0.0.7 (2024-02-11) +- Add `LMStudioAgent` to support consuming openai-like API from LMStudio local server. diff --git a/dotnet/src/AutoGen.Mistral/Agent/MistralClientAgent.cs b/dotnet/src/AutoGen.Mistral/Agent/MistralClientAgent.cs new file mode 100644 index 00000000000..2ba28bbb701 --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/Agent/MistralClientAgent.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MistralClientAgent.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoGen.Core; +using AutoGen.Mistral.Extension; + +namespace AutoGen.Mistral; + +/// +/// Mistral client agent. +/// +/// This agent supports the following input message types: +/// +/// where T is +/// +/// +/// This agent returns the following message types: +/// +/// where T is +/// +/// +/// You can register this agent with +/// to support more AutoGen message types. +/// +public class MistralClientAgent : IStreamingAgent +{ + private readonly MistralClient _client; + private readonly string _systemMessage; + private readonly string _model; + private readonly int? _randomSeed; + private readonly bool _jsonOutput = false; + private ToolChoiceEnum? _toolChoice; + + /// + /// Create a new instance of . + /// + /// + /// the name of this agent + /// the mistral model id. + /// system message. + /// the seed to generate output. + /// tool choice strategy. + /// use json output. + public MistralClientAgent( + MistralClient client, + string name, + string model, + string systemMessage = "You are a helpful AI assistant", + int? randomSeed = null, + ToolChoiceEnum? toolChoice = null, + bool jsonOutput = false) + { + _client = client; + Name = name; + _systemMessage = systemMessage; + _model = model; + _randomSeed = randomSeed; + _jsonOutput = jsonOutput; + _toolChoice = toolChoice; + } + + public string Name { get; } + + public async Task GenerateReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + CancellationToken cancellationToken = default) + { + var request = BuildChatRequest(messages, options); + var response = await _client.CreateChatCompletionsAsync(request); + + return new MessageEnvelope(response, from: this.Name); + } + + public async Task> GenerateStreamingReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + CancellationToken cancellationToken = default) + { + var request = BuildChatRequest(messages, options); + var response = _client.StreamingChatCompletionsAsync(request); + + return ProcessMessage(response); + } + + private async IAsyncEnumerable ProcessMessage(IAsyncEnumerable response) + { + await foreach (var content in response) + { + yield return new MessageEnvelope(content, from: this.Name); + } + } + + private ChatCompletionRequest BuildChatRequest(IEnumerable messages, GenerateReplyOptions? options) + { + var chatHistory = BuildChatHistory(messages); + var chatRequest = new ChatCompletionRequest(model: _model, messages: chatHistory.ToList(), temperature: options?.Temperature, randomSeed: _randomSeed) + { + MaxTokens = options?.MaxToken, + ResponseFormat = _jsonOutput ? new ResponseFormat() { ResponseFormatType = "json_object" } : null, + }; + + if (options?.Functions != null) + { + chatRequest.Tools = options.Functions.Select(f => new FunctionTool(f.ToMistralFunctionDefinition())).ToList(); + chatRequest.ToolChoice = _toolChoice ?? ToolChoiceEnum.Auto; + } + + return chatRequest; + } + + private IEnumerable BuildChatHistory(IEnumerable messages) + { + var history = messages.Select(m => m switch + { + IMessage chatMessage => chatMessage.Content, + _ => throw new ArgumentException("Invalid message type") + }); + + // if there's no system message in the history, add one to the beginning + if (!history.Any(m => m.Role == ChatMessage.RoleEnum.System)) + { + history = new[] { new ChatMessage(ChatMessage.RoleEnum.System, _systemMessage) }.Concat(history); + } + + return history; + } +} diff --git a/dotnet/src/AutoGen.Mistral/AutoGen.Mistral.csproj b/dotnet/src/AutoGen.Mistral/AutoGen.Mistral.csproj new file mode 100644 index 00000000000..f7de19ca0c9 --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/AutoGen.Mistral.csproj @@ -0,0 +1,27 @@ + + + + netstandard2.0 + AutoGen.Mistral + + + + + + + AutoGen.Mistral + + Provide support for consuming Mistral model in AutoGen + + + + + + + + + + + + + diff --git a/dotnet/src/AutoGen.Mistral/Converters/JsonPropertyNameEnumConverter.cs b/dotnet/src/AutoGen.Mistral/Converters/JsonPropertyNameEnumConverter.cs new file mode 100644 index 00000000000..5a4f9f9cb18 --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/Converters/JsonPropertyNameEnumConverter.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// JsonPropertyNameEnumConverter.cs + +using System; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoGen.Mistral; + +internal class JsonPropertyNameEnumConverter : JsonConverter where T : struct, Enum +{ + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string value = reader.GetString() ?? throw new JsonException("Value was null."); + + foreach (var field in typeToConvert.GetFields()) + { + var attribute = field.GetCustomAttribute(); + if (attribute?.Name == value) + { + return (T)Enum.Parse(typeToConvert, field.Name); + } + } + + throw new JsonException($"Unable to convert \"{value}\" to enum {typeToConvert}."); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + var field = value.GetType().GetField(value.ToString()); + var attribute = field.GetCustomAttribute(); + + if (attribute != null) + { + writer.WriteStringValue(attribute.Name); + } + else + { + writer.WriteStringValue(value.ToString()); + } + } +} diff --git a/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionRequest.cs b/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionRequest.cs new file mode 100644 index 00000000000..71a084673f1 --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionRequest.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ChatCompletionRequest.cs + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AutoGen.Mistral; + +public class ChatCompletionRequest +{ + /// + /// Initializes a new instance of the class. + /// + /// ID of the model to use. You can use the [List Available Models](/api#operation/listModels) API to see all of your available models, or see our [Model overview](/models) for model descriptions. (required). + /// The prompt(s) to generate completions for, encoded as a list of dict with role and content. The first prompt role should be `user` or `system`. (required). + /// What sampling temperature to use, between 0.0 and 1.0. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend altering this or `top_p` but not both. (default to 0.7M). + /// Nucleus sampling, where the model considers the results of the tokens with `top_p` probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or `temperature` but not both. (default to 1M). + /// The maximum number of tokens to generate in the completion. The token count of your prompt plus `max_tokens` cannot exceed the model's context length. . + /// Whether to stream back partial progress. If set, tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a data: [DONE] message. Otherwise, the server will hold the request open until the timeout or until completion, with the response containing the full result as JSON. (default to false). + /// Whether to inject a safety prompt before all conversations. (default to false). + /// The seed to use for random sampling. If set, different calls will generate deterministic results. . + public ChatCompletionRequest(string? model = default(string), List? messages = default(List), float? temperature = 0.7f, float? topP = 1f, int? maxTokens = default(int?), bool? stream = false, bool safePrompt = false, int? randomSeed = default(int?)) + { + // to ensure "model" is required (not null) + if (model == null) + { + throw new ArgumentNullException("model is a required property for ChatCompletionRequest and cannot be null"); + } + this.Model = model; + // to ensure "messages" is required (not null) + if (messages == null) + { + throw new ArgumentNullException("messages is a required property for ChatCompletionRequest and cannot be null"); + } + this.Messages = messages; + // use default value if no "temperature" provided + this.Temperature = temperature ?? 0.7f; + // use default value if no "topP" provided + this.TopP = topP ?? 1f; + this.MaxTokens = maxTokens; + // use default value if no "stream" provided + this.Stream = stream ?? false; + this.SafePrompt = safePrompt; + this.RandomSeed = randomSeed; + } + /// + /// ID of the model to use. You can use the [List Available Models](/api#operation/listModels) API to see all of your available models, or see our [Model overview](/models) for model descriptions. + /// + /// ID of the model to use. You can use the [List Available Models](/api#operation/listModels) API to see all of your available models, or see our [Model overview](/models) for model descriptions. + /// mistral-tiny + [JsonPropertyName("model")] + public string Model { get; set; } + + /// + /// The prompt(s) to generate completions for, encoded as a list of dict with role and content. The first prompt role should be `user` or `system`. + /// + /// The prompt(s) to generate completions for, encoded as a list of dict with role and content. The first prompt role should be `user` or `system`. + /// [{"role":"user","content":"What is the best French cheese?"}] + [JsonPropertyName("messages")] + public List Messages { get; set; } + + /// + /// What sampling temperature to use, between 0.0 and 1.0. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend altering this or `top_p` but not both. + /// + /// What sampling temperature to use, between 0.0 and 1.0. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend altering this or `top_p` but not both. + /// 0.7 + [JsonPropertyName("temperature")] + public float? Temperature { get; set; } + + /// + /// Nucleus sampling, where the model considers the results of the tokens with `top_p` probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or `temperature` but not both. + /// + /// Nucleus sampling, where the model considers the results of the tokens with `top_p` probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or `temperature` but not both. + /// 1 + [JsonPropertyName("top_p")] + public float? TopP { get; set; } + + /// + /// The maximum number of tokens to generate in the completion. The token count of your prompt plus `max_tokens` cannot exceed the model's context length. + /// + /// The maximum number of tokens to generate in the completion. The token count of your prompt plus `max_tokens` cannot exceed the model's context length. + /// 16 + [JsonPropertyName("max_tokens")] + public int? MaxTokens { get; set; } + + /// + /// Whether to stream back partial progress. If set, tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a data: [DONE] message. Otherwise, the server will hold the request open until the timeout or until completion, with the response containing the full result as JSON. + /// + /// Whether to stream back partial progress. If set, tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a data: [DONE] message. Otherwise, the server will hold the request open until the timeout or until completion, with the response containing the full result as JSON. + [JsonPropertyName("stream")] + public bool? Stream { get; set; } + + /// + /// Whether to inject a safety prompt before all conversations. + /// + /// Whether to inject a safety prompt before all conversations. + [JsonPropertyName("safe_prompt")] + public bool SafePrompt { get; set; } + + /// + /// The seed to use for random sampling. If set, different calls will generate deterministic results. + /// + /// The seed to use for random sampling. If set, different calls will generate deterministic results. + [JsonPropertyName("random_seed")] + public int? RandomSeed { get; set; } + + [JsonPropertyName("tools")] + public List? Tools { get; set; } + + [JsonPropertyName("tool_choice")] + public ToolChoiceEnum? ToolChoice { get; set; } + + [JsonPropertyName("response_format")] + public ResponseFormat? ResponseFormat { get; set; } = null; +} diff --git a/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs b/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs new file mode 100644 index 00000000000..58dcf5297a3 --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AutoGen.Mistral; + +public class ChatCompletionResponse +{ + /// + /// Gets or Sets Id + /// + /// cmpl-e5cc70bb28c444948073e77776eb30ef + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// + /// Gets or Sets VarObject + /// + /// chat.completion + [JsonPropertyName("object")] + public string? VarObject { get; set; } + + /// + /// Gets or Sets Created + /// + /// 1702256327 + [JsonPropertyName("created")] + public int Created { get; set; } + + /// + /// Gets or Sets Model + /// + /// mistral-tiny + [JsonPropertyName("model")] + public string? Model { get; set; } + + /// + /// Gets or Sets Choices + /// + [JsonPropertyName("choices")] + public List? Choices { get; set; } + + /// + /// Gets or Sets Usage + /// + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } +} diff --git a/dotnet/src/AutoGen.Mistral/DTOs/ChatMessage.cs b/dotnet/src/AutoGen.Mistral/DTOs/ChatMessage.cs new file mode 100644 index 00000000000..c5dae2aa34d --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/DTOs/ChatMessage.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ChatMessage.cs + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AutoGen.Mistral; + +public class ChatMessage +{ + /// + /// Initializes a new instance of the class. + /// + /// role. + /// content. + public ChatMessage(RoleEnum? role = default(RoleEnum?), string? content = null) + { + this.Role = role; + this.Content = content; + } + + [JsonConverter(typeof(JsonPropertyNameEnumConverter))] + public enum RoleEnum + { + /// + /// Enum System for value: system + /// + [JsonPropertyName("system")] + //[EnumMember(Value = "system")] + System = 1, + + /// + /// Enum User for value: user + /// + [JsonPropertyName("user")] + //[EnumMember(Value = "user")] + User = 2, + + /// + /// Enum Assistant for value: assistant + /// + [JsonPropertyName("assistant")] + //[EnumMember(Value = "assistant")] + Assistant = 3, + + [JsonPropertyName("tool")] + Tool = 4, + } + + /// + /// Gets or Sets Role + /// + [JsonPropertyName("role")] + public RoleEnum? Role { get; set; } + + /// + /// Gets or Sets Content + /// + [JsonPropertyName("content")] + public string? Content { get; set; } + + /// + /// Gets or Sets name for tool calls + /// + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("tool_calls")] + public List? ToolCalls { get; set; } +} + +public class FunctionContent +{ + public FunctionContent(FunctionCall function) + { + this.Function = function; + } + + [JsonPropertyName("function")] + public FunctionCall Function { get; set; } + + public class FunctionCall + { + public FunctionCall(string name, string arguments) + { + this.Name = name; + this.Arguments = arguments; + } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("arguments")] + public string Arguments { get; set; } + } +} diff --git a/dotnet/src/AutoGen.Mistral/DTOs/Choice.cs b/dotnet/src/AutoGen.Mistral/DTOs/Choice.cs new file mode 100644 index 00000000000..ef874c90a0e --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/DTOs/Choice.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Choice.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.Mistral; + +public class Choice +{ + [JsonConverter(typeof(JsonPropertyNameEnumConverter))] + public enum FinishReasonEnum + { + /// + /// Enum Stop for value: stop + /// + [JsonPropertyName("stop")] + Stop = 1, + + /// + /// Enum Length for value: length + /// + [JsonPropertyName("length")] + Length = 2, + + /// + /// Enum ModelLength for value: model_length + /// + [JsonPropertyName("model_length")] + ModelLength = 3, + + [JsonPropertyName("error")] + Error = 4, + + [JsonPropertyName("tool_calls")] + ToolCalls = 5, + } + + /// + /// Gets or Sets FinishReason + /// + [JsonPropertyName("finish_reason")] + public FinishReasonEnum? FinishReason { get; set; } + + [JsonPropertyName("index")] + public int Index { get; set; } + + /// + /// Gets or Sets Message + /// + [JsonPropertyName("message")] + public ChatMessage? Message { get; set; } + + /// + /// Gets or Sets Delta + /// + [JsonPropertyName("delta")] + public ChatMessage? Delta { get; set; } +} diff --git a/dotnet/src/AutoGen.Mistral/DTOs/Error.cs b/dotnet/src/AutoGen.Mistral/DTOs/Error.cs new file mode 100644 index 00000000000..ed04721c67d --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/DTOs/Error.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace AutoGen.Mistral +{ + public class Error + { + public Error(string type, string message, string? param = default(string), string? code = default(string)) + { + Type = type; + Message = message; + Param = param; + Code = code; + } + + [JsonPropertyName("type")] + public string Type { get; set; } + + /// + /// Gets or Sets Message + /// + [JsonPropertyName("message")] + public string Message { get; set; } + + /// + /// Gets or Sets Param + /// + [JsonPropertyName("param")] + public string? Param { get; set; } + + /// + /// Gets or Sets Code + /// + [JsonPropertyName("code")] + public string? Code { get; set; } + } +} diff --git a/dotnet/src/AutoGen.Mistral/DTOs/ErrorResponse.cs b/dotnet/src/AutoGen.Mistral/DTOs/ErrorResponse.cs new file mode 100644 index 00000000000..ea3a999cc08 --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/DTOs/ErrorResponse.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ErrorResponse.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.Mistral; + +public class ErrorResponse +{ + public ErrorResponse(Error error) + { + Error = error; + } + /// + /// Gets or Sets Error + /// + [JsonPropertyName("error")] + public Error Error { get; set; } +} diff --git a/dotnet/src/AutoGen.Mistral/DTOs/FunctionDefinition.cs b/dotnet/src/AutoGen.Mistral/DTOs/FunctionDefinition.cs new file mode 100644 index 00000000000..663920330a2 --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/DTOs/FunctionDefinition.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// FunctionDefinition.cs + +using System.Text.Json.Serialization; +using Json.Schema; + +namespace AutoGen.Mistral; + +public class FunctionDefinition +{ + public FunctionDefinition(string name, string description, JsonSchema? parameters = default) + { + Name = name; + Description = description; + Parameters = parameters; + } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("parameters")] + public JsonSchema? Parameters { get; set; } +} diff --git a/dotnet/src/AutoGen.Mistral/DTOs/Model.cs b/dotnet/src/AutoGen.Mistral/DTOs/Model.cs new file mode 100644 index 00000000000..2d653f71859 --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/DTOs/Model.cs @@ -0,0 +1,61 @@ +using System; +using System.Text.Json.Serialization; + +namespace AutoGen.Mistral; + +public class Model +{ + /// + /// Initializes a new instance of the class. + /// + /// id (required). + /// varObject (required). + /// created (required). + /// ownedBy (required). + public Model(string? id = default(string), string? varObject = default(string), int created = default(int), string? ownedBy = default(string)) + { + // to ensure "id" is required (not null) + if (id == null) + { + throw new ArgumentNullException("id is a required property for Model and cannot be null"); + } + this.Id = id; + // to ensure "varObject" is required (not null) + if (varObject == null) + { + throw new ArgumentNullException("varObject is a required property for Model and cannot be null"); + } + this.VarObject = varObject; + this.Created = created; + // to ensure "ownedBy" is required (not null) + if (ownedBy == null) + { + throw new ArgumentNullException("ownedBy is a required property for Model and cannot be null"); + } + this.OwnedBy = ownedBy; + } + + /// + /// Gets or Sets Id + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// Gets or Sets VarObject + /// + [JsonPropertyName("object")] + public string VarObject { get; set; } + + /// + /// Gets or Sets Created + /// + [JsonPropertyName("created")] + public int Created { get; set; } + + /// + /// Gets or Sets OwnedBy + /// + [JsonPropertyName("owned_by")] + public string OwnedBy { get; set; } +} diff --git a/dotnet/src/AutoGen.Mistral/DTOs/ResponseFormat.cs b/dotnet/src/AutoGen.Mistral/DTOs/ResponseFormat.cs new file mode 100644 index 00000000000..08a5c7426ea --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/DTOs/ResponseFormat.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ResponseFormat.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.Mistral; + +public class ResponseFormat +{ + [JsonPropertyName("type")] + public string ResponseFormatType { get; set; } = "json_object"; +} diff --git a/dotnet/src/AutoGen.Mistral/DTOs/Tool.cs b/dotnet/src/AutoGen.Mistral/DTOs/Tool.cs new file mode 100644 index 00000000000..49e1a9b777d --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/DTOs/Tool.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Tool.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.Mistral; + +public abstract class ToolBase +{ + [JsonPropertyName("type")] + public string Type { get; set; } + + public ToolBase(string type) + { + Type = type; + } +} + +public class FunctionTool : ToolBase +{ + public FunctionTool(FunctionDefinition function) + : base("function") + { + Function = function; + } + + [JsonPropertyName("function")] + public FunctionDefinition Function { get; set; } +} + +[JsonConverter(typeof(JsonPropertyNameEnumConverter))] +public enum ToolChoiceEnum +{ + /// + /// Auto-detect whether to call a function. + /// + [JsonPropertyName("auto")] + Auto = 0, + + /// + /// Won't call a function. + /// + [JsonPropertyName("none")] + None, + + /// + /// Force to call a function. + /// + [JsonPropertyName("any")] + Any, +} diff --git a/dotnet/src/AutoGen.Mistral/DTOs/Usage.cs b/dotnet/src/AutoGen.Mistral/DTOs/Usage.cs new file mode 100644 index 00000000000..3e739e3bc11 --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/DTOs/Usage.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Usage.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.Mistral; + +public class Usage +{ + [JsonPropertyName("prompt_tokens")] + public int PromptTokens { get; set; } + + /// + /// Gets or Sets CompletionTokens + /// + /// 93 + [JsonPropertyName("completion_tokens")] + public int CompletionTokens { get; set; } + + /// + /// Gets or Sets TotalTokens + /// + /// 107 + [JsonPropertyName("total_tokens")] + public int TotalTokens { get; set; } +} diff --git a/dotnet/src/AutoGen.Mistral/Extension/FunctionContractExtension.cs b/dotnet/src/AutoGen.Mistral/Extension/FunctionContractExtension.cs new file mode 100644 index 00000000000..eb38b32982a --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/Extension/FunctionContractExtension.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// FunctionContractExtension.cs + +using System; +using System.Collections.Generic; +using AutoGen.Core; +using Json.Schema; +using Json.Schema.Generation; + +namespace AutoGen.Mistral.Extension; + +public static class FunctionContractExtension +{ + /// + /// Convert a to a that can be used in funciton call. + /// + /// function contract + /// + public static FunctionDefinition ToMistralFunctionDefinition(this FunctionContract functionContract) + { + var functionDefinition = new FunctionDefinition(functionContract.Name ?? throw new Exception("Function name cannot be null"), functionContract.Description ?? throw new Exception("Function description cannot be null")); + var requiredParameterNames = new List(); + var propertiesSchemas = new Dictionary(); + var propertySchemaBuilder = new JsonSchemaBuilder().Type(SchemaValueType.Object); + foreach (var param in functionContract.Parameters ?? []) + { + if (param.Name is null) + { + throw new InvalidOperationException("Parameter name cannot be null"); + } + + var schemaBuilder = new JsonSchemaBuilder().FromType(param.ParameterType ?? throw new ArgumentNullException(nameof(param.ParameterType))); + if (param.Description != null) + { + schemaBuilder = schemaBuilder.Description(param.Description); + } + + if (param.IsRequired) + { + requiredParameterNames.Add(param.Name); + } + + var schema = schemaBuilder.Build(); + propertiesSchemas[param.Name] = schema; + + } + propertySchemaBuilder = propertySchemaBuilder.Properties(propertiesSchemas); + propertySchemaBuilder = propertySchemaBuilder.Required(requiredParameterNames); + + var option = new System.Text.Json.JsonSerializerOptions() + { + PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase + }; + + functionDefinition.Parameters = propertySchemaBuilder.Build(); + + return functionDefinition; + } +} diff --git a/dotnet/src/AutoGen.Mistral/Extension/MistralAgentExtension.cs b/dotnet/src/AutoGen.Mistral/Extension/MistralAgentExtension.cs new file mode 100644 index 00000000000..5b3c998b6c0 --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/Extension/MistralAgentExtension.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MistralAgentExtension.cs + +using AutoGen.Core; + +namespace AutoGen.Mistral.Extension; + +public static class MistralAgentExtension +{ + /// + /// Register a to support more AutoGen message types. + /// + public static MiddlewareStreamingAgent RegisterMessageConnector( + this MistralClientAgent agent, MistralChatMessageConnector? connector = null) + { + if (connector == null) + { + connector = new MistralChatMessageConnector(); + } + + return agent.RegisterStreamingMiddleware(connector) + .RegisterMiddleware(connector); + + } + + /// + /// Register a to support more AutoGen message types. + /// + public static MiddlewareStreamingAgent RegisterMessageConnector( + this MiddlewareStreamingAgent agent, MistralChatMessageConnector? connector = null) + { + if (connector == null) + { + connector = new MistralChatMessageConnector(); + } + + return agent.RegisterStreamingMiddleware(connector) + .RegisterMiddleware(connector); + } +} diff --git a/dotnet/src/AutoGen.Mistral/Middleware/MistralChatMessageConnector.cs b/dotnet/src/AutoGen.Mistral/Middleware/MistralChatMessageConnector.cs new file mode 100644 index 00000000000..44f34401e1c --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/Middleware/MistralChatMessageConnector.cs @@ -0,0 +1,324 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MistralChatMessageConnector.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using AutoGen.Core; + +namespace AutoGen.Mistral; + +public class MistralChatMessageConnector : IStreamingMiddleware, IMiddleware +{ + public string? Name => nameof(MistralChatMessageConnector); + + public Task> InvokeAsync(MiddlewareContext context, IStreamingAgent agent, CancellationToken cancellationToken = default) + { + return Task.FromResult(StreamingInvoke(context, agent, cancellationToken)); + } + + private async IAsyncEnumerable StreamingInvoke(MiddlewareContext context, IStreamingAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var messages = context.Messages; + var chatMessages = ProcessMessage(messages, agent); + var chunks = new List(); + await foreach (var reply in await agent.GenerateStreamingReplyAsync(chatMessages, context.Options, cancellationToken)) + { + if (reply is IStreamingMessage chatMessage) + { + chunks.Add(chatMessage.Content); + var response = ProcessChatCompletionResponse(chatMessage, agent); + if (response is not null) + { + yield return response; + } + } + else + { + yield return reply; + } + } + + // if chunks is not empty, then return the aggregate message as the last message + // this is to meet the requirement of streaming call api + // where the last message should be the same result of non-streaming call api + if (chunks.Count == 0) + { + yield break; + } + + var lastResponse = chunks.Last() ?? throw new ArgumentNullException("chunks.Last()"); + var finalResponse = chunks.First() ?? throw new ArgumentNullException("chunks.First()"); + if (lastResponse.Choices!.First().FinishReason == Choice.FinishReasonEnum.ToolCalls) + { + // process as tool call message + foreach (var response in chunks) + { + if (finalResponse.Choices!.First().Message is null) + { + finalResponse.Choices!.First().Message = response.Choices!.First().Delta; + if (finalResponse.Choices!.First().Message!.ToolCalls is null) + { + finalResponse.Choices!.First().Message!.ToolCalls = new List(); + } + } + + if (response.Choices!.First().Delta!.ToolCalls is not null) + { + finalResponse.Choices!.First().Message!.ToolCalls!.AddRange(response.Choices!.First().Delta!.ToolCalls!); + } + + finalResponse.Choices!.First().FinishReason = response.Choices!.First().FinishReason; + + // the usage information will be included in the last message + if (response.Usage is not null) + { + finalResponse.Usage = response.Usage; + } + } + } + else + { + // process as plain text message + foreach (var response in chunks) + { + if (finalResponse.Choices!.First().Message is null) + { + finalResponse.Choices!.First().Message = response.Choices!.First().Delta; + } + + finalResponse.Choices!.First().Message!.Content += response.Choices!.First().Delta!.Content; + finalResponse.Choices!.First().FinishReason = response.Choices!.First().FinishReason; + // the usage information will be included in the last message + if (response.Usage is not null) + { + finalResponse.Usage = response.Usage; + } + } + } + + yield return PostProcessMessage(finalResponse, agent); + } + + public async Task InvokeAsync(MiddlewareContext context, IAgent agent, CancellationToken cancellationToken = default) + { + var messages = context.Messages; + var chatMessages = ProcessMessage(messages, agent); + var response = await agent.GenerateReplyAsync(chatMessages, context.Options, cancellationToken); + + if (response is IMessage chatMessage) + { + return PostProcessMessage(chatMessage.Content, agent); + } + else + { + return response; + } + } + + private IEnumerable ProcessMessage(IEnumerable messages, IAgent agent) + { + return messages.SelectMany(m => + { + if (m is IMessage chatMessage) + { + return [MessageEnvelope.Create(chatMessage.Content, from: chatMessage.From)]; + } + else + { + return m switch + { + TextMessage textMessage => ProcessTextMessage(textMessage, agent), + ToolCallMessage toolCallMessage when (toolCallMessage.From is null || toolCallMessage.From == agent.Name) => ProcessToolCallMessage(toolCallMessage, agent), + ToolCallResultMessage toolCallResultMessage => ProcessToolCallResultMessage(toolCallResultMessage, agent), + AggregateMessage aggregateMessage => ProcessFunctionCallMiddlewareMessage(aggregateMessage, agent), // message type support for functioncall middleware + _ => [m], + }; + } + }); + } + + private IMessage PostProcessMessage(ChatCompletionResponse response, IAgent from) + { + if (response.Choices is null) + { + throw new ArgumentNullException("response.Choices"); + } + + if (response.Choices?.Count != 1) + { + throw new NotSupportedException("response.Choices.Count != 1"); + } + + var choice = response.Choices[0]; + var finishReason = choice.FinishReason ?? throw new ArgumentNullException("choice.FinishReason"); + + if (finishReason == Choice.FinishReasonEnum.Stop || finishReason == Choice.FinishReasonEnum.Length) + { + return new TextMessage(Role.Assistant, choice.Message?.Content ?? throw new ArgumentNullException("choice.Message.Content"), from: from.Name); + } + else if (finishReason == Choice.FinishReasonEnum.ToolCalls) + { + var functionContents = choice.Message?.ToolCalls ?? throw new ArgumentNullException("choice.Message.ToolCalls"); + var toolCalls = functionContents.Select(f => new ToolCall(f.Function.Name, f.Function.Arguments)).ToList(); + return new ToolCallMessage(toolCalls, from: from.Name); + } + else + { + throw new NotSupportedException($"FinishReason {finishReason} is not supported"); + } + } + + private IStreamingMessage? ProcessChatCompletionResponse(IStreamingMessage message, IAgent agent) + { + var response = message.Content; + if (response.VarObject != "chat.completion.chunk") + { + throw new NotSupportedException($"VarObject {response.VarObject} is not supported"); + } + if (response.Choices is null) + { + throw new ArgumentNullException("response.Choices"); + } + + if (response.Choices?.Count != 1) + { + throw new NotSupportedException("response.Choices.Count != 1"); + } + + var choice = response.Choices[0]; + var delta = choice.Delta; + + // process text message if delta.content is not null + if (delta?.Content is string content) + { + return new TextMessageUpdate(role: Role.Assistant, content, from: agent.Name); + } + else if (delta?.ToolCalls is var toolCalls && toolCalls is { Count: 1 }) + { + var toolCall = toolCalls[0]; + var functionContent = toolCall.Function; + + return new ToolCallMessageUpdate(functionContent.Name, functionContent.Arguments, from: agent.Name); + } + else + { + return null; + } + } + + private IEnumerable> ProcessTextMessage(TextMessage textMessage, IAgent agent) + { + IEnumerable messages; + // check if textMessage is system message + if (textMessage.Role == Role.System) + { + messages = [new ChatMessage(ChatMessage.RoleEnum.System, textMessage.Content)]; + } + else if (textMessage.From == agent.Name) + { + // if this message is from agent iteself, then its role should be assistant + messages = [new ChatMessage(ChatMessage.RoleEnum.Assistant, textMessage.Content)]; + } + else if (textMessage.From is null) + { + // if from is null, then process the message based on the role + if (textMessage.Role == Role.User) + { + messages = [new ChatMessage(ChatMessage.RoleEnum.User, textMessage.Content)]; + } + else if (textMessage.Role == Role.Assistant) + { + messages = [new ChatMessage(ChatMessage.RoleEnum.Assistant, textMessage.Content)]; + } + else + { + throw new NotSupportedException($"Role {textMessage.Role} is not supported"); + } + } + else + { + // if from is not null, then the message is from user + messages = [new ChatMessage(ChatMessage.RoleEnum.User, textMessage.Content)]; + } + + return messages.Select(m => new MessageEnvelope(m, from: textMessage.From)); + } + + private IEnumerable> ProcessToolCallResultMessage(ToolCallResultMessage toolCallResultMessage, IAgent agent) + { + var from = toolCallResultMessage.From; + var messages = new List(); + foreach (var toolCall in toolCallResultMessage.ToolCalls) + { + if (toolCall.Result is null) + { + continue; + } + + var message = new ChatMessage(ChatMessage.RoleEnum.Tool, content: toolCall.Result) + { + Name = toolCall.FunctionName, + }; + + messages.Add(message); + } + + return messages.Select(m => new MessageEnvelope(m, from: toolCallResultMessage.From)); + } + + /// + /// Process the aggregate message from function call middleware. If the message is from another agent, this message will be interpreted as an ordinary plain . + /// If the message is from the same agent or the from field is empty, this message will be expanded to the tool call message and tool call result message. + /// + /// + /// + /// + /// + private IEnumerable> ProcessFunctionCallMiddlewareMessage(AggregateMessage aggregateMessage, IAgent agent) + { + if (aggregateMessage.From is string from && from != agent.Name) + { + // if the message is from another agent, then interpret it as a plain text message + // where the content of the plain text message is the content of the tool call result message + var contents = aggregateMessage.Message2.ToolCalls.Select(t => t.Result); + var messages = contents.Select(c => new ChatMessage(ChatMessage.RoleEnum.Assistant, c)); + + return messages.Select(m => new MessageEnvelope(m, from: from)); + } + + // if the message is from the same agent or the from field is empty, then expand the message to tool call message and tool call result message + var toolCallMessage = aggregateMessage.Message1; + var toolCallResultMessage = aggregateMessage.Message2; + + return this.ProcessToolCallMessage(toolCallMessage, agent).Concat(this.ProcessToolCallResultMessage(toolCallResultMessage, agent)); + } + + private IEnumerable> ProcessToolCallMessage(ToolCallMessage toolCallMessage, IAgent agent) + { + IEnumerable messages; + + // the scenario is not support when tool call message is from another agent + if (toolCallMessage.From is string from && from != agent.Name) + { + throw new NotSupportedException("Tool call message from another agent is not supported"); + } + + // convert tool call message to chat message + var chatMessage = new ChatMessage(ChatMessage.RoleEnum.Assistant); + chatMessage.ToolCalls = new List(); + foreach (var toolCall in toolCallMessage.ToolCalls) + { + var functionCall = new FunctionContent.FunctionCall(toolCall.FunctionName, toolCall.FunctionArguments); + var functionContent = new FunctionContent(functionCall); + chatMessage.ToolCalls.Add(functionContent); + } + + messages = [chatMessage]; + + return messages.Select(m => new MessageEnvelope(m, from: toolCallMessage.From)); + } +} diff --git a/dotnet/src/AutoGen.Mistral/MistralAIModelID.cs b/dotnet/src/AutoGen.Mistral/MistralAIModelID.cs new file mode 100644 index 00000000000..a0571281c94 --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/MistralAIModelID.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MistralAIModelID.cs + +namespace AutoGen.Mistral; + +public class MistralAIModelID +{ + public const string OPEN_MISTRAL_7B = "open-mistral-7b"; + public const string OPEN_MISTRAL_8X7B = "open-mixtral-8x7b"; + public const string OPEN_MISTRAL_8X22B = "open-mixtral-8x22b"; + public const string MISTRAL_SMALL_LATEST = "mistral-small-latest"; + public const string MISTRAL_MEDIUM_LATEST = "mistral-medium-latest"; + public const string MISTRAL_LARGE_LATEST = "mistral-large-latest"; +} diff --git a/dotnet/src/AutoGen.Mistral/MistralClient.cs b/dotnet/src/AutoGen.Mistral/MistralClient.cs new file mode 100644 index 00000000000..5fc3d110985 --- /dev/null +++ b/dotnet/src/AutoGen.Mistral/MistralClient.cs @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MistralClient.cs + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Security.Authentication; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace AutoGen.Mistral; + +public class MistralClient : IDisposable +{ + private readonly HttpClient _httpClient; + private readonly string baseUrl = "https://api.mistral.ai/v1"; + + public MistralClient(string apiKey, string? baseUrl = null) + { + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); + _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); + this.baseUrl = baseUrl ?? this.baseUrl; + } + + public MistralClient(HttpClient httpClient, string? baseUrl = null) + { + _httpClient = httpClient; + _httpClient.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); + this.baseUrl = baseUrl ?? this.baseUrl; + } + + public async Task CreateChatCompletionsAsync(ChatCompletionRequest chatCompletionRequest) + { + chatCompletionRequest.Stream = false; + var response = await HttpRequestRaw(HttpMethod.Post, chatCompletionRequest); + response.EnsureSuccessStatusCode(); + + var responseStream = await response.Content.ReadAsStreamAsync(); + return await JsonSerializer.DeserializeAsync(responseStream) ?? throw new Exception("Failed to deserialize response"); + } + + public async IAsyncEnumerable StreamingChatCompletionsAsync(ChatCompletionRequest chatCompletionRequest) + { + chatCompletionRequest.Stream = true; + var response = await HttpRequestRaw(HttpMethod.Post, chatCompletionRequest, streaming: true); + using var stream = await response.Content.ReadAsStreamAsync(); + using StreamReader reader = new StreamReader(stream); + string line; + + SseEvent currentEvent = new SseEvent(); + while ((line = await reader.ReadLineAsync()) != null) + { + if (!string.IsNullOrEmpty(line)) + { + currentEvent.Data = line.Substring("data:".Length).Trim(); + } + else // an empty line indicates the end of an event + { + if (currentEvent.Data == "[DONE]") + { + continue; + } + else if (currentEvent.EventType == null) + { + var res = await JsonSerializer.DeserializeAsync( + new MemoryStream(Encoding.UTF8.GetBytes(currentEvent.Data))) ?? throw new Exception("Failed to deserialize response"); + yield return res; + } + else if (currentEvent.EventType != null) + { + var res = await JsonSerializer.DeserializeAsync( + new MemoryStream(Encoding.UTF8.GetBytes(currentEvent.Data))); + throw new Exception(res?.Error.Message); + } + + // Reset the current event for the next one + currentEvent = new SseEvent(); + } + } + } + + protected async Task HttpRequestRaw(HttpMethod verb, object postData, bool streaming = false) + { + var url = $"{baseUrl}/chat/completions"; + HttpResponseMessage response; + string resultAsString; + HttpRequestMessage req = new HttpRequestMessage(verb, url); + + if (postData != null) + { + if (postData is HttpContent) + { + req.Content = postData as HttpContent; + } + else + { + string jsonContent = JsonSerializer.Serialize(postData, + new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); + var stringContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + req.Content = stringContent; + } + } + + response = await this._httpClient.SendAsync(req, + streaming ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); + + if (response.IsSuccessStatusCode) + { + return response; + } + else + { + try + { + resultAsString = await response.Content.ReadAsStringAsync(); + } + catch (Exception e) + { + resultAsString = + "Additionally, the following error was thrown when attempting to read the response content: " + + e.ToString(); + } + + if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + throw new AuthenticationException( + "Mistral rejected your authorization, most likely due to an invalid API Key. Full API response follows: " + + resultAsString); + } + else if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) + { + throw new HttpRequestException( + "Mistral had an internal server error, which can happen occasionally. Please retry your request. " + + GetErrorMessage(resultAsString, response, url, url)); + } + else + { + throw new HttpRequestException(GetErrorMessage(resultAsString, response, url, url)); + } + } + } + + private string GetErrorMessage(string resultAsString, HttpResponseMessage response, string name, string description = "") + { + return $"Error at {name} ({description}) with HTTP status code: {response.StatusCode}. Content: {resultAsString ?? ""}"; + } + + public void Dispose() + { + _httpClient.Dispose(); + } + + public class SseEvent + { + public SseEvent(string? eventType = null, string? data = null) + { + EventType = eventType; + Data = data; + } + + public string? EventType { get; set; } + public string? Data { get; set; } + } +} diff --git a/dotnet/src/AutoGen.OpenAI/Agent/GPTAgent.cs b/dotnet/src/AutoGen.OpenAI/Agent/GPTAgent.cs new file mode 100644 index 00000000000..cb5a97c1310 --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI/Agent/GPTAgent.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GPTAgent.cs + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using AutoGen.OpenAI.Extension; +using Azure.AI.OpenAI; + +namespace AutoGen.OpenAI; + +/// +/// GPT agent that can be used to connect to OpenAI chat models like GPT-3.5, GPT-4, etc. +/// supports the following message types as input: +/// - +/// - +/// - +/// - +/// - +/// - +/// - where T is +/// - where TMessage1 is and TMessage2 is +/// +/// returns the following message types: +/// - +/// - +/// - where TMessage1 is and TMessage2 is +/// +public class GPTAgent : IStreamingAgent +{ + private readonly IDictionary>>? functionMap; + private readonly OpenAIClient openAIClient; + private readonly string? modelName; + private readonly OpenAIChatAgent _innerAgent; + + public GPTAgent( + string name, + string systemMessage, + ILLMConfig config, + float temperature = 0.7f, + int maxTokens = 1024, + int? seed = null, + ChatCompletionsResponseFormat? responseFormat = null, + IEnumerable? functions = null, + IDictionary>>? functionMap = null) + { + openAIClient = config switch + { + AzureOpenAIConfig azureConfig => new OpenAIClient(new Uri(azureConfig.Endpoint), new Azure.AzureKeyCredential(azureConfig.ApiKey)), + OpenAIConfig openAIConfig => new OpenAIClient(openAIConfig.ApiKey), + _ => throw new ArgumentException($"Unsupported config type {config.GetType()}"), + }; + + modelName = config switch + { + AzureOpenAIConfig azureConfig => azureConfig.DeploymentName, + OpenAIConfig openAIConfig => openAIConfig.ModelId, + _ => throw new ArgumentException($"Unsupported config type {config.GetType()}"), + }; + + _innerAgent = new OpenAIChatAgent(openAIClient, name, modelName, systemMessage, temperature, maxTokens, seed, responseFormat, functions); + Name = name; + this.functionMap = functionMap; + } + + public GPTAgent( + string name, + string systemMessage, + OpenAIClient openAIClient, + string modelName, + float temperature = 0.7f, + int maxTokens = 1024, + int? seed = null, + ChatCompletionsResponseFormat? responseFormat = null, + IEnumerable? functions = null, + IDictionary>>? functionMap = null) + { + this.openAIClient = openAIClient; + this.modelName = modelName; + Name = name; + this.functionMap = functionMap; + _innerAgent = new OpenAIChatAgent(openAIClient, name, modelName, systemMessage, temperature, maxTokens, seed, responseFormat, functions); + } + + public string Name { get; } + + public async Task GenerateReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + CancellationToken cancellationToken = default) + { + var agent = this._innerAgent + .RegisterMessageConnector(); + if (this.functionMap is not null) + { + var functionMapMiddleware = new FunctionCallMiddleware(functionMap: this.functionMap); + agent = agent.RegisterMiddleware(functionMapMiddleware); + } + + return await agent.GenerateReplyAsync(messages, options, cancellationToken); + } + + public async Task> GenerateStreamingReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + CancellationToken cancellationToken = default) + { + var agent = this._innerAgent + .RegisterMessageConnector(); + if (this.functionMap is not null) + { + var functionMapMiddleware = new FunctionCallMiddleware(functionMap: this.functionMap); + agent = agent.RegisterStreamingMiddleware(functionMapMiddleware); + } + + return await agent.GenerateStreamingReplyAsync(messages, options, cancellationToken); + } +} diff --git a/dotnet/src/AutoGen.OpenAI/Agent/OpenAIChatAgent.cs b/dotnet/src/AutoGen.OpenAI/Agent/OpenAIChatAgent.cs new file mode 100644 index 00000000000..ecebe7fc3fa --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI/Agent/OpenAIChatAgent.cs @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIChatAgent.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using AutoGen.OpenAI.Extension; +using Azure.AI.OpenAI; + +namespace AutoGen.OpenAI; + +/// +/// OpenAI client agent. This agent is a thin wrapper around to provide a simple interface for chat completions. +/// To better work with other agents, it's recommended to use which supports more message types and have a better compatibility with other agents. +/// supports the following message types: +/// +/// +/// where T is : chat request message. +/// +/// +/// returns the following message types: +/// +/// +/// where T is : chat response message. +/// where T is : streaming chat completions update. +/// +/// +/// +public class OpenAIChatAgent : IStreamingAgent +{ + private readonly OpenAIClient openAIClient; + private readonly string modelName; + private readonly float _temperature; + private readonly int _maxTokens = 1024; + private readonly IEnumerable? _functions; + private readonly string _systemMessage; + private readonly ChatCompletionsResponseFormat? _responseFormat; + private readonly int? _seed; + + /// + /// Create a new instance of . + /// + /// openai client + /// agent name + /// model name. e.g. gpt-turbo-3.5 + /// system message + /// temperature + /// max tokens to generated + /// response format, set it to to enable json mode. + /// seed to use, set it to enable deterministic output + /// functions + public OpenAIChatAgent( + OpenAIClient openAIClient, + string name, + string modelName, + string systemMessage = "You are a helpful AI assistant", + float temperature = 0.7f, + int maxTokens = 1024, + int? seed = null, + ChatCompletionsResponseFormat? responseFormat = null, + IEnumerable? functions = null) + { + this.openAIClient = openAIClient; + this.modelName = modelName; + this.Name = name; + _temperature = temperature; + _maxTokens = maxTokens; + _functions = functions; + _systemMessage = systemMessage; + _responseFormat = responseFormat; + _seed = seed; + } + + public string Name { get; } + + public async Task GenerateReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + CancellationToken cancellationToken = default) + { + var settings = this.CreateChatCompletionsOptions(options, messages); + var reply = await this.openAIClient.GetChatCompletionsAsync(settings, cancellationToken); + + return new MessageEnvelope(reply.Value.Choices.First().Message, from: this.Name); + } + + public Task> GenerateStreamingReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + CancellationToken cancellationToken = default) + { + return Task.FromResult(this.StreamingReplyAsync(messages, options, cancellationToken)); + } + + private async IAsyncEnumerable StreamingReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var settings = this.CreateChatCompletionsOptions(options, messages); + var response = await this.openAIClient.GetChatCompletionsStreamingAsync(settings); + await foreach (var update in response.WithCancellation(cancellationToken)) + { + if (update.ChoiceIndex > 0) + { + throw new InvalidOperationException("Only one choice is supported in streaming response"); + } + + yield return new MessageEnvelope(update, from: this.Name); + } + } + + private ChatCompletionsOptions CreateChatCompletionsOptions(GenerateReplyOptions? options, IEnumerable messages) + { + var oaiMessages = messages.Select(m => m switch + { + IMessage chatRequestMessage => chatRequestMessage.Content, + _ => throw new ArgumentException("Invalid message type") + }); + + // add system message if there's no system message in messages + if (!oaiMessages.Any(m => m is ChatRequestSystemMessage)) + { + oaiMessages = new[] { new ChatRequestSystemMessage(_systemMessage) }.Concat(oaiMessages); + } + + var settings = new ChatCompletionsOptions(this.modelName, oaiMessages) + { + MaxTokens = options?.MaxToken ?? _maxTokens, + Temperature = options?.Temperature ?? _temperature, + ResponseFormat = _responseFormat, + Seed = _seed, + }; + + var openAIFunctionDefinitions = options?.Functions?.Select(f => f.ToOpenAIFunctionDefinition()); + var functions = openAIFunctionDefinitions ?? _functions; + if (functions is not null && functions.Count() > 0) + { + foreach (var f in functions) + { + settings.Tools.Add(new ChatCompletionsFunctionToolDefinition(f)); + } + } + + if (options?.StopSequence is var sequence && sequence is { Length: > 0 }) + { + foreach (var seq in sequence) + { + settings.StopSequences.Add(seq); + } + } + + return settings; + } +} diff --git a/dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj b/dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj new file mode 100644 index 00000000000..182d112227b --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj @@ -0,0 +1,25 @@ + + + netstandard2.0 + AutoGen.OpenAI + + + + + + + AutoGen.OpenAI + + OpenAI Intergration for AutoGen. + + + + + + + + + + + + diff --git a/dotnet/src/AutoGen.OpenAI/AzureOpenAIConfig.cs b/dotnet/src/AutoGen.OpenAI/AzureOpenAIConfig.cs new file mode 100644 index 00000000000..31df784ed21 --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI/AzureOpenAIConfig.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AzureOpenAIConfig.cs + +namespace AutoGen.OpenAI; + +public class AzureOpenAIConfig : ILLMConfig +{ + public AzureOpenAIConfig(string endpoint, string deploymentName, string apiKey, string? modelId = null) + { + this.Endpoint = endpoint; + this.DeploymentName = deploymentName; + this.ApiKey = apiKey; + this.ModelId = modelId; + } + + public string Endpoint { get; } + + public string DeploymentName { get; } + + public string ApiKey { get; } + + public string? ModelId { get; } +} diff --git a/dotnet/src/AutoGen.OpenAI/Extension/FunctionContractExtension.cs b/dotnet/src/AutoGen.OpenAI/Extension/FunctionContractExtension.cs new file mode 100644 index 00000000000..4accdc4d8d4 --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI/Extension/FunctionContractExtension.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// FunctionContractExtension.cs + +using System; +using System.Collections.Generic; +using Azure.AI.OpenAI; +using Json.Schema; +using Json.Schema.Generation; + +namespace AutoGen.OpenAI.Extension; + +public static class FunctionContractExtension +{ + /// + /// Convert a to a that can be used in gpt funciton call. + /// + /// function contract + /// + public static FunctionDefinition ToOpenAIFunctionDefinition(this FunctionContract functionContract) + { + var functionDefinition = new FunctionDefinition + { + Name = functionContract.Name, + Description = functionContract.Description, + }; + var requiredParameterNames = new List(); + var propertiesSchemas = new Dictionary(); + var propertySchemaBuilder = new JsonSchemaBuilder().Type(SchemaValueType.Object); + foreach (var param in functionContract.Parameters ?? []) + { + if (param.Name is null) + { + throw new InvalidOperationException("Parameter name cannot be null"); + } + + var schemaBuilder = new JsonSchemaBuilder().FromType(param.ParameterType ?? throw new ArgumentNullException(nameof(param.ParameterType))); + if (param.Description != null) + { + schemaBuilder = schemaBuilder.Description(param.Description); + } + + if (param.IsRequired) + { + requiredParameterNames.Add(param.Name); + } + + var schema = schemaBuilder.Build(); + propertiesSchemas[param.Name] = schema; + + } + propertySchemaBuilder = propertySchemaBuilder.Properties(propertiesSchemas); + propertySchemaBuilder = propertySchemaBuilder.Required(requiredParameterNames); + + var option = new System.Text.Json.JsonSerializerOptions() + { + PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase + }; + + functionDefinition.Parameters = BinaryData.FromObjectAsJson(propertySchemaBuilder.Build(), option); + + return functionDefinition; + } +} diff --git a/dotnet/src/AutoGen.OpenAI/Extension/MessageExtension.cs b/dotnet/src/AutoGen.OpenAI/Extension/MessageExtension.cs new file mode 100644 index 00000000000..92e0f3776f5 --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI/Extension/MessageExtension.cs @@ -0,0 +1,228 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MessageExtension.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using Azure.AI.OpenAI; + +namespace AutoGen.OpenAI; + +public static class MessageExtension +{ + public static string TEXT_CONTENT_TYPE = "text"; + public static string IMAGE_CONTENT_TYPE = "image"; + public static ChatRequestUserMessage ToChatRequestUserMessage(this Message message) + { + if (message.Value is ChatRequestUserMessage message1) + { + return message1; + } + else if (message?.Metadata is { Count: > 0 }) + { + var itemList = new List(); + foreach (var item in message.Metadata) + { + if (item.Key == TEXT_CONTENT_TYPE && item.Value is string txt) + { + itemList.Add(new ChatMessageTextContentItem(txt)); + } + else if (item.Key == IMAGE_CONTENT_TYPE && item.Value is string url) + { + itemList.Add(new ChatMessageImageContentItem(new Uri(url))); + } + } + + if (itemList.Count > 0) + { + return new ChatRequestUserMessage(itemList); + } + else + { + throw new ArgumentException("Content is null and metadata is null"); + } + } + else if (!string.IsNullOrEmpty(message?.Content)) + { + return new ChatRequestUserMessage(message!.Content); + } + + throw new ArgumentException("Content is null and metadata is null"); + } + + public static IEnumerable ToOpenAIChatRequestMessage(this IAgent agent, IMessage message) + { + if (message is IMessage oaiMessage) + { + // short-circuit + return [oaiMessage.Content]; + } + + if (message.From != agent.Name) + { + if (message is TextMessage textMessage) + { + if (textMessage.Role == Role.System) + { + var msg = new ChatRequestSystemMessage(textMessage.Content); + + return [msg]; + } + else + { + var msg = new ChatRequestUserMessage(textMessage.Content); + return [msg]; + } + } + else if (message is ImageMessage imageMessage) + { + // multi-modal + var msg = new ChatRequestUserMessage(new ChatMessageImageContentItem(new Uri(imageMessage.Url))); + + return [msg]; + } + else if (message is ToolCallMessage) + { + throw new ArgumentException($"ToolCallMessage is not supported when message.From is not the same with agent"); + } + else if (message is ToolCallResultMessage toolCallResult) + { + return toolCallResult.ToolCalls.Select(m => + { + var msg = new ChatRequestToolMessage(m.Result, m.FunctionName); + + return msg; + }); + } + else if (message is MultiModalMessage multiModalMessage) + { + var messageContent = multiModalMessage.Content.Select(m => + { + return m switch + { + TextMessage textMessage => new ChatMessageTextContentItem(textMessage.Content), + ImageMessage imageMessage => new ChatMessageImageContentItem(new Uri(imageMessage.Url)), + _ => throw new ArgumentException($"Unknown message type: {m.GetType()}") + }; + }); + + var msg = new ChatRequestUserMessage(messageContent); + return [msg]; + } + else if (message is AggregateMessage aggregateMessage) + { + // convert as user message + var resultMessage = aggregateMessage.Message2; + return resultMessage.ToolCalls.Select(m => new ChatRequestUserMessage(m.Result)); + } + else if (message is Message msg) + { + if (msg.Role == Role.System) + { + var systemMessage = new ChatRequestSystemMessage(msg.Content ?? string.Empty); + return [systemMessage]; + } + else if (msg.FunctionName is null && msg.FunctionArguments is null) + { + var userMessage = msg.ToChatRequestUserMessage(); + return [userMessage]; + } + else if (msg.FunctionName is not null && msg.FunctionArguments is not null && msg.Content is not null) + { + if (msg.Role == Role.Function) + { + return [new ChatRequestFunctionMessage(msg.FunctionName, msg.Content)]; + } + else + { + return [new ChatRequestUserMessage(msg.Content)]; + } + } + else + { + var userMessage = new ChatRequestUserMessage(msg.Content ?? throw new ArgumentException("Content is null")); + return [userMessage]; + } + } + else + { + throw new ArgumentException($"Unknown message type: {message.GetType()}"); + } + } + else + { + if (message is TextMessage textMessage) + { + if (textMessage.Role == Role.System) + { + throw new ArgumentException("System message is not supported when message.From is the same with agent"); + } + + + return [new ChatRequestAssistantMessage(textMessage.Content)]; + } + else if (message is ToolCallMessage toolCallMessage) + { + var assistantMessage = new ChatRequestAssistantMessage(string.Empty); + var toolCalls = toolCallMessage.ToolCalls.Select(tc => new ChatCompletionsFunctionToolCall(tc.FunctionName, tc.FunctionName, tc.FunctionArguments)); + foreach (var tc in toolCalls) + { + assistantMessage.ToolCalls.Add(tc); + } + + return [assistantMessage]; + } + else if (message is AggregateMessage aggregateMessage) + { + var toolCallMessage1 = aggregateMessage.Message1; + var toolCallResultMessage = aggregateMessage.Message2; + + var assistantMessage = new ChatRequestAssistantMessage(string.Empty); + var toolCalls = toolCallMessage1.ToolCalls.Select(tc => new ChatCompletionsFunctionToolCall(tc.FunctionName, tc.FunctionName, tc.FunctionArguments)); + foreach (var tc in toolCalls) + { + assistantMessage.ToolCalls.Add(tc); + } + + var toolCallResults = toolCallResultMessage.ToolCalls.Select(tc => new ChatRequestToolMessage(tc.Result, tc.FunctionName)); + + // return assistantMessage and tool call result messages + var messages = new List { assistantMessage }; + messages.AddRange(toolCallResults); + + return messages; + } + else if (message is Message msg) + { + if (msg.FunctionArguments is not null && msg.FunctionName is not null && msg.Content is not null) + { + var assistantMessage = new ChatRequestAssistantMessage(msg.Content); + assistantMessage.FunctionCall = new FunctionCall(msg.FunctionName, msg.FunctionArguments); + var functionCallMessage = new ChatRequestFunctionMessage(msg.FunctionName, msg.Content); + return [assistantMessage, functionCallMessage]; + } + else + { + if (msg.Role == Role.Function) + { + return [new ChatRequestFunctionMessage(msg.FunctionName!, msg.Content!)]; + } + else + { + var assistantMessage = new ChatRequestAssistantMessage(msg.Content!); + if (msg.FunctionName is not null && msg.FunctionArguments is not null) + { + assistantMessage.FunctionCall = new FunctionCall(msg.FunctionName, msg.FunctionArguments); + } + + return [assistantMessage]; + } + } + } + else + { + throw new ArgumentException($"Unknown message type: {message.GetType()}"); + } + } + } +} diff --git a/dotnet/src/AutoGen.OpenAI/Extension/OpenAIAgentExtension.cs b/dotnet/src/AutoGen.OpenAI/Extension/OpenAIAgentExtension.cs new file mode 100644 index 00000000000..1e8ae58954e --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI/Extension/OpenAIAgentExtension.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIAgentExtension.cs + +namespace AutoGen.OpenAI.Extension; + +public static class OpenAIAgentExtension +{ + /// + /// Register an to the + /// + /// the connector to use. If null, a new instance of will be created. + public static MiddlewareStreamingAgent RegisterMessageConnector( + this OpenAIChatAgent agent, OpenAIChatRequestMessageConnector? connector = null) + { + if (connector == null) + { + connector = new OpenAIChatRequestMessageConnector(); + } + + return agent.RegisterStreamingMiddleware(connector); + } + + /// + /// Register an to the where T is + /// + /// the connector to use. If null, a new instance of will be created. + public static MiddlewareStreamingAgent RegisterMessageConnector( + this MiddlewareStreamingAgent agent, OpenAIChatRequestMessageConnector? connector = null) + { + if (connector == null) + { + connector = new OpenAIChatRequestMessageConnector(); + } + + return agent.RegisterStreamingMiddleware(connector); + } +} diff --git a/dotnet/src/AutoGen.OpenAI/GlobalUsing.cs b/dotnet/src/AutoGen.OpenAI/GlobalUsing.cs new file mode 100644 index 00000000000..d66bf001ed5 --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI/GlobalUsing.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GlobalUsing.cs + +global using AutoGen.Core; diff --git a/dotnet/src/AutoGen.OpenAI/Middleware/OpenAIChatRequestMessageConnector.cs b/dotnet/src/AutoGen.OpenAI/Middleware/OpenAIChatRequestMessageConnector.cs new file mode 100644 index 00000000000..c1581cbec08 --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI/Middleware/OpenAIChatRequestMessageConnector.cs @@ -0,0 +1,445 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIChatRequestMessageConnector.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.OpenAI; + +namespace AutoGen.OpenAI; + +/// +/// This middleware converts the incoming to where T is before sending to agent. And converts the output to after receiving from agent. +/// Supported are +/// - +/// - +/// - +/// - +/// - +/// - +/// - where T is +/// - where TMessage1 is and TMessage2 is +/// +public class OpenAIChatRequestMessageConnector : IMiddleware, IStreamingMiddleware +{ + private bool strictMode = false; + + public OpenAIChatRequestMessageConnector(bool strictMode = false) + { + this.strictMode = strictMode; + } + + public string? Name => nameof(OpenAIChatRequestMessageConnector); + + public async Task InvokeAsync(MiddlewareContext context, IAgent agent, CancellationToken cancellationToken = default) + { + var chatMessages = ProcessIncomingMessages(agent, context.Messages) + .Select(m => new MessageEnvelope(m)); + + var reply = await agent.GenerateReplyAsync(chatMessages, context.Options, cancellationToken); + + return PostProcessMessage(reply); + } + + public async Task> InvokeAsync( + MiddlewareContext context, + IStreamingAgent agent, + CancellationToken cancellationToken = default) + { + return InvokeStreamingAsync(context, agent, cancellationToken); + } + + private async IAsyncEnumerable InvokeStreamingAsync( + MiddlewareContext context, + IStreamingAgent agent, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var chatMessages = ProcessIncomingMessages(agent, context.Messages) + .Select(m => new MessageEnvelope(m)); + var streamingReply = await agent.GenerateStreamingReplyAsync(chatMessages, context.Options, cancellationToken); + string? currentToolName = null; + await foreach (var reply in streamingReply) + { + if (reply is IStreamingMessage update) + { + if (update.Content.FunctionName is string functionName) + { + currentToolName = functionName; + } + else if (update.Content.ToolCallUpdate is StreamingFunctionToolCallUpdate toolCallUpdate && toolCallUpdate.Name is string toolCallName) + { + currentToolName = toolCallName; + } + var postProcessMessage = PostProcessStreamingMessage(update, currentToolName); + if (postProcessMessage != null) + { + yield return postProcessMessage; + } + } + else + { + yield return reply; + } + } + } + + public IMessage PostProcessMessage(IMessage message) + { + return message switch + { + TextMessage => message, + ImageMessage => message, + MultiModalMessage => message, + ToolCallMessage => message, + ToolCallResultMessage => message, + Message => message, + AggregateMessage => message, + IMessage m => PostProcessMessage(m), + _ => throw new InvalidOperationException("The type of message is not supported. Must be one of TextMessage, ImageMessage, MultiModalMessage, ToolCallMessage, ToolCallResultMessage, Message, IMessage, AggregateMessage"), + }; + } + + public IStreamingMessage? PostProcessStreamingMessage(IStreamingMessage update, string? currentToolName) + { + if (update.Content.ContentUpdate is string contentUpdate) + { + // text message + return new TextMessageUpdate(Role.Assistant, contentUpdate, from: update.From); + } + else if (update.Content.FunctionName is string functionName) + { + return new ToolCallMessageUpdate(functionName, string.Empty, from: update.From); + } + else if (update.Content.FunctionArgumentsUpdate is string functionArgumentsUpdate && currentToolName is string) + { + return new ToolCallMessageUpdate(currentToolName, functionArgumentsUpdate, from: update.From); + } + else if (update.Content.ToolCallUpdate is StreamingFunctionToolCallUpdate tooCallUpdate && currentToolName is string) + { + return new ToolCallMessageUpdate(tooCallUpdate.Name ?? currentToolName, tooCallUpdate.ArgumentsUpdate, from: update.From); + } + else + { + return null; + } + } + + private IMessage PostProcessMessage(IMessage message) + { + var chatResponseMessage = message.Content; + if (chatResponseMessage.Content is string content) + { + return new TextMessage(Role.Assistant, content, message.From); + } + + if (chatResponseMessage.FunctionCall is FunctionCall functionCall) + { + return new ToolCallMessage(functionCall.Name, functionCall.Arguments, message.From); + } + + if (chatResponseMessage.ToolCalls.Where(tc => tc is ChatCompletionsFunctionToolCall).Any()) + { + var functionToolCalls = chatResponseMessage.ToolCalls + .Where(tc => tc is ChatCompletionsFunctionToolCall) + .Select(tc => (ChatCompletionsFunctionToolCall)tc); + + var toolCalls = functionToolCalls.Select(tc => new ToolCall(tc.Name, tc.Arguments)); + + return new ToolCallMessage(toolCalls, message.From); + } + + throw new InvalidOperationException("Invalid ChatResponseMessage"); + } + + public IEnumerable ProcessIncomingMessages(IAgent agent, IEnumerable messages) + { + return messages.SelectMany(m => + { + if (m.From == null) + { + return ProcessIncomingMessagesWithEmptyFrom(m); + } + else if (m.From == agent.Name) + { + return ProcessIncomingMessagesForSelf(m); + } + else + { + return ProcessIncomingMessagesForOther(m); + } + }); + } + + private IEnumerable ProcessIncomingMessagesForSelf(IMessage message) + { + return message switch + { + TextMessage textMessage => ProcessIncomingMessagesForSelf(textMessage), + ImageMessage imageMessage => ProcessIncomingMessagesForSelf(imageMessage), + MultiModalMessage multiModalMessage => ProcessIncomingMessagesForSelf(multiModalMessage), + ToolCallMessage toolCallMessage => ProcessIncomingMessagesForSelf(toolCallMessage), + ToolCallResultMessage toolCallResultMessage => ProcessIncomingMessagesForSelf(toolCallResultMessage), + Message msg => ProcessIncomingMessagesForSelf(msg), + IMessage crm => ProcessIncomingMessagesForSelf(crm), + AggregateMessage aggregateMessage => ProcessIncomingMessagesForSelf(aggregateMessage), + _ => throw new NotImplementedException(), + }; + } + + private IEnumerable ProcessIncomingMessagesWithEmptyFrom(IMessage message) + { + return message switch + { + TextMessage textMessage => ProcessIncomingMessagesWithEmptyFrom(textMessage), + ImageMessage imageMessage => ProcessIncomingMessagesWithEmptyFrom(imageMessage), + MultiModalMessage multiModalMessage => ProcessIncomingMessagesWithEmptyFrom(multiModalMessage), + ToolCallMessage toolCallMessage => ProcessIncomingMessagesWithEmptyFrom(toolCallMessage), + ToolCallResultMessage toolCallResultMessage => ProcessIncomingMessagesWithEmptyFrom(toolCallResultMessage), + Message msg => ProcessIncomingMessagesWithEmptyFrom(msg), + IMessage crm => ProcessIncomingMessagesWithEmptyFrom(crm), + AggregateMessage aggregateMessage => ProcessIncomingMessagesWithEmptyFrom(aggregateMessage), + _ => throw new NotImplementedException(), + }; + } + + private IEnumerable ProcessIncomingMessagesForOther(IMessage message) + { + return message switch + { + TextMessage textMessage => ProcessIncomingMessagesForOther(textMessage), + ImageMessage imageMessage => ProcessIncomingMessagesForOther(imageMessage), + MultiModalMessage multiModalMessage => ProcessIncomingMessagesForOther(multiModalMessage), + ToolCallMessage toolCallMessage => ProcessIncomingMessagesForOther(toolCallMessage), + ToolCallResultMessage toolCallResultMessage => ProcessIncomingMessagesForOther(toolCallResultMessage), + Message msg => ProcessIncomingMessagesForOther(msg), + IMessage crm => ProcessIncomingMessagesForOther(crm), + AggregateMessage aggregateMessage => ProcessIncomingMessagesForOther(aggregateMessage), + _ => throw new NotImplementedException(), + }; + } + + private IEnumerable ProcessIncomingMessagesForSelf(TextMessage message) + { + if (message.Role == Role.System) + { + return new[] { new ChatRequestSystemMessage(message.Content) }; + } + else + { + return new[] { new ChatRequestAssistantMessage(message.Content) }; + } + } + + private IEnumerable ProcessIncomingMessagesForSelf(ImageMessage _) + { + return [new ChatRequestAssistantMessage("// Image Message is not supported")]; + } + + private IEnumerable ProcessIncomingMessagesForSelf(MultiModalMessage _) + { + return [new ChatRequestAssistantMessage("// MultiModal Message is not supported")]; + } + + private IEnumerable ProcessIncomingMessagesForSelf(ToolCallMessage message) + { + var toolCall = message.ToolCalls.Select(tc => new ChatCompletionsFunctionToolCall(tc.FunctionName, tc.FunctionName, tc.FunctionArguments)); + var chatRequestMessage = new ChatRequestAssistantMessage(string.Empty); + foreach (var tc in toolCall) + { + chatRequestMessage.ToolCalls.Add(tc); + } + + return new[] { chatRequestMessage }; + } + + private IEnumerable ProcessIncomingMessagesForSelf(ToolCallResultMessage message) + { + return message.ToolCalls.Select(tc => new ChatRequestToolMessage(tc.Result, tc.FunctionName)); + } + + private IEnumerable ProcessIncomingMessagesForSelf(Message message) + { + if (message.Role == Role.System) + { + return new[] { new ChatRequestSystemMessage(message.Content) }; + } + else if (message.Content is string content && content is { Length: > 0 }) + { + if (message.FunctionName is null) + { + return new[] { new ChatRequestAssistantMessage(message.Content) }; + } + else + { + return new[] { new ChatRequestToolMessage(content, message.FunctionName) }; + } + } + else if (message.FunctionName is string functionName) + { + var msg = new ChatRequestAssistantMessage(content: null) + { + FunctionCall = new FunctionCall(functionName, message.FunctionArguments) + }; + + return new[] + { + msg, + }; + } + else + { + throw new InvalidOperationException("Invalid Message as message from self."); + } + } + + private IEnumerable ProcessIncomingMessagesForSelf(IMessage message) + { + return new[] { message.Content }; + } + + private IEnumerable ProcessIncomingMessagesForSelf(AggregateMessage aggregateMessage) + { + var toolCallMessage1 = aggregateMessage.Message1; + var toolCallResultMessage = aggregateMessage.Message2; + + var assistantMessage = new ChatRequestAssistantMessage(string.Empty); + var toolCalls = toolCallMessage1.ToolCalls.Select(tc => new ChatCompletionsFunctionToolCall(tc.FunctionName, tc.FunctionName, tc.FunctionArguments)); + foreach (var tc in toolCalls) + { + assistantMessage.ToolCalls.Add(tc); + } + + var toolCallResults = toolCallResultMessage.ToolCalls.Select(tc => new ChatRequestToolMessage(tc.Result, tc.FunctionName)); + + // return assistantMessage and tool call result messages + var messages = new List { assistantMessage }; + messages.AddRange(toolCallResults); + + return messages; + } + + private IEnumerable ProcessIncomingMessagesForOther(TextMessage message) + { + if (message.Role == Role.System) + { + return new[] { new ChatRequestSystemMessage(message.Content) }; + } + else + { + return new[] { new ChatRequestUserMessage(message.Content) }; + } + } + + private IEnumerable ProcessIncomingMessagesForOther(ImageMessage message) + { + return new[] { new ChatRequestUserMessage([ + new ChatMessageImageContentItem(new Uri(message.Url)), + ])}; + } + + private IEnumerable ProcessIncomingMessagesForOther(MultiModalMessage message) + { + IEnumerable items = message.Content.Select(ci => ci switch + { + TextMessage text => new ChatMessageTextContentItem(text.Content), + ImageMessage image => new ChatMessageImageContentItem(new Uri(image.Url)), + _ => throw new NotImplementedException(), + }); + + return new[] { new ChatRequestUserMessage(items) }; + } + + private IEnumerable ProcessIncomingMessagesForOther(ToolCallMessage msg) + { + throw new ArgumentException("ToolCallMessage is not supported when message.From is not the same with agent"); + } + + private IEnumerable ProcessIncomingMessagesForOther(ToolCallResultMessage message) + { + return message.ToolCalls.Select(tc => new ChatRequestToolMessage(tc.Result, tc.FunctionName)); + } + + private IEnumerable ProcessIncomingMessagesForOther(Message message) + { + if (message.Role == Role.System) + { + return new[] { new ChatRequestSystemMessage(message.Content) }; + } + else if (message.Content is string content && content is { Length: > 0 }) + { + if (message.FunctionName is not null) + { + return new[] { new ChatRequestToolMessage(content, message.FunctionName) }; + } + + return new[] { new ChatRequestUserMessage(message.Content) }; + } + else if (message.FunctionName is string _) + { + return new[] + { + new ChatRequestUserMessage("// Message type is not supported"), + }; + } + else + { + throw new InvalidOperationException("Invalid Message as message from other."); + } + } + + private IEnumerable ProcessIncomingMessagesForOther(IMessage message) + { + return new[] { message.Content }; + } + + private IEnumerable ProcessIncomingMessagesForOther(AggregateMessage aggregateMessage) + { + // convert as user message + var resultMessage = aggregateMessage.Message2; + + return resultMessage.ToolCalls.Select(tc => new ChatRequestUserMessage(tc.Result)); + } + + private IEnumerable ProcessIncomingMessagesWithEmptyFrom(TextMessage message) + { + return ProcessIncomingMessagesForOther(message); + } + + private IEnumerable ProcessIncomingMessagesWithEmptyFrom(ImageMessage message) + { + return ProcessIncomingMessagesForOther(message); + } + + private IEnumerable ProcessIncomingMessagesWithEmptyFrom(MultiModalMessage message) + { + return ProcessIncomingMessagesForOther(message); + } + + private IEnumerable ProcessIncomingMessagesWithEmptyFrom(ToolCallMessage message) + { + return ProcessIncomingMessagesForSelf(message); + } + + private IEnumerable ProcessIncomingMessagesWithEmptyFrom(ToolCallResultMessage message) + { + return ProcessIncomingMessagesForOther(message); + } + + private IEnumerable ProcessIncomingMessagesWithEmptyFrom(Message message) + { + return ProcessIncomingMessagesForOther(message); + } + + private IEnumerable ProcessIncomingMessagesWithEmptyFrom(IMessage message) + { + return new[] { message.Content }; + } + + private IEnumerable ProcessIncomingMessagesWithEmptyFrom(AggregateMessage aggregateMessage) + { + return ProcessIncomingMessagesForOther(aggregateMessage); + } +} diff --git a/dotnet/src/AutoGen.OpenAI/OpenAIConfig.cs b/dotnet/src/AutoGen.OpenAI/OpenAIConfig.cs new file mode 100644 index 00000000000..35ce1e491aa --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI/OpenAIConfig.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIConfig.cs + +namespace AutoGen.OpenAI; + +public class OpenAIConfig : ILLMConfig +{ + public OpenAIConfig(string apiKey, string modelId) + { + this.ApiKey = apiKey; + this.ModelId = modelId; + } + + public string ApiKey { get; } + + public string ModelId { get; } +} diff --git a/dotnet/src/AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj b/dotnet/src/AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj new file mode 100644 index 00000000000..70d75006701 --- /dev/null +++ b/dotnet/src/AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj @@ -0,0 +1,27 @@ + + + + netstandard2.0 + AutoGen.SemanticKernel + + + + + + + AutoGen.SemanticKernel + + This package contains the semantic kernel integration for AutoGen + + + + + + + + + + + + + diff --git a/dotnet/src/AutoGen.SemanticKernel/Extension/KernelExtension.cs b/dotnet/src/AutoGen.SemanticKernel/Extension/KernelExtension.cs new file mode 100644 index 00000000000..f1589ab09e6 --- /dev/null +++ b/dotnet/src/AutoGen.SemanticKernel/Extension/KernelExtension.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// KernelExtension.cs + +using Microsoft.SemanticKernel; + +namespace AutoGen.SemanticKernel.Extension; + +public static class KernelExtension +{ + public static SemanticKernelAgent ToSemanticKernelAgent(this Kernel kernel, string name, string systemMessage = "You are a helpful AI assistant", PromptExecutionSettings? settings = null) + { + return new SemanticKernelAgent(kernel, name, systemMessage, settings); + } +} diff --git a/dotnet/src/AutoGen.SemanticKernel/Extension/SemanticKernelAgentExtension.cs b/dotnet/src/AutoGen.SemanticKernel/Extension/SemanticKernelAgentExtension.cs new file mode 100644 index 00000000000..4d450945dab --- /dev/null +++ b/dotnet/src/AutoGen.SemanticKernel/Extension/SemanticKernelAgentExtension.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SemanticKernelAgentExtension.cs + +namespace AutoGen.SemanticKernel.Extension; + +public static class SemanticKernelAgentExtension +{ + /// + /// Register an to the + /// + /// the connector to use. If null, a new instance of will be created. + public static MiddlewareStreamingAgent RegisterMessageConnector( + this SemanticKernelAgent agent, SemanticKernelChatMessageContentConnector? connector = null) + { + if (connector == null) + { + connector = new SemanticKernelChatMessageContentConnector(); + } + + return agent.RegisterStreamingMiddleware(connector); + } + + /// + /// Register an to the where T is + /// + /// the connector to use. If null, a new instance of will be created. + public static MiddlewareStreamingAgent RegisterMessageConnector( + this MiddlewareStreamingAgent agent, SemanticKernelChatMessageContentConnector? connector = null) + { + if (connector == null) + { + connector = new SemanticKernelChatMessageContentConnector(); + } + + return agent.RegisterStreamingMiddleware(connector); + } +} diff --git a/dotnet/src/AutoGen.SemanticKernel/GlobalUsing.cs b/dotnet/src/AutoGen.SemanticKernel/GlobalUsing.cs new file mode 100644 index 00000000000..d66bf001ed5 --- /dev/null +++ b/dotnet/src/AutoGen.SemanticKernel/GlobalUsing.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GlobalUsing.cs + +global using AutoGen.Core; diff --git a/dotnet/src/AutoGen.SemanticKernel/Middleware/SemanticKernelChatMessageContentConnector.cs b/dotnet/src/AutoGen.SemanticKernel/Middleware/SemanticKernelChatMessageContentConnector.cs new file mode 100644 index 00000000000..e4b7527cd05 --- /dev/null +++ b/dotnet/src/AutoGen.SemanticKernel/Middleware/SemanticKernelChatMessageContentConnector.cs @@ -0,0 +1,260 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SemanticKernelChatMessageContentConnector.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace AutoGen.SemanticKernel; + +/// +/// This middleware converts the incoming to before passing to agent. +/// And converts the reply message from to before returning to the caller. +/// +/// requirement for agent +/// - Input message type: where T is +/// - Reply message type: where T is +/// - (streaming) Reply message type: where T is +/// +/// This middleware supports the following message types: +/// - +/// - +/// - +/// +/// This middleware returns the following message types: +/// - +/// - +/// - +/// - (streaming) +/// +public class SemanticKernelChatMessageContentConnector : IMiddleware, IStreamingMiddleware +{ + public string? Name => nameof(SemanticKernelChatMessageContentConnector); + + public async Task InvokeAsync(MiddlewareContext context, IAgent agent, CancellationToken cancellationToken = default) + { + var messages = context.Messages; + + var chatMessageContents = ProcessMessage(messages, agent) + .Select(m => new MessageEnvelope(m)); + var reply = await agent.GenerateReplyAsync(chatMessageContents, context.Options, cancellationToken); + + return PostProcessMessage(reply); + } + + public Task> InvokeAsync(MiddlewareContext context, IStreamingAgent agent, CancellationToken cancellationToken = default) + { + return Task.FromResult(InvokeStreamingAsync(context, agent, cancellationToken)); + } + + private async IAsyncEnumerable InvokeStreamingAsync( + MiddlewareContext context, + IStreamingAgent agent, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var chatMessageContents = ProcessMessage(context.Messages, agent) + .Select(m => new MessageEnvelope(m)); + + await foreach (var reply in await agent.GenerateStreamingReplyAsync(chatMessageContents, context.Options, cancellationToken)) + { + yield return PostProcessStreamingMessage(reply); + } + } + + private IMessage PostProcessMessage(IMessage input) + { + return input switch + { + IMessage messageEnvelope => PostProcessMessage(messageEnvelope), + _ => input, + }; + } + + private IStreamingMessage PostProcessStreamingMessage(IStreamingMessage input) + { + return input switch + { + IStreamingMessage streamingMessage => PostProcessMessage(streamingMessage), + IMessage msg => PostProcessMessage(msg), + _ => input, + }; + } + + private IMessage PostProcessMessage(IMessage messageEnvelope) + { + var chatMessageContent = messageEnvelope.Content; + var items = chatMessageContent.Items.Select(i => i switch + { + TextContent txt => new TextMessage(Role.Assistant, txt.Text!, messageEnvelope.From), + ImageContent img when img.Uri is Uri uri => new ImageMessage(Role.Assistant, uri.ToString(), from: messageEnvelope.From), + ImageContent img when img.Uri is null => throw new InvalidOperationException("ImageContent.Uri is null"), + _ => throw new InvalidOperationException("Unsupported content type"), + }); + + if (items.Count() == 1) + { + return items.First(); + } + else + { + return new MultiModalMessage(Role.Assistant, items, from: messageEnvelope.From); + } + } + + private IStreamingMessage PostProcessMessage(IStreamingMessage streamingMessage) + { + var chatMessageContent = streamingMessage.Content; + if (chatMessageContent.ChoiceIndex > 0) + { + throw new InvalidOperationException("Only one choice is supported in streaming response"); + } + return new TextMessageUpdate(Role.Assistant, chatMessageContent.Content, streamingMessage.From); + } + + private IEnumerable ProcessMessage(IEnumerable messages, IAgent agent) + { + return messages.SelectMany(m => + { + if (m is IMessage chatMessageContent) + { + return [chatMessageContent.Content]; + } + if (m.From == agent.Name) + { + return ProcessMessageForSelf(m); + } + else + { + return ProcessMessageForOthers(m); + } + }); + } + + private IEnumerable ProcessMessageForSelf(IMessage message) + { + return message switch + { + TextMessage textMessage => ProcessMessageForSelf(textMessage), + MultiModalMessage multiModalMessage => ProcessMessageForSelf(multiModalMessage), + Message m => ProcessMessageForSelf(m), + _ => throw new System.NotImplementedException(), + }; + } + + private IEnumerable ProcessMessageForOthers(IMessage message) + { + return message switch + { + TextMessage textMessage => ProcessMessageForOthers(textMessage), + MultiModalMessage multiModalMessage => ProcessMessageForOthers(multiModalMessage), + ImageMessage imageMessage => ProcessMessageForOthers(imageMessage), + Message m => ProcessMessageForOthers(m), + _ => throw new InvalidOperationException("unsupported message type, only support TextMessage, ImageMessage, MultiModalMessage and Message."), + }; + } + + private IEnumerable ProcessMessageForSelf(TextMessage message) + { + if (message.Role == Role.System) + { + return [new ChatMessageContent(AuthorRole.System, message.Content)]; + } + else + { + return [new ChatMessageContent(AuthorRole.Assistant, message.Content)]; + } + } + + + private IEnumerable ProcessMessageForOthers(TextMessage message) + { + if (message.Role == Role.System) + { + return [new ChatMessageContent(AuthorRole.System, message.Content)]; + } + else + { + return [new ChatMessageContent(AuthorRole.User, message.Content)]; + } + } + + private IEnumerable ProcessMessageForOthers(ImageMessage message) + { + var imageContent = new ImageContent(new Uri(message.Url)); + var collectionItems = new ChatMessageContentItemCollection(); + collectionItems.Add(imageContent); + return [new ChatMessageContent(AuthorRole.User, collectionItems)]; + } + + private IEnumerable ProcessMessageForSelf(MultiModalMessage message) + { + throw new System.InvalidOperationException("MultiModalMessage is not supported in the semantic kernel if it's from self."); + } + + private IEnumerable ProcessMessageForOthers(MultiModalMessage message) + { + var collections = new ChatMessageContentItemCollection(); + foreach (var item in message.Content) + { + if (item is TextMessage textContent) + { + collections.Add(new TextContent(textContent.Content)); + } + else if (item is ImageMessage imageContent) + { + collections.Add(new ImageContent(new Uri(imageContent.Url))); + } + else + { + throw new InvalidOperationException($"Unsupported message type: {item.GetType().Name}"); + } + } + return [new ChatMessageContent(AuthorRole.User, collections)]; + } + + + private IEnumerable ProcessMessageForSelf(Message message) + { + if (message.Role == Role.System) + { + return [new ChatMessageContent(AuthorRole.System, message.Content)]; + } + else if (message.Content is string && message.FunctionName is null && message.FunctionArguments is null) + { + return [new ChatMessageContent(AuthorRole.Assistant, message.Content)]; + } + else if (message.Content is null && message.FunctionName is not null && message.FunctionArguments is not null) + { + throw new System.InvalidOperationException("Function call is not supported in the semantic kernel if it's from self."); + } + else + { + throw new System.InvalidOperationException("Unsupported message type"); + } + } + + private IEnumerable ProcessMessageForOthers(Message message) + { + if (message.Role == Role.System) + { + return [new ChatMessageContent(AuthorRole.System, message.Content)]; + } + else if (message.Content is string && message.FunctionName is null && message.FunctionArguments is null) + { + return [new ChatMessageContent(AuthorRole.User, message.Content)]; + } + else if (message.Content is null && message.FunctionName is not null && message.FunctionArguments is not null) + { + throw new System.InvalidOperationException("Function call is not supported in the semantic kernel if it's from others."); + } + else + { + throw new System.InvalidOperationException("Unsupported message type"); + } + } +} diff --git a/dotnet/src/AutoGen.SemanticKernel/SemanticKernelAgent.cs b/dotnet/src/AutoGen.SemanticKernel/SemanticKernelAgent.cs new file mode 100644 index 00000000000..b887a6ef586 --- /dev/null +++ b/dotnet/src/AutoGen.SemanticKernel/SemanticKernelAgent.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SemanticKernelAgent.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +namespace AutoGen.SemanticKernel; + +/// +/// Semantic Kernel Agent +/// Income message could be one of the following type: +/// +/// where T is +/// +/// +/// Return message could be one of the following type: +/// +/// where T is +/// (streaming) where T is +/// +/// +/// To support more AutoGen built-in , register with . +/// +public class SemanticKernelAgent : IStreamingAgent +{ + private readonly Kernel _kernel; + private readonly string _systemMessage; + private readonly PromptExecutionSettings? _settings; + + public SemanticKernelAgent( + Kernel kernel, + string name, + string systemMessage = "You are a helpful AI assistant", + PromptExecutionSettings? settings = null) + { + _kernel = kernel; + this.Name = name; + _systemMessage = systemMessage; + _settings = settings; + } + + public string Name { get; } + + + public async Task GenerateReplyAsync(IEnumerable messages, GenerateReplyOptions? options = null, CancellationToken cancellationToken = default) + { + var chatHistory = BuildChatHistory(messages); + var option = BuildOption(options); + var chatService = _kernel.GetRequiredService(); + + var reply = await chatService.GetChatMessageContentsAsync(chatHistory, option, _kernel, cancellationToken); + + if (reply.Count > 1) + { + throw new InvalidOperationException("ResultsPerPrompt greater than 1 is not supported in this semantic kernel agent"); + } + + return new MessageEnvelope(reply.First(), from: this.Name); + } + + public async Task> GenerateStreamingReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + CancellationToken cancellationToken = default) + { + var chatHistory = BuildChatHistory(messages); + var option = BuildOption(options); + var chatService = _kernel.GetRequiredService(); + var response = chatService.GetStreamingChatMessageContentsAsync(chatHistory, option, _kernel, cancellationToken); + + return ProcessMessage(response); + } + + private ChatHistory BuildChatHistory(IEnumerable messages) + { + var chatMessageContents = ProcessMessage(messages); + // if there's no system message in chatMessageContents, add one to the beginning + if (!chatMessageContents.Any(c => c.Role == AuthorRole.System)) + { + chatMessageContents = new[] { new ChatMessageContent(AuthorRole.System, _systemMessage) }.Concat(chatMessageContents); + } + + return new ChatHistory(chatMessageContents); + } + + private PromptExecutionSettings BuildOption(GenerateReplyOptions? options) + { + return _settings ?? new OpenAIPromptExecutionSettings + { + Temperature = options?.Temperature ?? 0.7f, + MaxTokens = options?.MaxToken ?? 1024, + StopSequences = options?.StopSequence, + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions, + ResultsPerPrompt = 1, + }; + } + + private async IAsyncEnumerable ProcessMessage(IAsyncEnumerable response) + { + await foreach (var content in response) + { + if (content.ChoiceIndex > 0) + { + throw new InvalidOperationException("Only one choice is supported in streaming response"); + } + + yield return new MessageEnvelope(content, from: this.Name); + } + } + + private IEnumerable ProcessMessage(IEnumerable messages) + { + return messages.Select(m => m switch + { + IMessage cmc => cmc.Content, + _ => throw new ArgumentException("Invalid message type") + }); + } +} diff --git a/dotnet/src/AutoGen.SourceGenerator/AutoGen.SourceGenerator.csproj b/dotnet/src/AutoGen.SourceGenerator/AutoGen.SourceGenerator.csproj new file mode 100644 index 00000000000..a9d2766318c --- /dev/null +++ b/dotnet/src/AutoGen.SourceGenerator/AutoGen.SourceGenerator.csproj @@ -0,0 +1,60 @@ + + + + netstandard2.0 + false + + true + + 35954224-b94e-4024-b0ef-7ba7cf80c0d8 + $(GetTargetPathDependsOn);GetDependencyTargetPaths + false + $(NoWarn);NU5128 + $(DefineConstants);LAUNCH_DEBUGGER + + + + + + + AutoGen.SourceGenerator + Source generator for AutoGen. This package provides type-safe function call to AutoGen agents. + + + + + + + + + + + + + + + + + + + + + + + TextTemplatingFilePreprocessor + FunctionCallTemplate.cs + + + + + + + + + + True + True + FunctionCallTemplate.tt + + + diff --git a/dotnet/src/AutoGen.SourceGenerator/DocumentCommentExtension.cs b/dotnet/src/AutoGen.SourceGenerator/DocumentCommentExtension.cs new file mode 100644 index 00000000000..a09c77c2d75 --- /dev/null +++ b/dotnet/src/AutoGen.SourceGenerator/DocumentCommentExtension.cs @@ -0,0 +1,295 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// DocumentCommentExtension.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +// copyright: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/DocumentationCommentExtensions.cs#L17 +namespace AutoGen.SourceGenerator +{ + internal static class DocumentCommentExtension + { + public static bool IsMissingOrDefault(this SyntaxToken token) + { + return token.IsKind(SyntaxKind.None) + || token.IsMissing; + } + + public static string? GetParameterDescriptionFromDocumentationCommentTriviaSyntax(this DocumentationCommentTriviaSyntax documentationCommentTrivia, string parameterName) + { + var parameterElements = documentationCommentTrivia.Content.GetXmlElements("param"); + + var parameter = parameterElements.FirstOrDefault(element => + { + var xml = XElement.Parse(element.ToString()); + var nameAttribute = xml.Attribute("name"); + return nameAttribute != null && nameAttribute.Value == parameterName; + }); + + if (parameter is not null) + { + var xml = XElement.Parse(parameter.ToString()); + + return xml.Nodes().OfType().FirstOrDefault()?.Value; + } + + return null; + } + + public static string? GetNamespaceNameFromClassDeclarationSyntax(this ClassDeclarationSyntax classDeclaration) + { + return classDeclaration.Parent is NamespaceDeclarationSyntax namespaceDeclarationSyntax ? namespaceDeclarationSyntax.Name.ToString() + : classDeclaration.Parent is FileScopedNamespaceDeclarationSyntax fileScopedNamespaceDeclarationSyntax ? fileScopedNamespaceDeclarationSyntax.Name.ToString() + : null; + } + + public static DocumentationCommentTriviaSyntax? GetDocumentationCommentTriviaSyntax(this SyntaxNode node) + { + if (node == null) + { + return null; + } + + foreach (var leadingTrivia in node.GetLeadingTrivia()) + { + if (leadingTrivia.GetStructure() is DocumentationCommentTriviaSyntax structure) + { + return structure; + } + } + + return null; + } + + public static XmlNodeSyntax GetFirstXmlElement(this SyntaxList content, string elementName) + { + return content.GetXmlElements(elementName).FirstOrDefault(); + } + + public static IEnumerable GetXmlElements(this SyntaxList content, string elementName) + { + foreach (XmlNodeSyntax syntax in content) + { + if (syntax is XmlEmptyElementSyntax emptyElement) + { + if (string.Equals(elementName, emptyElement.Name.ToString(), StringComparison.Ordinal)) + { + yield return emptyElement; + } + + continue; + } + + if (syntax is XmlElementSyntax elementSyntax) + { + if (string.Equals(elementName, elementSyntax.StartTag?.Name?.ToString(), StringComparison.Ordinal)) + { + yield return elementSyntax; + } + + continue; + } + } + } + + public static T ReplaceExteriorTrivia(this T node, SyntaxTrivia trivia) + where T : XmlNodeSyntax + { + // Make sure to include a space after the '///' characters. + SyntaxTrivia triviaWithSpace = SyntaxFactory.DocumentationCommentExterior(trivia.ToString() + " "); + + return node.ReplaceTrivia( + node.DescendantTrivia(descendIntoTrivia: true).Where(i => i.IsKind(SyntaxKind.DocumentationCommentExteriorTrivia)), + (originalTrivia, rewrittenTrivia) => SelectExteriorTrivia(rewrittenTrivia, trivia, triviaWithSpace)); + } + + public static SyntaxList WithoutFirstAndLastNewlines(this SyntaxList summaryContent) + { + if (summaryContent.Count == 0) + { + return summaryContent; + } + + if (!(summaryContent[0] is XmlTextSyntax firstSyntax)) + { + return summaryContent; + } + + if (!(summaryContent[summaryContent.Count - 1] is XmlTextSyntax lastSyntax)) + { + return summaryContent; + } + + SyntaxTokenList firstSyntaxTokens = firstSyntax.TextTokens; + + int removeFromStart; + if (IsXmlNewLine(firstSyntaxTokens[0])) + { + removeFromStart = 1; + } + else + { + if (!IsXmlWhitespace(firstSyntaxTokens[0])) + { + return summaryContent; + } + + if (!IsXmlNewLine(firstSyntaxTokens[1])) + { + return summaryContent; + } + + removeFromStart = 2; + } + + SyntaxTokenList lastSyntaxTokens = lastSyntax.TextTokens; + + int removeFromEnd; + if (IsXmlNewLine(lastSyntaxTokens[lastSyntaxTokens.Count - 1])) + { + removeFromEnd = 1; + } + else + { + if (!IsXmlWhitespace(lastSyntaxTokens[lastSyntaxTokens.Count - 1])) + { + return summaryContent; + } + + if (!IsXmlNewLine(lastSyntaxTokens[lastSyntaxTokens.Count - 2])) + { + return summaryContent; + } + + removeFromEnd = 2; + } + + for (int i = 0; i < removeFromStart; i++) + { + firstSyntaxTokens = firstSyntaxTokens.RemoveAt(0); + } + + if (firstSyntax == lastSyntax) + { + lastSyntaxTokens = firstSyntaxTokens; + } + + for (int i = 0; i < removeFromEnd; i++) + { + if (!lastSyntaxTokens.Any()) + { + break; + } + + lastSyntaxTokens = lastSyntaxTokens.RemoveAt(lastSyntaxTokens.Count - 1); + } + + summaryContent = summaryContent.RemoveAt(summaryContent.Count - 1); + if (lastSyntaxTokens.Count != 0) + { + summaryContent = summaryContent.Add(lastSyntax.WithTextTokens(lastSyntaxTokens)); + } + + if (firstSyntax != lastSyntax) + { + summaryContent = summaryContent.RemoveAt(0); + if (firstSyntaxTokens.Count != 0) + { + summaryContent = summaryContent.Insert(0, firstSyntax.WithTextTokens(firstSyntaxTokens)); + } + } + + if (summaryContent.Count > 0) + { + // Make sure to remove the leading trivia + summaryContent = summaryContent.Replace(summaryContent[0], summaryContent[0].WithLeadingTrivia()); + + // Remove leading spaces (between the start tag and the start of the paragraph content) + if (summaryContent[0] is XmlTextSyntax firstTextSyntax && firstTextSyntax.TextTokens.Count > 0) + { + SyntaxToken firstTextToken = firstTextSyntax.TextTokens[0]; + string firstTokenText = firstTextToken.Text; + string trimmed = firstTokenText.TrimStart(); + if (trimmed != firstTokenText) + { + SyntaxToken newFirstToken = SyntaxFactory.Token( + firstTextToken.LeadingTrivia, + firstTextToken.Kind(), + trimmed, + firstTextToken.ValueText.TrimStart(), + firstTextToken.TrailingTrivia); + + summaryContent = summaryContent.Replace(firstTextSyntax, firstTextSyntax.ReplaceToken(firstTextToken, newFirstToken)); + } + } + } + + return summaryContent; + } + + public static bool IsXmlNewLine(this SyntaxToken node) + { + return node.IsKind(SyntaxKind.XmlTextLiteralNewLineToken); + } + + public static bool IsXmlWhitespace(this SyntaxToken node) + { + return node.IsKind(SyntaxKind.XmlTextLiteralToken) + && string.IsNullOrWhiteSpace(node.Text); + } + + /// + /// Adjust the leading and trailing trivia associated with + /// tokens to ensure the formatter properly indents the exterior trivia. + /// + /// The type of syntax node. + /// The syntax node to adjust tokens. + /// A equivalent to the input , adjusted by moving any + /// trailing trivia from tokens to be leading trivia of the + /// following token. + public static T AdjustDocumentationCommentNewLineTrivia(this T node) + where T : SyntaxNode + { + var tokensForAdjustment = + from token in node.DescendantTokens() + where token.IsKind(SyntaxKind.XmlTextLiteralNewLineToken) + where token.HasTrailingTrivia + let next = token.GetNextToken(includeZeroWidth: true, includeSkipped: true, includeDirectives: true, includeDocumentationComments: true) + where !next.IsMissingOrDefault() + select new KeyValuePair(token, next); + + Dictionary replacements = new Dictionary(); + foreach (var pair in tokensForAdjustment) + { + replacements[pair.Key] = pair.Key.WithTrailingTrivia(); + replacements[pair.Value] = pair.Value.WithLeadingTrivia(pair.Value.LeadingTrivia.InsertRange(0, pair.Key.TrailingTrivia)); + } + + return node.ReplaceTokens(replacements.Keys, (originalToken, rewrittenToken) => replacements[originalToken]); + } + + public static XmlNameSyntax? GetName(this XmlNodeSyntax element) + { + return (element as XmlElementSyntax)?.StartTag?.Name + ?? (element as XmlEmptyElementSyntax)?.Name; + } + + private static SyntaxTrivia SelectExteriorTrivia(SyntaxTrivia rewrittenTrivia, SyntaxTrivia trivia, SyntaxTrivia triviaWithSpace) + { + // if the trivia had a trailing space, make sure to preserve it + if (rewrittenTrivia.ToString().EndsWith(" ")) + { + return triviaWithSpace; + } + + // otherwise the space is part of the leading trivia of the following token, so don't add an extra one to + // the exterior trivia + return trivia; + } + } +} diff --git a/dotnet/src/AutoGen.SourceGenerator/FunctionCallGenerator.cs b/dotnet/src/AutoGen.SourceGenerator/FunctionCallGenerator.cs new file mode 100644 index 00000000000..50bdc03f0af --- /dev/null +++ b/dotnet/src/AutoGen.SourceGenerator/FunctionCallGenerator.cs @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// FunctionCallGenerator.cs + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using AutoGen.SourceGenerator.Template; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Newtonsoft.Json; + +namespace AutoGen.SourceGenerator +{ + [Generator] + public partial class FunctionCallGenerator : IIncrementalGenerator + { + private const string FUNCTION_CALL_ATTRIBUTION = "AutoGen.Core.FunctionAttribute"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { +#if LAUNCH_DEBUGGER + if (!System.Diagnostics.Debugger.IsAttached) + { + System.Diagnostics.Debugger.Launch(); + } +#endif + var optionProvider = context.AnalyzerConfigOptionsProvider.Select((provider, ct) => + { + var generateFunctionDefinitionContract = provider.GlobalOptions.TryGetValue("build_property.EnableContract", out var value) && value?.ToLowerInvariant() == "true"; + + return generateFunctionDefinitionContract; + }); + // step 1 + // filter syntax tree and search syntax node that satisfied the following conditions + // - is partial class + var partialClassSyntaxProvider = context.SyntaxProvider.CreateSyntaxProvider( + (node, ct) => + { + return node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.Modifiers.Any(SyntaxKind.PartialKeyword); + }, + (ctx, ct) => + { + // first check if any method of the class has FunctionAttribution attribute + // if not, then return null + var filePath = ctx.Node.SyntaxTree.FilePath; + var fileName = Path.GetFileNameWithoutExtension(filePath); + + + var classDeclarationSyntax = ctx.Node as ClassDeclarationSyntax; + var nameSpace = classDeclarationSyntax?.Parent as NamespaceDeclarationSyntax; + var fullClassName = $"{nameSpace?.Name}.{classDeclarationSyntax!.Identifier}"; + if (classDeclarationSyntax == null) + { + return null; + } + + if (!classDeclarationSyntax.Members.Any(member => member.AttributeLists.Any(attributeList => attributeList.Attributes.Any(attribute => + { + return ctx.SemanticModel.GetSymbolInfo(attribute).Symbol is IMethodSymbol methodSymbol && methodSymbol.ContainingType.ToDisplayString() == FUNCTION_CALL_ATTRIBUTION; + })))) + { + return null; + } + + // collect methods that has FunctionAttribution attribute + var methodDeclarationSyntaxes = classDeclarationSyntax.Members.Where(member => member.AttributeLists.Any(attributeList => attributeList.Attributes.Any(attribute => + { + return ctx.SemanticModel.GetSymbolInfo(attribute).Symbol is IMethodSymbol methodSymbol && methodSymbol.ContainingType.ToDisplayString() == FUNCTION_CALL_ATTRIBUTION; + }))) + .Select(member => member as MethodDeclarationSyntax) + .Where(method => method != null); + + var className = classDeclarationSyntax.Identifier.ToString(); + var namespaceName = classDeclarationSyntax.GetNamespaceNameFromClassDeclarationSyntax(); + var functionContracts = methodDeclarationSyntaxes.Select(method => CreateFunctionContract(method!, className, namespaceName)); + + return new PartialClassOutput(fullClassName, classDeclarationSyntax, functionContracts); + }) + .Where(node => node != null) + .Collect(); + + var aggregateProvider = optionProvider.Combine(partialClassSyntaxProvider); + // step 2 + context.RegisterSourceOutput(aggregateProvider, + (ctx, source) => + { + var groups = source.Right.GroupBy(item => item!.FullClassName); + foreach (var group in groups) + { + var functionContracts = group.SelectMany(item => item!.FunctionContracts).ToArray(); + var className = group.First()!.ClassDeclarationSyntax.Identifier.ToString(); + var namespaceName = group.First()!.ClassDeclarationSyntax.GetNamespaceNameFromClassDeclarationSyntax() ?? string.Empty; + var functionTT = new FunctionCallTemplate + { + NameSpace = namespaceName, + ClassName = className, + FunctionContracts = functionContracts.ToArray(), + }; + + var functionSource = functionTT.TransformText(); + var fileName = $"{className}.generated.cs"; + + ctx.AddSource(fileName, SourceText.From(functionSource, System.Text.Encoding.UTF8)); + File.WriteAllText(Path.Combine(Path.GetTempPath(), fileName), functionSource); + } + + if (source.Left) + { + var overallFunctionDefinition = source.Right.SelectMany(x => x!.FunctionContracts.Select(y => new { fullClassName = x.FullClassName, y = y })); + var overallFunctionDefinitionObject = overallFunctionDefinition.Select( + x => new + { + fullClassName = x.fullClassName, + functionDefinition = new + { + x.y.Name, + x.y.Description, + x.y.ReturnType, + Parameters = x.y.Parameters.Select(y => new + { + y.Name, + y.Description, + y.JsonType, + y.JsonItemType, + y.Type, + y.IsOptional, + y.DefaultValue, + }), + }, + }); + + var json = JsonConvert.SerializeObject(overallFunctionDefinitionObject, formatting: Formatting.Indented); + // wrap json inside csharp block, as SG doesn't support generating non-source file + json = $@"/* wrap json inside csharp block, as SG doesn't support generating non-source file +{json} +*/"; + ctx.AddSource("FunctionDefinition.json", SourceText.From(json, System.Text.Encoding.UTF8)); + } + }); + } + + private class PartialClassOutput + { + public PartialClassOutput(string fullClassName, ClassDeclarationSyntax classDeclarationSyntax, IEnumerable functionContracts) + { + FullClassName = fullClassName; + ClassDeclarationSyntax = classDeclarationSyntax; + FunctionContracts = functionContracts; + } + + public string FullClassName { get; } + + public ClassDeclarationSyntax ClassDeclarationSyntax { get; } + + public IEnumerable FunctionContracts { get; } + } + + private FunctionContract CreateFunctionContract(MethodDeclarationSyntax method, string? className, string? namespaceName) + { + // get function_call attribute + var functionCallAttribute = method.AttributeLists.SelectMany(attributeList => attributeList.Attributes) + .FirstOrDefault(attribute => attribute.Name.ToString() == FUNCTION_CALL_ATTRIBUTION); + // get document string if exist + var documentationCommentTrivia = method.GetDocumentationCommentTriviaSyntax(); + + var functionName = method.Identifier.ToString(); + var functionDescription = functionCallAttribute?.ArgumentList?.Arguments.FirstOrDefault(argument => argument.NameEquals?.Name.ToString() == "Description")?.Expression.ToString() ?? string.Empty; + + if (string.IsNullOrEmpty(functionDescription)) + { + // if functionDescription is empty, then try to get it from documentationCommentTrivia + // firstly, try getting from tag + var summary = documentationCommentTrivia?.Content.GetFirstXmlElement("summary"); + if (summary is not null && XElement.Parse(summary.ToString()) is XElement element) + { + functionDescription = element.Nodes().OfType().FirstOrDefault()?.Value; + + // remove [space...][//|///][space...] from functionDescription + // replace [^\S\r\n]+[\/]+\s* with empty string + functionDescription = System.Text.RegularExpressions.Regex.Replace(functionDescription, @"[^\S\r\n]+\/[\/]+\s*", string.Empty); + } + else + { + // if tag is not exist, then simply use the entire leading trivia as functionDescription + functionDescription = method.GetLeadingTrivia().ToString(); + + // remove [space...][//|///][space...] from functionDescription + // replace [^\S\r\n]+[\/]+\s* with empty string + functionDescription = System.Text.RegularExpressions.Regex.Replace(functionDescription, @"[^\S\r\n]+\/[\/]+\s*", string.Empty); + } + } + + // get parameters + var parameters = method.ParameterList.Parameters.Select(parameter => + { + var description = $"{parameter.Identifier}. type is {parameter.Type}"; + + // try to get parameter description from documentationCommentTrivia + var parameterDocumentationComment = documentationCommentTrivia?.GetParameterDescriptionFromDocumentationCommentTriviaSyntax(parameter.Identifier.ToString()); + if (parameterDocumentationComment is not null) + { + description = parameterDocumentationComment.ToString(); + // remove [space...][//|///][space...] from functionDescription + // replace [^\S\r\n]+[\/]+\s* with empty string + description = System.Text.RegularExpressions.Regex.Replace(description, @"[^\S\r\n]+\/[\/]+\s*", string.Empty); + } + var jsonItemType = parameter.Type!.ToString().EndsWith("[]") ? parameter.Type!.ToString().Substring(0, parameter.Type!.ToString().Length - 2) : null; + return new ParameterContract + { + Name = parameter.Identifier.ToString(), + JsonType = parameter.Type!.ToString() switch + { + "string" => "string", + "string[]" => "array", + "System.Int32" or "int" => "integer", + "System.Int64" or "long" => "integer", + "System.Single" or "float" => "number", + "System.Double" or "double" => "number", + "System.Boolean" or "bool" => "boolean", + "System.DateTime" => "string", + "System.Guid" => "string", + "System.Object" => "object", + _ => "object", + }, + JsonItemType = jsonItemType, + Type = parameter.Type!.ToString(), + Description = description, + IsOptional = parameter.Default != null, + // if Default is null or "null", then DefaultValue is null + DefaultValue = parameter.Default?.ToString() == "null" ? null : parameter.Default?.Value.ToString(), + }; + }); + + return new FunctionContract + { + ClassName = className, + Namespace = namespaceName, + Name = functionName, + Description = functionDescription?.Trim() ?? functionName, + Parameters = parameters.ToArray(), + ReturnType = method.ReturnType.ToString(), + }; + } + } +} diff --git a/dotnet/src/AutoGen.SourceGenerator/FunctionContract.cs b/dotnet/src/AutoGen.SourceGenerator/FunctionContract.cs new file mode 100644 index 00000000000..2f26352173d --- /dev/null +++ b/dotnet/src/AutoGen.SourceGenerator/FunctionContract.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// FunctionContract.cs + +namespace AutoGen.SourceGenerator +{ + internal class FunctionContract + { + public string? Namespace { get; set; } + + public string? ClassName { get; set; } + + public string? Name { get; set; } + + public string? Description { get; set; } + + public string? ReturnDescription { get; set; } + + public ParameterContract[]? Parameters { get; set; } + + public string? ReturnType { get; set; } + } + + internal class ParameterContract + { + public string? Name { get; set; } + + public string? Description { get; set; } + + public string? JsonType { get; set; } + + public string? JsonItemType { get; set; } + + public string? Type { get; set; } + + public bool IsOptional { get; set; } + + public string? DefaultValue { get; set; } + + } +} diff --git a/dotnet/src/AutoGen.SourceGenerator/FunctionExtension.cs b/dotnet/src/AutoGen.SourceGenerator/FunctionExtension.cs new file mode 100644 index 00000000000..a56e4cb54f4 --- /dev/null +++ b/dotnet/src/AutoGen.SourceGenerator/FunctionExtension.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// FunctionExtension.cs + +using AutoGen.SourceGenerator; + +internal static class FunctionExtension +{ + public static string GetFunctionName(this FunctionContract function) + { + return function.Name ?? string.Empty; + } + + public static string GetFunctionSchemaClassName(this FunctionContract function) + { + return $"{function.GetFunctionName()}Schema"; + } + + public static string GetFunctionDefinitionName(this FunctionContract function) + { + return $"{function.GetFunctionName()}Function"; + } + + public static string GetFunctionWrapperName(this FunctionContract function) + { + return $"{function.GetFunctionName()}Wrapper"; + } + + public static string GetFunctionContractName(this FunctionContract function) + { + return $"{function.GetFunctionName()}FunctionContract"; + } +} diff --git a/dotnet/src/AutoGen.SourceGenerator/README.md b/dotnet/src/AutoGen.SourceGenerator/README.md new file mode 100644 index 00000000000..a40fbe60407 --- /dev/null +++ b/dotnet/src/AutoGen.SourceGenerator/README.md @@ -0,0 +1,113 @@ +### AutoGen.SourceGenerator + +This package carries a source generator that adds support for type-safe function definition generation. Simply mark a method with `Function` attribute, and the source generator will generate a function definition and a function call wrapper for you. + +### Get start + +First, add the following to your project file and set `GenerateDocumentationFile` property to true + +```xml + + + true + +``` +```xml + + + +``` + +> Nightly Build feed: https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/AutoGen/nuget/v3/index.json + +Then, for the methods you want to generate function definition and function call wrapper, mark them with `Function` attribute: + +> Note: For the best of performance, try using primitive types for the parameters and return type. + +```csharp +// file: MyFunctions.cs + +using AutoGen; + +// a partial class is required +// and the class must be public +public partial class MyFunctions +{ + /// + /// Add two numbers. + /// + /// The first number. + /// The second number. + [Function] + public Task AddAsync(int a, int b) + { + return Task.FromResult($"{a} + {b} = {a + b}"); + } +} +``` + +The source generator will generate the following code based on the method signature and documentation. It helps you save the effort of writing function definition and keep it up to date with the actual method signature. + +```csharp +// file: MyFunctions.generated.cs +public partial class MyFunctions +{ + private class AddAsyncSchema + { + public int a {get; set;} + public int b {get; set;} + } + + public Task AddAsyncWrapper(string arguments) + { + var schema = JsonSerializer.Deserialize( + arguments, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }); + return AddAsync(schema.a, schema.b); + } + + public FunctionDefinition AddAsyncFunction + { + get => new FunctionDefinition + { + Name = @"AddAsync", + Description = """ +Add two numbers. +""", + Parameters = BinaryData.FromObjectAsJson(new + { + Type = "object", + Properties = new + { + a = new + { + Type = @"number", + Description = @"The first number.", + }, + b = new + { + Type = @"number", + Description = @"The second number.", + }, + }, + Required = new [] + { + "a", + "b", + }, + }, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }) + }; + } +} +``` + +For more examples, please check out the following project +- [AutoGen.BasicSamples](../sample/AutoGen.BasicSamples/) +- [AutoGen.SourceGenerator.Tests](../../test/AutoGen.SourceGenerator.Tests/) diff --git a/dotnet/src/AutoGen.SourceGenerator/Template/FunctionCallTemplate.cs b/dotnet/src/AutoGen.SourceGenerator/Template/FunctionCallTemplate.cs new file mode 100644 index 00000000000..1d455bd3041 --- /dev/null +++ b/dotnet/src/AutoGen.SourceGenerator/Template/FunctionCallTemplate.cs @@ -0,0 +1,447 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version: 17.0.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +namespace AutoGen.SourceGenerator.Template +{ + using System.Linq; + using System.Collections.Generic; + using Microsoft.CodeAnalysis; + using System; + + /// + /// Class to produce the template output + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "17.0.0.0")] + internal partial class FunctionCallTemplate : FunctionCallTemplateBase + { + /// + /// Create the template output + /// + public virtual string TransformText() + { + this.Write(""); + this.Write(@"//---------------------- +// +// This code was generated by a tool. +// +//---------------------- +using Azure.AI.OpenAI; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using System; +using AutoGen.Core; +using AutoGen.OpenAI.Extension; + +"); +if (!String.IsNullOrEmpty(NameSpace)) { + this.Write("namespace "); + this.Write(this.ToStringHelper.ToStringWithCulture(NameSpace)); + this.Write("\r\n{\r\n"); +} + this.Write(" public partial class "); + this.Write(this.ToStringHelper.ToStringWithCulture(ClassName)); + this.Write("\r\n {\r\n"); +foreach (var functionContract in FunctionContracts) { + this.Write("\r\n private class "); + this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.GetFunctionSchemaClassName())); + this.Write("\r\n {\r\n"); +foreach (var parameter in functionContract.Parameters) { +if (parameter.IsOptional) { + this.Write(" [JsonPropertyName(@\""); + this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Name)); + this.Write("\")]\r\n\t\t\tpublic "); + this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Type)); + this.Write(" "); + this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Name)); + this.Write(" {get; set;} = "); + this.Write(this.ToStringHelper.ToStringWithCulture(parameter.DefaultValue)); + this.Write(";\r\n"); +} else { + this.Write(" [JsonPropertyName(@\""); + this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Name)); + this.Write("\")]\r\n\t\t\tpublic "); + this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Type)); + this.Write(" "); + this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Name)); + this.Write(" {get; set;}\r\n"); +} +} + this.Write(" }\r\n\r\n public "); + this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.ReturnType)); + this.Write(" "); + this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.GetFunctionWrapperName())); + this.Write("(string arguments)\r\n {\r\n var schema = JsonSerializer.Deserializ" + + "e<"); + this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.GetFunctionSchemaClassName())); + this.Write(">(\r\n arguments, \r\n new JsonSerializerOptions\r\n " + + " {\r\n PropertyNamingPolicy = JsonNamingPolicy.CamelC" + + "ase,\r\n });\r\n"); + var argumentLists = string.Join(", ", functionContract.Parameters.Select(p => $"schema.{p.Name}")); + this.Write("\r\n return "); + this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.Name)); + this.Write("("); + this.Write(this.ToStringHelper.ToStringWithCulture(argumentLists)); + this.Write(");\r\n }\r\n\r\n public FunctionContract "); + this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.GetFunctionContractName())); + this.Write("\r\n {\r\n get => new FunctionContract\r\n {\r\n"); +if (functionContract.Namespace != null) { + this.Write(" Namespace = @\""); + this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.Namespace)); + this.Write("\",\r\n"); +} +if (functionContract.ClassName != null) { + this.Write(" ClassName = @\""); + this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.ClassName)); + this.Write("\",\r\n"); +} +if (functionContract.Name != null) { + this.Write(" Name = @\""); + this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.Name)); + this.Write("\",\r\n"); +} +if (functionContract.Description != null) { + this.Write(" Description = @\""); + this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.Description)); + this.Write("\",\r\n"); +} +if (functionContract.ReturnType != null) { + this.Write(" ReturnType = typeof("); + this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.ReturnType)); + this.Write("),\r\n"); +} +if (functionContract.ReturnDescription != null) { + this.Write(" ReturnDescription = @\""); + this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.ReturnDescription)); + this.Write("\",\r\n"); +} +if (functionContract.Parameters != null) { + this.Write(" Parameters = new []\r\n {\r\n"); +foreach (var parameter in functionContract.Parameters) { + this.Write(" new FunctionParameterContract\r\n {\r\n"); +if (parameter.Name != null) { + this.Write(" Name = @\""); + this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Name)); + this.Write("\",\r\n"); +} +if (parameter.Description != null) { + this.Write(" Description = @\""); + this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Description)); + this.Write("\",\r\n"); +} +if (parameter.Type != null) { + this.Write(" ParameterType = typeof("); + this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Type)); + this.Write("),\r\n"); +} + this.Write(" IsRequired = "); + this.Write(this.ToStringHelper.ToStringWithCulture(parameter.IsOptional ? "false" : "true")); + this.Write(",\r\n"); +if (parameter.DefaultValue != null) { + this.Write(" DefaultValue = "); + this.Write(this.ToStringHelper.ToStringWithCulture(parameter.DefaultValue)); + this.Write(",\r\n"); +} + this.Write(" },\r\n"); +} + this.Write(" },\r\n"); +} + this.Write(" };\r\n }\r\n\r\n public Azure.AI.OpenAI.FunctionDefinition "); + this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.GetFunctionDefinitionName())); + this.Write("\r\n {\r\n get => this."); + this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.GetFunctionContractName())); + this.Write(".ToOpenAIFunctionDefinition();\r\n }\r\n"); +} + this.Write(" }\r\n"); +if (!String.IsNullOrEmpty(NameSpace)) { + this.Write("}\r\n"); +} + this.Write("\r\n"); + return this.GenerationEnvironment.ToString(); + } + +public string NameSpace {get; set;} +public string ClassName {get; set;} +public IEnumerable FunctionContracts {get; set;} +public bool IsStatic {get; set;} = false; + + } + #region Base class + /// + /// Base class for this transformation + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "17.0.0.0")] + internal class FunctionCallTemplateBase + { + #region Fields + private global::System.Text.StringBuilder generationEnvironmentField; + private global::System.CodeDom.Compiler.CompilerErrorCollection errorsField; + private global::System.Collections.Generic.List indentLengthsField; + private string currentIndentField = ""; + private bool endsWithNewline; + private global::System.Collections.Generic.IDictionary sessionField; + #endregion + #region Properties + /// + /// The string builder that generation-time code is using to assemble generated output + /// + public System.Text.StringBuilder GenerationEnvironment + { + get + { + if ((this.generationEnvironmentField == null)) + { + this.generationEnvironmentField = new global::System.Text.StringBuilder(); + } + return this.generationEnvironmentField; + } + set + { + this.generationEnvironmentField = value; + } + } + /// + /// The error collection for the generation process + /// + public System.CodeDom.Compiler.CompilerErrorCollection Errors + { + get + { + if ((this.errorsField == null)) + { + this.errorsField = new global::System.CodeDom.Compiler.CompilerErrorCollection(); + } + return this.errorsField; + } + } + /// + /// A list of the lengths of each indent that was added with PushIndent + /// + private System.Collections.Generic.List indentLengths + { + get + { + if ((this.indentLengthsField == null)) + { + this.indentLengthsField = new global::System.Collections.Generic.List(); + } + return this.indentLengthsField; + } + } + /// + /// Gets the current indent we use when adding lines to the output + /// + public string CurrentIndent + { + get + { + return this.currentIndentField; + } + } + /// + /// Current transformation session + /// + public virtual global::System.Collections.Generic.IDictionary Session + { + get + { + return this.sessionField; + } + set + { + this.sessionField = value; + } + } + #endregion + #region Transform-time helpers + /// + /// Write text directly into the generated output + /// + public void Write(string textToAppend) + { + if (string.IsNullOrEmpty(textToAppend)) + { + return; + } + // If we're starting off, or if the previous text ended with a newline, + // we have to append the current indent first. + if (((this.GenerationEnvironment.Length == 0) + || this.endsWithNewline)) + { + this.GenerationEnvironment.Append(this.currentIndentField); + this.endsWithNewline = false; + } + // Check if the current text ends with a newline + if (textToAppend.EndsWith(global::System.Environment.NewLine, global::System.StringComparison.CurrentCulture)) + { + this.endsWithNewline = true; + } + // This is an optimization. If the current indent is "", then we don't have to do any + // of the more complex stuff further down. + if ((this.currentIndentField.Length == 0)) + { + this.GenerationEnvironment.Append(textToAppend); + return; + } + // Everywhere there is a newline in the text, add an indent after it + textToAppend = textToAppend.Replace(global::System.Environment.NewLine, (global::System.Environment.NewLine + this.currentIndentField)); + // If the text ends with a newline, then we should strip off the indent added at the very end + // because the appropriate indent will be added when the next time Write() is called + if (this.endsWithNewline) + { + this.GenerationEnvironment.Append(textToAppend, 0, (textToAppend.Length - this.currentIndentField.Length)); + } + else + { + this.GenerationEnvironment.Append(textToAppend); + } + } + /// + /// Write text directly into the generated output + /// + public void WriteLine(string textToAppend) + { + this.Write(textToAppend); + this.GenerationEnvironment.AppendLine(); + this.endsWithNewline = true; + } + /// + /// Write formatted text directly into the generated output + /// + public void Write(string format, params object[] args) + { + this.Write(string.Format(global::System.Globalization.CultureInfo.CurrentCulture, format, args)); + } + /// + /// Write formatted text directly into the generated output + /// + public void WriteLine(string format, params object[] args) + { + this.WriteLine(string.Format(global::System.Globalization.CultureInfo.CurrentCulture, format, args)); + } + /// + /// Raise an error + /// + public void Error(string message) + { + System.CodeDom.Compiler.CompilerError error = new global::System.CodeDom.Compiler.CompilerError(); + error.ErrorText = message; + this.Errors.Add(error); + } + /// + /// Raise a warning + /// + public void Warning(string message) + { + System.CodeDom.Compiler.CompilerError error = new global::System.CodeDom.Compiler.CompilerError(); + error.ErrorText = message; + error.IsWarning = true; + this.Errors.Add(error); + } + /// + /// Increase the indent + /// + public void PushIndent(string indent) + { + if ((indent == null)) + { + throw new global::System.ArgumentNullException("indent"); + } + this.currentIndentField = (this.currentIndentField + indent); + this.indentLengths.Add(indent.Length); + } + /// + /// Remove the last indent that was added with PushIndent + /// + public string PopIndent() + { + string returnValue = ""; + if ((this.indentLengths.Count > 0)) + { + int indentLength = this.indentLengths[(this.indentLengths.Count - 1)]; + this.indentLengths.RemoveAt((this.indentLengths.Count - 1)); + if ((indentLength > 0)) + { + returnValue = this.currentIndentField.Substring((this.currentIndentField.Length - indentLength)); + this.currentIndentField = this.currentIndentField.Remove((this.currentIndentField.Length - indentLength)); + } + } + return returnValue; + } + /// + /// Remove any indentation + /// + public void ClearIndent() + { + this.indentLengths.Clear(); + this.currentIndentField = ""; + } + #endregion + #region ToString Helpers + /// + /// Utility class to produce culture-oriented representation of an object as a string. + /// + public class ToStringInstanceHelper + { + private System.IFormatProvider formatProviderField = global::System.Globalization.CultureInfo.InvariantCulture; + /// + /// Gets or sets format provider to be used by ToStringWithCulture method. + /// + public System.IFormatProvider FormatProvider + { + get + { + return this.formatProviderField ; + } + set + { + if ((value != null)) + { + this.formatProviderField = value; + } + } + } + /// + /// This is called from the compile/run appdomain to convert objects within an expression block to a string + /// + public string ToStringWithCulture(object objectToConvert) + { + if ((objectToConvert == null)) + { + throw new global::System.ArgumentNullException("objectToConvert"); + } + System.Type t = objectToConvert.GetType(); + System.Reflection.MethodInfo method = t.GetMethod("ToString", new System.Type[] { + typeof(System.IFormatProvider)}); + if ((method == null)) + { + return objectToConvert.ToString(); + } + else + { + return ((string)(method.Invoke(objectToConvert, new object[] { + this.formatProviderField }))); + } + } + } + private ToStringInstanceHelper toStringHelperField = new ToStringInstanceHelper(); + /// + /// Helper to produce culture-oriented representation of an object as a string + /// + public ToStringInstanceHelper ToStringHelper + { + get + { + return this.toStringHelperField; + } + } + #endregion + } + #endregion +} diff --git a/dotnet/src/AutoGen.SourceGenerator/Template/FunctionCallTemplate.tt b/dotnet/src/AutoGen.SourceGenerator/Template/FunctionCallTemplate.tt new file mode 100644 index 00000000000..baa2a680fe2 --- /dev/null +++ b/dotnet/src/AutoGen.SourceGenerator/Template/FunctionCallTemplate.tt @@ -0,0 +1,116 @@ +<#@ template language="C#" linePragmas="false" visibility = "internal" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ import namespace="Microsoft.CodeAnalysis" #> +//---------------------- +// +// This code was generated by a tool. +// +//---------------------- +using Azure.AI.OpenAI; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using System; +using AutoGen.Core; +using AutoGen.OpenAI.Extension; + +<#if (!String.IsNullOrEmpty(NameSpace)) {#> +namespace <#=NameSpace#> +{ +<#}#> + public partial class <#=ClassName#> + { +<#foreach (var functionContract in FunctionContracts) {#> + + private class <#=functionContract.GetFunctionSchemaClassName()#> + { +<#foreach (var parameter in functionContract.Parameters) {#> +<#if (parameter.IsOptional) {#> + [JsonPropertyName(@"<#=parameter.Name#>")] + public <#=parameter.Type#> <#=parameter.Name#> {get; set;} = <#=parameter.DefaultValue#>; +<#} else {#> + [JsonPropertyName(@"<#=parameter.Name#>")] + public <#=parameter.Type#> <#=parameter.Name#> {get; set;} +<#}#> +<#}#> + } + + public <#=functionContract.ReturnType#> <#=functionContract.GetFunctionWrapperName()#>(string arguments) + { + var schema = JsonSerializer.Deserialize<<#=functionContract.GetFunctionSchemaClassName()#>>( + arguments, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }); +<# var argumentLists = string.Join(", ", functionContract.Parameters.Select(p => $"schema.{p.Name}")); #> + + return <#=functionContract.Name#>(<#=argumentLists#>); + } + + public FunctionContract <#=functionContract.GetFunctionContractName()#> + { + get => new FunctionContract + { +<#if (functionContract.Namespace != null) {#> + Namespace = @"<#=functionContract.Namespace#>", +<#}#> +<#if (functionContract.ClassName != null) {#> + ClassName = @"<#=functionContract.ClassName#>", +<#}#> +<#if (functionContract.Name != null) {#> + Name = @"<#=functionContract.Name#>", +<#}#> +<#if (functionContract.Description != null) {#> + Description = @"<#=functionContract.Description#>", +<#}#> +<#if (functionContract.ReturnType != null) {#> + ReturnType = typeof(<#=functionContract.ReturnType#>), +<#}#> +<#if (functionContract.ReturnDescription != null) {#> + ReturnDescription = @"<#=functionContract.ReturnDescription#>", +<#}#> +<#if (functionContract.Parameters != null) {#> + Parameters = new [] + { +<#foreach (var parameter in functionContract.Parameters) {#> + new FunctionParameterContract + { +<#if (parameter.Name != null) {#> + Name = @"<#=parameter.Name#>", +<#}#> +<#if (parameter.Description != null) {#> + Description = @"<#=parameter.Description#>", +<#}#> +<#if (parameter.Type != null) {#> + ParameterType = typeof(<#=parameter.Type#>), +<#}#> + IsRequired = <#=parameter.IsOptional ? "false" : "true"#>, +<#if (parameter.DefaultValue != null) {#> + DefaultValue = <#=parameter.DefaultValue#>, +<#}#> + }, +<#}#> + }, +<#}#> + }; + } + + public Azure.AI.OpenAI.FunctionDefinition <#=functionContract.GetFunctionDefinitionName()#> + { + get => this.<#=functionContract.GetFunctionContractName()#>.ToOpenAIFunctionDefinition(); + } +<#}#> + } +<#if (!String.IsNullOrEmpty(NameSpace)) {#> +} +<#}#> + +<#+ +public string NameSpace {get; set;} +public string ClassName {get; set;} +public IEnumerable FunctionContracts {get; set;} +public bool IsStatic {get; set;} = false; +#> \ No newline at end of file diff --git a/dotnet/src/AutoGen/API/LLMConfigAPI.cs b/dotnet/src/AutoGen/API/LLMConfigAPI.cs new file mode 100644 index 00000000000..5154f3dd5f5 --- /dev/null +++ b/dotnet/src/AutoGen/API/LLMConfigAPI.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// LLMConfigAPI.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoGen.OpenAI; + +namespace AutoGen +{ + public static class LLMConfigAPI + { + public static IEnumerable GetOpenAIConfigList( + string apiKey, + IEnumerable? modelIDs = null) + { + var models = modelIDs ?? new[] + { + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-4", + "gpt-4-32k", + "gpt-4-0613", + "gpt-4-32k-0613", + "gpt-4-1106-preview", + }; + + return models.Select(modelId => new OpenAIConfig(apiKey, modelId)); + } + + public static IEnumerable GetAzureOpenAIConfigList( + string endpoint, + string apiKey, + IEnumerable deploymentNames) + { + return deploymentNames.Select(deploymentName => new AzureOpenAIConfig(endpoint, deploymentName, apiKey)); + } + + /// + /// Get a list of LLMConfig objects from a JSON file. + /// + internal static IEnumerable ConfigListFromJson( + string filePath, + IEnumerable? filterModels = null) + { + // Disable this API from documentation for now. + throw new NotImplementedException(); + } + } +} diff --git a/dotnet/src/AutoGen/Agent/AssistantAgent.cs b/dotnet/src/AutoGen/Agent/AssistantAgent.cs new file mode 100644 index 00000000000..06f65042add --- /dev/null +++ b/dotnet/src/AutoGen/Agent/AssistantAgent.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AssistantAgent.cs + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen; + +public class AssistantAgent : ConversableAgent +{ + public AssistantAgent( + string name, + string systemMessage = "You are a helpful AI assistant", + ConversableAgentConfig? llmConfig = null, + Func, CancellationToken, Task>? isTermination = null, + HumanInputMode humanInputMode = HumanInputMode.NEVER, + IDictionary>>? functionMap = null, + string? defaultReply = null) + : base(name: name, + systemMessage: systemMessage, + llmConfig: llmConfig, + isTermination: isTermination, + humanInputMode: humanInputMode, + functionMap: functionMap, + defaultReply: defaultReply) + { + } +} diff --git a/dotnet/src/AutoGen/Agent/ConversableAgent.cs b/dotnet/src/AutoGen/Agent/ConversableAgent.cs new file mode 100644 index 00000000000..e70a74a801c --- /dev/null +++ b/dotnet/src/AutoGen/Agent/ConversableAgent.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ConversableAgent.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoGen.OpenAI; + +namespace AutoGen; + +public enum HumanInputMode +{ + /// + /// NEVER prompt the user for input + /// + NEVER = 0, + + /// + /// ALWAYS prompt the user for input + /// + ALWAYS = 1, + + /// + /// prompt the user for input if the message is not a termination message + /// + AUTO = 2, +} + +public class ConversableAgent : IAgent +{ + private readonly IAgent? innerAgent; + private readonly string? defaultReply; + private readonly HumanInputMode humanInputMode; + private readonly IDictionary>>? functionMap; + private readonly string systemMessage; + private readonly IEnumerable? functions; + + public ConversableAgent( + string name, + string systemMessage = "You are a helpful AI assistant", + IAgent? innerAgent = null, + string? defaultAutoReply = null, + HumanInputMode humanInputMode = HumanInputMode.NEVER, + Func, CancellationToken, Task>? isTermination = null, + IDictionary>>? functionMap = null) + { + this.Name = name; + this.defaultReply = defaultAutoReply; + this.functionMap = functionMap; + this.humanInputMode = humanInputMode; + this.innerAgent = innerAgent; + this.IsTermination = isTermination; + this.systemMessage = systemMessage; + } + + public ConversableAgent( + string name, + string systemMessage = "You are a helpful AI assistant", + ConversableAgentConfig? llmConfig = null, + Func, CancellationToken, Task>? isTermination = null, + HumanInputMode humanInputMode = HumanInputMode.AUTO, + IDictionary>>? functionMap = null, + string? defaultReply = null) + { + this.Name = name; + this.defaultReply = defaultReply; + this.functionMap = functionMap; + this.humanInputMode = humanInputMode; + this.IsTermination = isTermination; + this.systemMessage = systemMessage; + this.innerAgent = llmConfig?.ConfigList != null ? this.CreateInnerAgentFromConfigList(llmConfig) : null; + this.functions = llmConfig?.FunctionContracts; + } + + private IAgent? CreateInnerAgentFromConfigList(ConversableAgentConfig config) + { + IAgent? agent = null; + foreach (var llmConfig in config.ConfigList ?? Enumerable.Empty()) + { + agent = agent switch + { + null => llmConfig switch + { + AzureOpenAIConfig azureConfig => new GPTAgent(this.Name!, this.systemMessage, azureConfig, temperature: config.Temperature ?? 0), + OpenAIConfig openAIConfig => new GPTAgent(this.Name!, this.systemMessage, openAIConfig, temperature: config.Temperature ?? 0), + _ => throw new ArgumentException($"Unsupported config type {llmConfig.GetType()}"), + }, + IAgent innerAgent => innerAgent.RegisterReply(async (messages, cancellationToken) => + { + return await innerAgent.GenerateReplyAsync(messages, cancellationToken: cancellationToken); + }), + }; + } + + return agent; + } + + public string Name { get; } + + public Func, CancellationToken, Task>? IsTermination { get; } + + public async Task GenerateReplyAsync( + IEnumerable messages, + GenerateReplyOptions? overrideOptions = null, + CancellationToken cancellationToken = default) + { + // if there's no system message, add system message to the first of chat history + if (!messages.Any(m => m.IsSystemMessage())) + { + var systemMessage = new TextMessage(Role.System, this.systemMessage, from: this.Name); + messages = new[] { systemMessage }.Concat(messages); + } + + // process order: function_call -> human_input -> inner_agent -> default_reply -> self_execute + // first in, last out + + // process default reply + MiddlewareAgent agent; + if (this.innerAgent != null) + { + agent = innerAgent.RegisterMiddleware(async (msgs, option, agent, ct) => + { + var updatedMessages = msgs.Select(m => + { + if (m.From == this.Name) + { + m.From = this.innerAgent.Name; + return m; + } + else + { + return m; + } + }); + + return await agent.GenerateReplyAsync(updatedMessages, option, ct); + }); + } + else + { + agent = new MiddlewareAgent(new DefaultReplyAgent(this.Name!, this.defaultReply ?? "Default reply is not set. Please pass a default reply to assistant agent")); + } + + // process human input + var humanInputMiddleware = new HumanInputMiddleware(mode: this.humanInputMode, isTermination: this.IsTermination); + agent.Use(humanInputMiddleware); + + // process function call + var functionCallMiddleware = new FunctionCallMiddleware(functions: this.functions, functionMap: this.functionMap); + agent.Use(functionCallMiddleware); + + return await agent.GenerateReplyAsync(messages, overrideOptions, cancellationToken); + } +} diff --git a/dotnet/src/AutoGen/Agent/UserProxyAgent.cs b/dotnet/src/AutoGen/Agent/UserProxyAgent.cs new file mode 100644 index 00000000000..a48f07006b8 --- /dev/null +++ b/dotnet/src/AutoGen/Agent/UserProxyAgent.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// UserProxyAgent.cs + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen; + +public class UserProxyAgent : ConversableAgent +{ + public UserProxyAgent( + string name, + string systemMessage = "You are a helpful AI assistant", + ConversableAgentConfig? llmConfig = null, + Func, CancellationToken, Task>? isTermination = null, + HumanInputMode humanInputMode = HumanInputMode.ALWAYS, + IDictionary>>? functionMap = null, + string? defaultReply = null) + : base(name: name, + systemMessage: systemMessage, + llmConfig: llmConfig, + isTermination: isTermination, + humanInputMode: humanInputMode, + functionMap: functionMap, + defaultReply: defaultReply) + { + } +} diff --git a/dotnet/src/AutoGen/AutoGen.csproj b/dotnet/src/AutoGen/AutoGen.csproj new file mode 100644 index 00000000000..4d0d791eb8b --- /dev/null +++ b/dotnet/src/AutoGen/AutoGen.csproj @@ -0,0 +1,31 @@ + + + netstandard2.0 + AutoGen + + + + + + + AutoGen + + The all-in-one package for AutoGen. This package provides contracts, core functionalities, OpenAI integration, source generator, etc. for AutoGen. + + + + + + + + + + + + + + + + + + diff --git a/dotnet/src/AutoGen/ConversableAgentConfig.cs b/dotnet/src/AutoGen/ConversableAgentConfig.cs new file mode 100644 index 00000000000..50a83ba8620 --- /dev/null +++ b/dotnet/src/AutoGen/ConversableAgentConfig.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ConversableAgentConfig.cs + +using System.Collections.Generic; + +namespace AutoGen; + +public class ConversableAgentConfig +{ + public IEnumerable? FunctionContracts { get; set; } + + public IEnumerable? ConfigList { get; set; } + + public float? Temperature { get; set; } = 0.7f; + + public int? Timeout { get; set; } +} diff --git a/dotnet/src/AutoGen/GlobalUsing.cs b/dotnet/src/AutoGen/GlobalUsing.cs new file mode 100644 index 00000000000..d66bf001ed5 --- /dev/null +++ b/dotnet/src/AutoGen/GlobalUsing.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GlobalUsing.cs + +global using AutoGen.Core; diff --git a/dotnet/src/AutoGen/Middleware/HumanInputMiddleware.cs b/dotnet/src/AutoGen/Middleware/HumanInputMiddleware.cs new file mode 100644 index 00000000000..1a742b11c79 --- /dev/null +++ b/dotnet/src/AutoGen/Middleware/HumanInputMiddleware.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// HumanInputMiddleware.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen; + +/// +/// the middleware to get human input +/// +public class HumanInputMiddleware : IMiddleware +{ + private readonly HumanInputMode mode; + private readonly string prompt; + private readonly string exitKeyword; + private Func, CancellationToken, Task> isTermination; + private Func getInput = Console.ReadLine; + private Action writeLine = Console.WriteLine; + public string? Name => nameof(HumanInputMiddleware); + + public HumanInputMiddleware( + string prompt = "Please give feedback: Press enter or type 'exit' to stop the conversation.", + string exitKeyword = "exit", + HumanInputMode mode = HumanInputMode.AUTO, + Func, CancellationToken, Task>? isTermination = null, + Func? getInput = null, + Action? writeLine = null) + { + this.prompt = prompt; + this.isTermination = isTermination ?? DefaultIsTermination; + this.exitKeyword = exitKeyword; + this.mode = mode; + this.getInput = getInput ?? GetInput; + this.writeLine = writeLine ?? WriteLine; + } + + public async Task InvokeAsync(MiddlewareContext context, IAgent agent, CancellationToken cancellationToken = default) + { + // if the mode is never, then just return the input message + if (mode == HumanInputMode.NEVER) + { + return await agent.GenerateReplyAsync(context.Messages, context.Options, cancellationToken); + } + + // if the mode is always, then prompt the user for input + if (mode == HumanInputMode.ALWAYS) + { + this.writeLine(prompt); + var input = getInput(); + if (input == exitKeyword) + { + return new TextMessage(Role.Assistant, GroupChatExtension.TERMINATE, agent.Name); + } + + return new TextMessage(Role.Assistant, input, agent.Name); + } + + // if the mode is auto, then prompt the user for input if the message is not a termination message + if (mode == HumanInputMode.AUTO) + { + if (await isTermination(context.Messages, cancellationToken) is false) + { + return await agent.GenerateReplyAsync(context.Messages, context.Options, cancellationToken); + } + + this.writeLine(prompt); + var input = getInput(); + if (input == exitKeyword) + { + return new TextMessage(Role.Assistant, GroupChatExtension.TERMINATE, agent.Name); + } + + return new TextMessage(Role.Assistant, input, agent.Name); + } + + throw new InvalidOperationException("Invalid mode"); + } + + private async Task DefaultIsTermination(IEnumerable messages, CancellationToken _) + { + return messages?.Last().IsGroupChatTerminateMessage() is true; + } + + private string GetInput() + { + return Console.ReadLine(); + } + + private void WriteLine(string message) + { + Console.WriteLine(message); + } +} diff --git a/dotnet/test/.editorconfig b/dotnet/test/.editorconfig new file mode 100644 index 00000000000..cc0410613c4 --- /dev/null +++ b/dotnet/test/.editorconfig @@ -0,0 +1,7 @@ +# Suppressing errors for Test projects under test folder +[*.cs] +dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task +dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave +dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1998.severity = none # Async method lacks 'await' operators and will run synchronously +dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations \ No newline at end of file diff --git a/dotnet/test/AutoGen.Mistral.Tests/AutoGen.Mistral.Tests.csproj b/dotnet/test/AutoGen.Mistral.Tests/AutoGen.Mistral.Tests.csproj new file mode 100644 index 00000000000..eff70486928 --- /dev/null +++ b/dotnet/test/AutoGen.Mistral.Tests/AutoGen.Mistral.Tests.csproj @@ -0,0 +1,26 @@ + + + + $(TestTargetFramework) + enable + false + True + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/test/AutoGen.Mistral.Tests/MistralClientAgentTests.cs b/dotnet/test/AutoGen.Mistral.Tests/MistralClientAgentTests.cs new file mode 100644 index 00000000000..110e81fdb21 --- /dev/null +++ b/dotnet/test/AutoGen.Mistral.Tests/MistralClientAgentTests.cs @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MistralClientAgentTests.cs + +using System.Text.Json; +using AutoGen.Core; +using AutoGen.Mistral.Extension; +using AutoGen.Tests; +using FluentAssertions; +using Xunit.Abstractions; + +namespace AutoGen.Mistral.Tests; + +public partial class MistralClientAgentTests +{ + private ITestOutputHelper _output; + + public MistralClientAgentTests(ITestOutputHelper output) + { + _output = output; + } + + [Function] + public async Task GetWeather(string city) + { + return $"The weather in {city} is sunny."; + } + + [ApiKeyFact("MISTRAL_API_KEY")] + public async Task MistralAgentChatCompletionTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new InvalidOperationException("MISTRAL_API_KEY is not set."); + var client = new MistralClient(apiKey: apiKey); + + var agent = new MistralClientAgent( + client: client, + name: "MistralClientAgent", + model: "open-mistral-7b") + .RegisterMessageConnector(); + var singleAgentTest = new SingleAgentTest(_output); + await singleAgentTest.UpperCaseTest(agent); + await singleAgentTest.UpperCaseStreamingTestAsync(agent); + } + + [ApiKeyFact("MISTRAL_API_KEY")] + public async Task MistralAgentJsonModeTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new InvalidOperationException("MISTRAL_API_KEY is not set."); + var client = new MistralClient(apiKey: apiKey); + + var agent = new MistralClientAgent( + client: client, + name: "MistralClientAgent", + jsonOutput: true, + systemMessage: "You are a helpful assistant that convert input to json object", + model: "open-mistral-7b", + randomSeed: 0) + .RegisterMessageConnector(); + + var reply = await agent.SendAsync("name: John, age: 41, email: g123456@gmail.com"); + reply.Should().BeOfType(); + reply.GetContent().Should().NotBeNullOrEmpty(); + reply.From.Should().Be(agent.Name); + var json = reply.GetContent(); + var person = JsonSerializer.Deserialize(json!); + + person.Should().NotBeNull(); + person!.Name.Should().Be("John"); + person!.Age.Should().Be(41); + person!.Email.Should().Be("g123456@gmail.com"); + } + + [ApiKeyFact("MISTRAL_API_KEY")] + public async Task MistralAgentFunctionCallMessageTest() + { + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new InvalidOperationException("MISTRAL_API_KEY is not set."); + var client = new MistralClient(apiKey: apiKey); + var agent = new MistralClientAgent( + client: client, + name: "MistralClientAgent", + model: "mistral-small-latest", + randomSeed: 0) + .RegisterMessageConnector(); + + var weatherFunctionArgumets = """ + { + "city": "Seattle" + } + """; + var functionCallResult = await this.GetWeatherWrapper(weatherFunctionArgumets); + + IMessage[] chatHistory = [ + new TextMessage(Role.User, "what's the weather in Seattle?"), + new ToolCallMessage(this.GetWeatherFunctionContract.Name!, weatherFunctionArgumets, from: agent.Name), + new ToolCallResultMessage(functionCallResult, this.GetWeatherFunctionContract.Name!, weatherFunctionArgumets), + ]; + + var reply = await agent.SendAsync(chatHistory: chatHistory); + + reply.Should().BeOfType(); + reply.GetContent().Should().Be("The weather in Seattle is sunny."); + } + + [ApiKeyFact("MISTRAL_API_KEY")] + public async Task MistralAgentTwoAgentFunctionCallTest() + { + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new InvalidOperationException("MISTRAL_API_KEY is not set."); + var client = new MistralClient(apiKey: apiKey); + var twoAgentTest = new TwoAgentTest(_output); + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [twoAgentTest.GetWeatherFunctionContract]); + var functionCallAgent = new MistralClientAgent( + client: client, + name: "MistralClientAgent", + model: "mistral-small-latest", + randomSeed: 0) + .RegisterMessageConnector() + .RegisterMiddleware(functionCallMiddleware); + + var functionCallMiddlewareExecutorMiddleware = new FunctionCallMiddleware( + functionMap: new Dictionary>> + { + { twoAgentTest.GetWeatherFunctionContract.Name!, twoAgentTest.GetWeatherWrapper } + }); + var executorAgent = new MistralClientAgent( + client: client, + name: "ExecutorAgent", + model: "mistral-small-latest", + randomSeed: 0) + .RegisterMessageConnector() + .RegisterMiddleware(functionCallMiddlewareExecutorMiddleware); + await twoAgentTest.TwoAgentGetWeatherFunctionCallTestAsync(executorAgent, functionCallAgent); + } + + [ApiKeyFact("MISTRAL_API_KEY")] + public async Task MistralAgentFunctionCallMiddlewareMessageTest() + { + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new InvalidOperationException("MISTRAL_API_KEY is not set."); + var client = new MistralClient(apiKey: apiKey); + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [this.GetWeatherFunctionContract], + functionMap: new Dictionary>> + { + { this.GetWeatherFunctionContract.Name!, this.GetWeatherWrapper } + }); + var functionCallAgent = new MistralClientAgent( + client: client, + name: "MistralClientAgent", + model: "mistral-small-latest", + randomSeed: 0) + .RegisterMessageConnector() + .RegisterMiddleware(functionCallMiddleware); + + var question = new TextMessage(Role.User, "what's the weather in Seattle?"); + var reply = await functionCallAgent.SendAsync(question); + reply.Should().BeOfType>(); + + // resend the reply to the same agent so it can generate the final response + // because the reply's from is the agent's name + // in this case, the aggregate message will be converted to tool call message + tool call result message + var finalReply = await functionCallAgent.SendAsync(chatHistory: [question, reply]); + finalReply.Should().BeOfType(); + finalReply.GetContent().Should().Be("The weather in Seattle is sunny."); + + var anotherAgent = new MistralClientAgent( + client: client, + name: "AnotherMistralClientAgent", + model: "mistral-small-latest", + randomSeed: 0) + .RegisterMessageConnector(); + + // if send the reply to another agent with different name, + // the reply will be interpreted as a plain text message + var plainTextReply = await anotherAgent.SendAsync(chatHistory: [reply, question]); + plainTextReply.Should().BeOfType(); + } + + [ApiKeyFact("MISTRAL_API_KEY")] + public async Task MistralAgentFunctionCallAutoInvokeTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new InvalidOperationException("MISTRAL_API_KEY is not set."); + var client = new MistralClient(apiKey: apiKey); + var singleAgentTest = new SingleAgentTest(_output); + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [singleAgentTest.EchoAsyncFunctionContract], + functionMap: new Dictionary>> + { + { singleAgentTest.EchoAsyncFunctionContract.Name!, singleAgentTest.EchoAsyncWrapper } + }); + var agent = new MistralClientAgent( + client: client, + name: "MistralClientAgent", + model: "mistral-small-latest", + toolChoice: ToolChoiceEnum.Any, + randomSeed: 0) + .RegisterMessageConnector() + .RegisterMiddleware(functionCallMiddleware); + await singleAgentTest.EchoFunctionCallExecutionTestAsync(agent); + await singleAgentTest.EchoFunctionCallExecutionStreamingTestAsync(agent); + } + + [ApiKeyFact("MISTRAL_API_KEY")] + public async Task MistralAgentFunctionCallTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new InvalidOperationException("MISTRAL_API_KEY is not set."); + var client = new MistralClient(apiKey: apiKey); + var singleAgentTest = new SingleAgentTest(_output); + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [singleAgentTest.EchoAsyncFunctionContract, this.GetWeatherFunctionContract]); + var agent = new MistralClientAgent( + client: client, + name: "MistralClientAgent", + model: "mistral-small-latest", + toolChoice: ToolChoiceEnum.Any, + systemMessage: "You are a helpful assistant that can call functions", + randomSeed: 0) + .RegisterMessageConnector() + .RegisterMiddleware(functionCallMiddleware); + await singleAgentTest.EchoFunctionCallTestAsync(agent); + + + // streaming test + var question = new TextMessage(Role.User, "what's the weather in Seattle?"); + IMessage? finalReply = null; + + await foreach (var reply in await agent.GenerateStreamingReplyAsync([question])) + { + reply.From.Should().Be(agent.Name); + if (reply is IMessage message) + { + finalReply = message; + } + } + + finalReply.Should().NotBeNull(); + finalReply.Should().BeOfType(); + } +} diff --git a/dotnet/test/AutoGen.Mistral.Tests/MistralClientTests.cs b/dotnet/test/AutoGen.Mistral.Tests/MistralClientTests.cs new file mode 100644 index 00000000000..bd285adf673 --- /dev/null +++ b/dotnet/test/AutoGen.Mistral.Tests/MistralClientTests.cs @@ -0,0 +1,287 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MistralClientTests.cs + +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoGen.Core; +using AutoGen.Mistral.Extension; +using AutoGen.Tests; +using FluentAssertions; + +namespace AutoGen.Mistral.Tests; + +public partial class MistralClientTests +{ + [Function] + public async Task GetWeather(string city) + { + return $"The weather in {city} is sunny."; + } + + [ApiKeyFact("MISTRAL_API_KEY")] + public async Task MistralClientChatCompletionTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new InvalidOperationException("MISTRAL_API_KEY is not set."); + var client = new MistralClient(apiKey: apiKey); + + var systemMessage = new ChatMessage(ChatMessage.RoleEnum.System, "You are a helpful assistant."); + var userMessage = new ChatMessage(ChatMessage.RoleEnum.User, "What is the weather like today?"); + + var request = new ChatCompletionRequest( + model: "open-mistral-7b", + messages: new List { systemMessage, userMessage }, + temperature: 0); + + var response = await client.CreateChatCompletionsAsync(request); + + response.Choices!.Count().Should().Be(1); + response.Choices!.First().Message!.Content.Should().NotBeNullOrEmpty(); + response.Choices!.First().Message!.Role.Should().Be(ChatMessage.RoleEnum.Assistant); + response.Usage!.TotalTokens.Should().BeGreaterThan(0); + } + + [ApiKeyFact("MISTRAL_API_KEY")] + public async Task MistralClientStreamingChatCompletionTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new InvalidOperationException("MISTRAL_API_KEY is not set."); + var client = new MistralClient(apiKey: apiKey); + + var systemMessage = new ChatMessage(ChatMessage.RoleEnum.System, "You are a helpful assistant."); + var userMessage = new ChatMessage(ChatMessage.RoleEnum.User, "What is the weather like today?"); + + var request = new ChatCompletionRequest( + model: "open-mistral-7b", + messages: new List { systemMessage, userMessage }, + temperature: 0); + + var response = client.StreamingChatCompletionsAsync(request); + var results = new List(); + + await foreach (var item in response) + { + results.Add(item); + item.VarObject.Should().Be("chat.completion.chunk"); + } + + results.Count.Should().BeGreaterThan(0); + + // merge result + var finalResult = results.First(); + foreach (var result in results) + { + if (finalResult.Choices!.First().Message is null) + { + finalResult.Choices!.First().Message = result.Choices!.First().Delta; + } + else + { + finalResult.Choices!.First().Message!.Content += result.Choices!.First().Delta!.Content; + } + + // the usage information will be included in the last result + if (result.Usage != null) + { + finalResult.Usage = result.Usage; + } + } + finalResult.Choices!.First().Message!.Content.Should().NotBeNullOrEmpty(); + finalResult.Choices!.First().Message!.Role.Should().Be(ChatMessage.RoleEnum.Assistant); + finalResult.Usage!.TotalTokens.Should().BeGreaterThan(0); + } + + [ApiKeyFact("MISTRAL_API_KEY")] + public async Task MistralClientStreamingChatJsonModeCompletionTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new InvalidOperationException("MISTRAL_API_KEY is not set."); + var client = new MistralClient(apiKey: apiKey); + + var systemMessage = new ChatMessage(ChatMessage.RoleEnum.System, "You are a helpful assistant that convert input to json object"); + var userMessage = new ChatMessage(ChatMessage.RoleEnum.User, "name: John, age: 41, email: g123456@gmail.com"); + + var request = new ChatCompletionRequest( + model: "open-mistral-7b", + messages: new List { systemMessage, userMessage }, + temperature: 0) + { + ResponseFormat = new ResponseFormat { ResponseFormatType = "json_object" }, + }; + + var response = client.StreamingChatCompletionsAsync(request); + var results = new List(); + + await foreach (var item in response) + { + results.Add(item); + item.VarObject.Should().Be("chat.completion.chunk"); + } + + results.Count.Should().BeGreaterThan(0); + + // merge result + var finalResult = results.First(); + foreach (var result in results) + { + if (finalResult.Choices!.First().Message is null) + { + finalResult.Choices!.First().Message = result.Choices!.First().Delta; + } + else + { + finalResult.Choices!.First().Message!.Content += result.Choices!.First().Delta!.Content; + } + + // the usage information will be included in the last result + if (result.Usage != null) + { + finalResult.Usage = result.Usage; + } + } + + finalResult.Choices!.First().Message!.Content.Should().NotBeNullOrEmpty(); + finalResult.Choices!.First().Message!.Role.Should().Be(ChatMessage.RoleEnum.Assistant); + finalResult.Usage!.TotalTokens.Should().BeGreaterThan(0); + var responseContent = finalResult.Choices!.First().Message!.Content ?? throw new InvalidOperationException("Response content is null."); + var person = JsonSerializer.Deserialize(responseContent); + person.Should().NotBeNull(); + + person!.Name.Should().Be("John"); + person!.Age.Should().Be(41); + person!.Email.Should().Be("g123456@gmail.com"); + } + + [ApiKeyFact("MISTRAL_API_KEY")] + public async Task MistralClientJsonModeTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new InvalidOperationException("MISTRAL_API_KEY is not set."); + var client = new MistralClient(apiKey: apiKey); + + var systemMessage = new ChatMessage(ChatMessage.RoleEnum.System, "You are a helpful assistant that convert input to json object"); + var userMessage = new ChatMessage(ChatMessage.RoleEnum.User, "name: John, age: 41, email: g123456@gmail.com"); + + var request = new ChatCompletionRequest( + model: "open-mistral-7b", + messages: new List { systemMessage, userMessage }, + temperature: 0) + { + ResponseFormat = new ResponseFormat { ResponseFormatType = "json_object" }, + }; + + var response = await client.CreateChatCompletionsAsync(request); + + response.Choices!.Count().Should().Be(1); + response.Choices!.First().Message!.Content.Should().NotBeNullOrEmpty(); + response.Choices!.First().Message!.Role.Should().Be(ChatMessage.RoleEnum.Assistant); + response.Usage!.TotalTokens.Should().BeGreaterThan(0); + + // check if the response is a valid json object + var responseContent = response.Choices!.First().Message!.Content ?? throw new InvalidOperationException("Response content is null."); + var person = JsonSerializer.Deserialize(responseContent); + person.Should().NotBeNull(); + + person!.Name.Should().Be("John"); + person!.Age.Should().Be(41); + person!.Email.Should().Be("g123456@gmail.com"); + } + + + [ApiKeyFact("MISTRAL_API_KEY")] + public async Task MistralClientFunctionCallTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new InvalidOperationException("MISTRAL_API_KEY is not set."); + using var client = new MistralClient(apiKey: apiKey); + + var getWeatherFunctionContract = this.GetWeatherFunctionContract; + var functionDefinition = getWeatherFunctionContract.ToMistralFunctionDefinition(); + + var systemMessage = new ChatMessage(ChatMessage.RoleEnum.System, "You are a helpful assistant."); + var userMessage = new ChatMessage(ChatMessage.RoleEnum.User, "What is the weather in Seattle?"); + + var request = new ChatCompletionRequest( + model: "mistral-small-latest", // only large or small latest models support function calls + messages: new List { systemMessage, userMessage }, + temperature: 0) + { + Tools = [new FunctionTool(functionDefinition)], + ToolChoice = ToolChoiceEnum.Any, + }; + + var response = await client.CreateChatCompletionsAsync(request); + + response.Choices!.Count().Should().Be(1); + response.Choices!.First().Message!.Content.Should().BeNullOrEmpty(); + response.Choices!.First().FinishReason.Should().Be(Choice.FinishReasonEnum.ToolCalls); + response.Choices!.First().Message!.ToolCalls!.Count.Should().Be(1); + response.Choices!.First().Message!.ToolCalls!.First().Function.Name.Should().Be("GetWeather"); + } + + [ApiKeyFact("MISTRAL_API_KEY")] + public async Task MistralClientStreamingFunctionCallTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new InvalidOperationException("MISTRAL_API_KEY is not set."); + using var client = new MistralClient(apiKey: apiKey); + + var getWeatherFunctionContract = this.GetWeatherFunctionContract; + var functionDefinition = getWeatherFunctionContract.ToMistralFunctionDefinition(); + + var systemMessage = new ChatMessage(ChatMessage.RoleEnum.System, "You are a helpful assistant."); + var userMessage = new ChatMessage(ChatMessage.RoleEnum.User, "What is the weather in Seattle?"); + + var request = new ChatCompletionRequest( + model: "mistral-small-latest", + messages: new List { systemMessage, userMessage }, + temperature: 0) + { + Tools = [new FunctionTool(functionDefinition)], + ToolChoice = ToolChoiceEnum.Any, + }; + + var response = client.StreamingChatCompletionsAsync(request); + + var results = new List(); + await foreach (var item in response) + { + results.Add(item); + item.VarObject.Should().Be("chat.completion.chunk"); + } + + // merge result + var finalResult = results.First(); + var lastResult = results.Last(); + lastResult.Choices!.First().FinishReason.Should().Be(Choice.FinishReasonEnum.ToolCalls); + + foreach (var result in results) + { + if (finalResult.Choices!.First().Message is null) + { + finalResult.Choices!.First().Message = result.Choices!.First().Delta; + finalResult.Choices!.First().Message!.ToolCalls = []; + } + else + { + finalResult.Choices!.First().Message!.ToolCalls = finalResult.Choices!.First().Message!.ToolCalls!.Concat(result.Choices!.First().Delta!.ToolCalls!).ToList(); + } + + // the usage information will be included in the last result + if (result.Usage != null) + { + finalResult.Usage = result.Usage; + } + } + + finalResult.Choices!.First().Message!.Content.Should().BeNullOrEmpty(); + finalResult.Choices!.First().Message!.ToolCalls!.Count.Should().BeGreaterThan(0); + finalResult.Usage!.TotalTokens.Should().BeGreaterThan(0); + finalResult.Choices!.First().Message!.ToolCalls!.First().Function.Name.Should().Be("GetWeather"); + } +} +public class Person +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("age")] + public int Age { get; set; } + + [JsonPropertyName("email")] + public string Email { get; set; } = string.Empty; +} diff --git a/dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionExample.Add_Test.approved.txt b/dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionExample.Add_Test.approved.txt new file mode 100644 index 00000000000..9075d35b957 --- /dev/null +++ b/dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionExample.Add_Test.approved.txt @@ -0,0 +1,21 @@ +{ + "name": "Add", + "description": "Add function", + "parameters": { + "type": "object", + "properties": { + "a": { + "type": "integer", + "description": "a" + }, + "b": { + "type": "integer", + "description": "b" + } + }, + "required": [ + "a", + "b" + ] + } +} \ No newline at end of file diff --git a/dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionExample.DictionaryToString_Test.approved.txt b/dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionExample.DictionaryToString_Test.approved.txt new file mode 100644 index 00000000000..8b6aad2fcda --- /dev/null +++ b/dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionExample.DictionaryToString_Test.approved.txt @@ -0,0 +1,19 @@ +{ + "name": "DictionaryToStringAsync", + "description": "DictionaryToString function", + "parameters": { + "type": "object", + "properties": { + "xargs": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "an object of key-value pairs. key is string, value is string" + } + }, + "required": [ + "xargs" + ] + } +} \ No newline at end of file diff --git a/dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionExample.Query_Test.approved.txt b/dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionExample.Query_Test.approved.txt new file mode 100644 index 00000000000..6d16b5a91c0 --- /dev/null +++ b/dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionExample.Query_Test.approved.txt @@ -0,0 +1,24 @@ +{ + "name": "Query", + "description": "query function", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "query, required" + }, + "k": { + "type": "integer", + "description": "top k, optional, default value is 3" + }, + "thresold": { + "type": "number", + "description": "thresold, optional, default value is 0.5" + } + }, + "required": [ + "query" + ] + } +} \ No newline at end of file diff --git a/dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionExample.Sum_Test.approved.txt b/dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionExample.Sum_Test.approved.txt new file mode 100644 index 00000000000..ce86faf6a64 --- /dev/null +++ b/dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionExample.Sum_Test.approved.txt @@ -0,0 +1,19 @@ +{ + "name": "Sum", + "description": "Sum function", + "parameters": { + "type": "object", + "properties": { + "args": { + "type": "array", + "items": { + "type": "number" + }, + "description": "an array of double values" + } + }, + "required": [ + "args" + ] + } +} \ No newline at end of file diff --git a/dotnet/test/AutoGen.SourceGenerator.Tests/AutoGen.SourceGenerator.Tests.csproj b/dotnet/test/AutoGen.SourceGenerator.Tests/AutoGen.SourceGenerator.Tests.csproj new file mode 100644 index 00000000000..c4b2a8aa8ce --- /dev/null +++ b/dotnet/test/AutoGen.SourceGenerator.Tests/AutoGen.SourceGenerator.Tests.csproj @@ -0,0 +1,24 @@ + + + + $(TestTargetFramework) + enable + false + True + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/test/AutoGen.SourceGenerator.Tests/FilescopeNamespaceFunctionExample.cs b/dotnet/test/AutoGen.SourceGenerator.Tests/FilescopeNamespaceFunctionExample.cs new file mode 100644 index 00000000000..8293b26c162 --- /dev/null +++ b/dotnet/test/AutoGen.SourceGenerator.Tests/FilescopeNamespaceFunctionExample.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// FilescopeNamespaceFunctionExample.cs + +using AutoGen.Core; + +namespace AutoGen.SourceGenerator.Tests; +public partial class FilescopeNamespaceFunctionExample +{ + [Function] + public Task Add(int a, int b) + { + return Task.FromResult($"{a + b}"); + } +} diff --git a/dotnet/test/AutoGen.SourceGenerator.Tests/FunctionExample.test.cs b/dotnet/test/AutoGen.SourceGenerator.Tests/FunctionExample.test.cs new file mode 100644 index 00000000000..f7b90e0b96f --- /dev/null +++ b/dotnet/test/AutoGen.SourceGenerator.Tests/FunctionExample.test.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// FunctionExample.test.cs + +using System.Text.Json; +using ApprovalTests; +using ApprovalTests.Namers; +using ApprovalTests.Reporters; +using Azure.AI.OpenAI; +using FluentAssertions; +using Xunit; + +namespace AutoGen.SourceGenerator.Tests +{ + public class FunctionExample + { + private readonly FunctionExamples functionExamples = new FunctionExamples(); + private readonly JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions + { + WriteIndented = true, + }; + + [Fact] + public void Add_Test() + { + var args = new + { + a = 1, + b = 2, + }; + + this.VerifyFunction(functionExamples.AddWrapper, args, 3); + this.VerifyFunctionDefinition(functionExamples.AddFunction); + } + + [Fact] + public void Sum_Test() + { + var args = new + { + args = new double[] { 1, 2, 3 }, + }; + + this.VerifyFunction(functionExamples.SumWrapper, args, 6.0); + this.VerifyFunctionDefinition(functionExamples.SumFunction); + } + + [Fact] + public async Task DictionaryToString_Test() + { + var args = new + { + xargs = new Dictionary + { + { "a", "1" }, + { "b", "2" }, + }, + }; + + await this.VerifyAsyncFunction(functionExamples.DictionaryToStringAsyncWrapper, args, JsonSerializer.Serialize(args.xargs, jsonSerializerOptions)); + this.VerifyFunctionDefinition(functionExamples.DictionaryToStringAsyncFunction); + } + + [Fact] + public async Task TopLevelFunctionExampleAddTestAsync() + { + var example = new TopLevelStatementFunctionExample(); + var args = new + { + a = 1, + b = 2, + }; + + await this.VerifyAsyncFunction(example.AddWrapper, args, "3"); + } + + [Fact] + public async Task FilescopeFunctionExampleAddTestAsync() + { + var example = new FilescopeNamespaceFunctionExample(); + var args = new + { + a = 1, + b = 2, + }; + + await this.VerifyAsyncFunction(example.AddWrapper, args, "3"); + } + + [Fact] + public void Query_Test() + { + var args = new + { + query = "hello", + k = 3, + }; + + this.VerifyFunction(functionExamples.QueryWrapper, args, new[] { "hello", "hello", "hello" }); + this.VerifyFunctionDefinition(functionExamples.QueryFunction); + } + + [UseReporter(typeof(DiffReporter))] + [UseApprovalSubdirectory("ApprovalTests")] + private void VerifyFunctionDefinition(FunctionDefinition function) + { + var func = new + { + name = function.Name, + description = function.Description.Replace(Environment.NewLine, ","), + parameters = function.Parameters.ToObjectFromJson(options: jsonSerializerOptions), + }; + + Approvals.Verify(JsonSerializer.Serialize(func, jsonSerializerOptions)); + } + + private void VerifyFunction(Func func, U args, T expected) + { + var str = JsonSerializer.Serialize(args, jsonSerializerOptions); + var res = func(str); + res.Should().BeEquivalentTo(expected); + } + + private async Task VerifyAsyncFunction(Func> func, U args, T expected) + { + var str = JsonSerializer.Serialize(args, jsonSerializerOptions); + var res = await func(str); + res.Should().BeEquivalentTo(expected); + } + } +} diff --git a/dotnet/test/AutoGen.SourceGenerator.Tests/FunctionExamples.cs b/dotnet/test/AutoGen.SourceGenerator.Tests/FunctionExamples.cs new file mode 100644 index 00000000000..d48906d2cd5 --- /dev/null +++ b/dotnet/test/AutoGen.SourceGenerator.Tests/FunctionExamples.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// FunctionExamples.cs + +using System.Text.Json; +using AutoGen.Core; + +namespace AutoGen.SourceGenerator.Tests +{ + public partial class FunctionExamples + { + /// + /// Add function + /// + /// a + /// b + [FunctionAttribute] + public int Add(int a, int b) + { + return a + b; + } + + /// + /// Add two numbers. + /// + /// The first number. + /// The second number. + [Function] + public Task AddAsync(int a, int b) + { + return Task.FromResult($"{a} + {b} = {a + b}"); + } + + /// + /// Sum function + /// + /// an array of double values + [FunctionAttribute] + public double Sum(double[] args) + { + return args.Sum(); + } + + /// + /// DictionaryToString function + /// + /// an object of key-value pairs. key is string, value is string + [FunctionAttribute] + public Task DictionaryToStringAsync(Dictionary xargs) + { + var res = JsonSerializer.Serialize(xargs, new JsonSerializerOptions + { + WriteIndented = true, + }); + + return Task.FromResult(res); + } + + /// + /// query function + /// + /// query, required + /// top k, optional, default value is 3 + /// thresold, optional, default value is 0.5 + [FunctionAttribute] + public string[] Query(string query, int k = 3, float thresold = 0.5f) + { + return Enumerable.Repeat(query, k).ToArray(); + } + } +} diff --git a/dotnet/test/AutoGen.SourceGenerator.Tests/TopLevelStatementFunctionExample.cs b/dotnet/test/AutoGen.SourceGenerator.Tests/TopLevelStatementFunctionExample.cs new file mode 100644 index 00000000000..0acaa46a3fa --- /dev/null +++ b/dotnet/test/AutoGen.SourceGenerator.Tests/TopLevelStatementFunctionExample.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// TopLevelStatementFunctionExample.cs + +using AutoGen.Core; + +public partial class TopLevelStatementFunctionExample +{ + [Function] + public Task Add(int a, int b) + { + return Task.FromResult($"{a + b}"); + } +} diff --git a/dotnet/test/AutoGen.Tests/ApprovalTests/OpenAIMessageTests.BasicMessageTest.approved.txt b/dotnet/test/AutoGen.Tests/ApprovalTests/OpenAIMessageTests.BasicMessageTest.approved.txt new file mode 100644 index 00000000000..2cb58f4d88c --- /dev/null +++ b/dotnet/test/AutoGen.Tests/ApprovalTests/OpenAIMessageTests.BasicMessageTest.approved.txt @@ -0,0 +1,219 @@ +[ + { + "OriginalMessage": "TextMessage(system, You are a helpful AI assistant, )", + "ConvertedMessages": [ + { + "Role": "system", + "Content": "You are a helpful AI assistant" + } + ] + }, + { + "OriginalMessage": "TextMessage(user, Hello, user)", + "ConvertedMessages": [ + { + "Role": "user", + "Content": "Hello", + "MultiModaItem": null + } + ] + }, + { + "OriginalMessage": "TextMessage(assistant, How can I help you?, assistant)", + "ConvertedMessages": [ + { + "Role": "assistant", + "Content": "How can I help you?", + "TooCall": [], + "FunctionCallName": null, + "FunctionCallArguments": null + } + ] + }, + { + "OriginalMessage": "Message(system, You are a helpful AI assistant, , , )", + "ConvertedMessages": [ + { + "Role": "system", + "Content": "You are a helpful AI assistant" + } + ] + }, + { + "OriginalMessage": "Message(user, Hello, user, , )", + "ConvertedMessages": [ + { + "Role": "user", + "Content": "Hello", + "MultiModaItem": null + } + ] + }, + { + "OriginalMessage": "Message(assistant, How can I help you?, assistant, , )", + "ConvertedMessages": [ + { + "Role": "assistant", + "Content": "How can I help you?", + "TooCall": [], + "FunctionCallName": null, + "FunctionCallArguments": null + } + ] + }, + { + "OriginalMessage": "Message(function, result, user, , )", + "ConvertedMessages": [ + { + "Role": "user", + "Content": "result", + "MultiModaItem": null + } + ] + }, + { + "OriginalMessage": "Message(assistant, , assistant, functionName, functionArguments)", + "ConvertedMessages": [ + { + "Role": "assistant", + "Content": null, + "TooCall": [], + "FunctionCallName": "functionName", + "FunctionCallArguments": "functionArguments" + } + ] + }, + { + "OriginalMessage": "ImageMessage(user, https://example.com/image.png, user)", + "ConvertedMessages": [ + { + "Role": "user", + "Content": null, + "MultiModaItem": [ + { + "Type": "Image", + "ImageUrl": { + "Url": "https://example.com/image.png", + "Detail": null + } + } + ] + } + ] + }, + { + "OriginalMessage": "MultiModalMessage(assistant, user)\n\tTextMessage(user, Hello, user)\n\tImageMessage(user, https://example.com/image.png, user)", + "ConvertedMessages": [ + { + "Role": "user", + "Content": null, + "MultiModaItem": [ + { + "Type": "Text", + "Text": "Hello" + }, + { + "Type": "Image", + "ImageUrl": { + "Url": "https://example.com/image.png", + "Detail": null + } + } + ] + } + ] + }, + { + "OriginalMessage": "ToolCallMessage(assistant)\n\tToolCall(test, test, )", + "ConvertedMessages": [ + { + "Role": "assistant", + "Content": "", + "TooCall": [ + { + "Type": "Function", + "Name": "test", + "Arguments": "test", + "Id": "test" + } + ], + "FunctionCallName": null, + "FunctionCallArguments": null + } + ] + }, + { + "OriginalMessage": "ToolCallResultMessage(user)\n\tToolCall(test, test, result)", + "ConvertedMessages": [ + { + "Role": "tool", + "Content": "result", + "ToolCallId": "test" + } + ] + }, + { + "OriginalMessage": "ToolCallResultMessage(user)\n\tToolCall(result, test, test)\n\tToolCall(result, test, test)", + "ConvertedMessages": [ + { + "Role": "tool", + "Content": "test", + "ToolCallId": "result" + }, + { + "Role": "tool", + "Content": "test", + "ToolCallId": "result" + } + ] + }, + { + "OriginalMessage": "ToolCallMessage(assistant)\n\tToolCall(test, test, )\n\tToolCall(test, test, )", + "ConvertedMessages": [ + { + "Role": "assistant", + "Content": "", + "TooCall": [ + { + "Type": "Function", + "Name": "test", + "Arguments": "test", + "Id": "test" + }, + { + "Type": "Function", + "Name": "test", + "Arguments": "test", + "Id": "test" + } + ], + "FunctionCallName": null, + "FunctionCallArguments": null + } + ] + }, + { + "OriginalMessage": "AggregateMessage(assistant)\n\tToolCallMessage(assistant)\n\tToolCall(test, test, )\n\tToolCallResultMessage(assistant)\n\tToolCall(test, test, result)", + "ConvertedMessages": [ + { + "Role": "assistant", + "Content": "", + "TooCall": [ + { + "Type": "Function", + "Name": "test", + "Arguments": "test", + "Id": "test" + } + ], + "FunctionCallName": null, + "FunctionCallArguments": null + }, + { + "Role": "tool", + "Content": "result", + "ToolCallId": "test" + } + ] + } +] \ No newline at end of file diff --git a/dotnet/test/AutoGen.Tests/Attribute/EnvironmentSpecificFactAttribute.cs b/dotnet/test/AutoGen.Tests/Attribute/EnvironmentSpecificFactAttribute.cs new file mode 100644 index 00000000000..1042dec6f27 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/Attribute/EnvironmentSpecificFactAttribute.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// EnvironmentSpecificFactAttribute.cs + +using System; +using Xunit; + +namespace AutoGen.Tests +{ + /// + /// A base class for environment-specific fact attributes. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public abstract class EnvironmentSpecificFactAttribute : FactAttribute + { + private readonly string _skipMessage; + + /// + /// Creates a new instance of the class. + /// + /// The message to be used when skipping the test marked with this attribute. + protected EnvironmentSpecificFactAttribute(string skipMessage) + { + _skipMessage = skipMessage ?? throw new ArgumentNullException(nameof(skipMessage)); + } + + public sealed override string Skip => IsEnvironmentSupported() ? string.Empty : _skipMessage; + + /// + /// A method used to evaluate whether to skip a test marked with this attribute. Skips iff this method evaluates to false. + /// + protected abstract bool IsEnvironmentSupported(); + } +} diff --git a/dotnet/test/AutoGen.Tests/Attribute/OpenAIFact.cs b/dotnet/test/AutoGen.Tests/Attribute/OpenAIFact.cs new file mode 100644 index 00000000000..44457d8f571 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/Attribute/OpenAIFact.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIFact.cs + +using System; +using System.Linq; + +namespace AutoGen.Tests +{ + /// + /// A fact for tests requiring OPENAI_API_KEY env. + /// + public sealed class ApiKeyFactAttribute : EnvironmentSpecificFactAttribute + { + private readonly string[] _envVariableNames; + public ApiKeyFactAttribute(params string[] envVariableNames) : base($"{envVariableNames} is not found in env") + { + _envVariableNames = envVariableNames; + } + + /// + protected override bool IsEnvironmentSupported() + { + return _envVariableNames.All(Environment.GetEnvironmentVariables().Contains); + } + } +} diff --git a/dotnet/test/AutoGen.Tests/AutoGen.Tests.csproj b/dotnet/test/AutoGen.Tests/AutoGen.Tests.csproj new file mode 100644 index 00000000000..f7e6b036506 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/AutoGen.Tests.csproj @@ -0,0 +1,24 @@ + + + + $(TestTargetFramework) + True + $(NoWarn);xUnit1013 + + + + + + + + + + + + + + + + + + diff --git a/dotnet/test/AutoGen.Tests/BasicSampleTest.cs b/dotnet/test/AutoGen.Tests/BasicSampleTest.cs new file mode 100644 index 00000000000..19de2bdef4b --- /dev/null +++ b/dotnet/test/AutoGen.Tests/BasicSampleTest.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// BasicSampleTest.cs + +using System; +using System.IO; +using System.Threading.Tasks; +using AutoGen.BasicSample; +using Xunit.Abstractions; + +namespace AutoGen.Tests +{ + public class BasicSampleTest + { + private readonly ITestOutputHelper _output; + + public BasicSampleTest(ITestOutputHelper output) + { + _output = output; + Console.SetOut(new ConsoleWriter(_output)); + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task AssistantAgentTestAsync() + { + await Example01_AssistantAgent.RunAsync(); + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task TwoAgentMathClassTestAsync() + { + await Example02_TwoAgent_MathChat.RunAsync(); + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task AgentFunctionCallTestAsync() + { + await Example03_Agent_FunctionCall.RunAsync(); + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task OpenAIAgent_JsonMode() + { + await Example13_OpenAIAgent_JsonMode.RunAsync(); + } + + [ApiKeyFact("MISTRAL_API_KEY")] + public async Task MistralClientAgent_TokenCount() + { + await Example14_MistralClientAgent_TokenCount.RunAsync(); + } + + [ApiKeyFact("OPENAI_API_KEY")] + public async Task DynamicGroupChatGetMLNetPRTestAsync() + { + await Example04_Dynamic_GroupChat_Coding_Task.RunAsync(); + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task DynamicGroupChatCalculateFibonacciAsync() + { + await Example07_Dynamic_GroupChat_Calculate_Fibonacci.RunAsync(); + await Example07_Dynamic_GroupChat_Calculate_Fibonacci.RunWorkflowAsync(); + } + + [ApiKeyFact("OPENAI_API_KEY")] + public async Task DalleAndGPT4VTestAsync() + { + await Example05_Dalle_And_GPT4V.RunAsync(); + } + + public class ConsoleWriter : StringWriter + { + private ITestOutputHelper output; + public ConsoleWriter(ITestOutputHelper output) + { + this.output = output; + } + + public override void WriteLine(string? m) + { + output.WriteLine(m); + } + } + } +} diff --git a/dotnet/test/AutoGen.Tests/EchoAgent.cs b/dotnet/test/AutoGen.Tests/EchoAgent.cs new file mode 100644 index 00000000000..28a7b91bad5 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/EchoAgent.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// EchoAgent.cs + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Tests +{ + internal class EchoAgent : IAgent + { + public EchoAgent(string name) + { + Name = name; + } + public string Name { get; } + + public Task GenerateReplyAsync( + IEnumerable conversation, + GenerateReplyOptions? options = null, + CancellationToken ct = default) + { + // return the most recent message + var lastMessage = conversation.Last(); + lastMessage.From = this.Name; + + return Task.FromResult(lastMessage); + } + } +} diff --git a/dotnet/test/AutoGen.Tests/GlobalUsing.cs b/dotnet/test/AutoGen.Tests/GlobalUsing.cs new file mode 100644 index 00000000000..d00ae3ce4fc --- /dev/null +++ b/dotnet/test/AutoGen.Tests/GlobalUsing.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// globalUsing.cs + +global using AutoGen.Core; diff --git a/dotnet/test/AutoGen.Tests/MathClassTest.cs b/dotnet/test/AutoGen.Tests/MathClassTest.cs new file mode 100644 index 00000000000..3f1eac76246 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/MathClassTest.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MathClassTest.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoGen.OpenAI; +using FluentAssertions; +using Xunit.Abstractions; + +namespace AutoGen.Tests +{ + public partial class MathClassTest + { + private readonly ITestOutputHelper _output; + public MathClassTest(ITestOutputHelper output) + { + _output = output; + } + + [FunctionAttribute] + public async Task CreateMathQuestion(string question, int question_index) + { + return $@"// ignore this line [MATH_QUESTION] +Question #{question_index}: +{question}"; + } + + [FunctionAttribute] + public async Task AnswerQuestion(string answer) + { + return $@"// ignore this line [MATH_ANSWER] +The answer is {answer}, teacher please check answer"; + } + + [FunctionAttribute] + public async Task AnswerIsCorrect(string message) + { + return $@"// ignore this line [ANSWER_IS_CORRECT] +{message}"; + } + + [FunctionAttribute] + public async Task UpdateProgress(int correctAnswerCount) + { + if (correctAnswerCount >= 5) + { + return $@"// ignore this line [UPDATE_PROGRESS] +{GroupChatExtension.TERMINATE}"; + } + else + { + return $@"// ignore this line [UPDATE_PROGRESS] +the number of resolved question is {correctAnswerCount} +teacher, please create the next math question"; + } + } + + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task AssistantAgentMathChatTestAsync() + { + var teacher = await CreateTeacherAssistantAgentAsync(); + var student = await CreateStudentAssistantAgentAsync(); + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new ArgumentException("AZURE_OPENAI_API_KEY is not set"); + var endPoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new ArgumentException("AZURE_OPENAI_ENDPOINT is not set"); + var model = "gpt-35-turbo-16k"; + var admin = new GPTAgent( + name: "Admin", + systemMessage: $@"You are admin. You ask teacher to create 5 math questions. You update progress after each question is answered.", + config: new AzureOpenAIConfig(endPoint, model, key), + functions: new[] + { + this.UpdateProgressFunction, + }, + functionMap: new Dictionary>> + { + { this.UpdateProgressFunction.Name, this.UpdateProgressWrapper }, + }) + .RegisterMiddleware(async (messages, options, agent, ct) => + { + // check admin reply to make sure it calls UpdateProgress function + var maxAttempt = 5; + var reply = await agent.GenerateReplyAsync(messages, options, ct); + while (maxAttempt-- > 0) + { + if (options?.Functions is { Length: 0 }) + { + return reply; + } + + var formattedMessage = reply.FormatMessage(); + this._output.WriteLine(formattedMessage); + if (reply.GetContent()?.Contains("[UPDATE_PROGRESS]") is true) + { + return reply; + } + else + { + await Task.Delay(1000); + var review = "Admin, please update progress based on conversation"; + reply = await agent.SendAsync(review, messages, ct); + } + } + + throw new Exception("Admin does not call UpdateProgress function"); + }); + + await RunMathChatAsync(teacher, student, admin); + } + + private async Task CreateTeacherAssistantAgentAsync() + { + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new ArgumentException("AZURE_OPENAI_API_KEY is not set"); + var endPoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new ArgumentException("AZURE_OPENAI_ENDPOINT is not set"); + var model = "gpt-35-turbo-16k"; + var config = new AzureOpenAIConfig(endPoint, model, key); + var llmConfig = new ConversableAgentConfig + { + ConfigList = new[] + { + config, + }, + FunctionContracts = new[] + { + this.CreateMathQuestionFunctionContract, + this.AnswerIsCorrectFunctionContract, + }, + }; + + var teacher = new AssistantAgent( + name: "Teacher", + systemMessage: $@"You are a preschool math teacher. +You create math question and ask student to answer it. +Then you check if the answer is correct. +If the answer is wrong, you ask student to fix it. +If the answer is correct, you create another math question. +", + llmConfig: llmConfig, + functionMap: new Dictionary>> + { + { this.CreateMathQuestionFunction.Name, this.CreateMathQuestionWrapper }, + { this.AnswerIsCorrectFunction.Name, this.AnswerIsCorrectWrapper }, + }); + + return teacher; + } + + private async Task CreateStudentAssistantAgentAsync() + { + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new ArgumentException("AZURE_OPENAI_API_KEY is not set"); + var endPoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new ArgumentException("AZURE_OPENAI_ENDPOINT is not set"); + var model = "gpt-35-turbo-16k"; + var config = new AzureOpenAIConfig(endPoint, model, key); + var llmConfig = new ConversableAgentConfig + { + FunctionContracts = new[] + { + this.AnswerQuestionFunctionContract, + }, + ConfigList = new[] + { + config, + }, + }; + var student = new AssistantAgent( + name: "Student", + systemMessage: $@"You are a student. Here's your workflow in pseudo code: +-workflow- +answer_question +if answer is wrong + fix_answer +-end- + +Here are a few examples of answer_question: +-example 1- +2 + +Here are a few examples of fix_answer: +-example 1- +sorry, the answer should be 2, not 3 +", + llmConfig: llmConfig, + functionMap: new Dictionary>> + { + { this.AnswerQuestionFunction.Name, this.AnswerQuestionWrapper } + }); + + return student; + } + + private async Task RunMathChatAsync(IAgent teacher, IAgent student, IAgent admin) + { + var group = new GroupChat( + [ + admin, + teacher, + student, + ], + admin); + + admin.SendIntroduction($@"Welcome to the group chat! I'm admin", group); + teacher.SendIntroduction($@"Hey I'm Teacher", group); + student.SendIntroduction($@"Hey I'm Student", group); + admin.SendIntroduction(@$"Teacher, please create pre-school math question for student and check answer. +Student, for each question, please answer it and ask teacher to check if the answer is correct. +I'll update the progress after each question is answered. +The conversation will end after 5 correct answers. +", group); + + var groupChatManager = new GroupChatManager(group); + var chatHistory = await admin.InitiateChatAsync(groupChatManager, maxRound: 50); + + // print chat history + foreach (var message in chatHistory) + { + _output.WriteLine(message.FormatMessage()); + } + + // check if there's five questions from teacher + chatHistory.Where(msg => msg.From == teacher.Name && msg.GetContent()?.Contains("[MATH_QUESTION]") is true) + .Count() + .Should().BeGreaterThanOrEqualTo(5); + + // check if there's more than five answers from student (answer might be wrong) + chatHistory.Where(msg => msg.From == student.Name && msg.GetContent()?.Contains("[MATH_ANSWER]") is true) + .Count() + .Should().BeGreaterThanOrEqualTo(5); + + // check if there's five answer_is_correct from teacher + chatHistory.Where(msg => msg.From == teacher.Name && msg.GetContent()?.Contains("[ANSWER_IS_CORRECT]") is true) + .Count() + .Should().BeGreaterThanOrEqualTo(5); + + // check if there's terminate chat message from admin + chatHistory.Where(msg => msg.From == admin.Name && msg.IsGroupChatTerminateMessage()) + .Count() + .Should().Be(1); + } + } +} diff --git a/dotnet/test/AutoGen.Tests/MiddlewareAgentTest.cs b/dotnet/test/AutoGen.Tests/MiddlewareAgentTest.cs new file mode 100644 index 00000000000..9241c9e94f9 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/MiddlewareAgentTest.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MiddlewareAgentTest.cs + +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace AutoGen.Tests; + +public class MiddlewareAgentTest +{ + [Fact] + public async Task MiddlewareAgentUseTestAsync() + { + IAgent echoAgent = new EchoAgent("echo"); + + var middlewareAgent = new MiddlewareAgent(echoAgent); + + // no middleware added + // the reply should be the same as the original agent + middlewareAgent.Name.Should().Be("echo"); + var reply = await middlewareAgent.SendAsync("hello"); + reply.GetContent().Should().Be("hello"); + + middlewareAgent.Use(async (messages, options, agent, ct) => + { + var lastMessage = messages.Last() as TextMessage; + lastMessage!.Content = $"[middleware 0] {lastMessage.Content}"; + return await agent.GenerateReplyAsync(messages, options, ct); + }); + + reply = await middlewareAgent.SendAsync("hello"); + reply.GetContent().Should().Be("[middleware 0] hello"); + + middlewareAgent.Use(async (messages, options, agent, ct) => + { + var lastMessage = messages.Last() as TextMessage; + lastMessage!.Content = $"[middleware 1] {lastMessage.Content}"; + return await agent.GenerateReplyAsync(messages, options, ct); + }); + + // when multiple middleware are added, they will be executed in LIFO order + reply = await middlewareAgent.SendAsync("hello"); + reply.GetContent().Should().Be("[middleware 0] [middleware 1] hello"); + + // test short cut + // short cut middleware will not call next middleware + middlewareAgent.Use(async (messages, options, next, ct) => + { + var lastMessage = messages.Last() as TextMessage; + lastMessage!.Content = $"[middleware shortcut] {lastMessage.Content}"; + return lastMessage; + }); + reply = await middlewareAgent.SendAsync("hello"); + reply.GetContent().Should().Be("[middleware shortcut] hello"); + } + + [Fact] + public async Task RegisterMiddlewareTestAsync() + { + var echoAgent = new EchoAgent("echo"); + + // RegisterMiddleware will return a new agent and keep the original agent unchanged + var middlewareAgent = echoAgent.RegisterMiddleware(async (messages, options, agent, ct) => + { + var lastMessage = messages.Last() as TextMessage; + lastMessage!.Content = $"[middleware 0] {lastMessage.Content}"; + return await agent.GenerateReplyAsync(messages, options, ct); + }); + + middlewareAgent.Should().BeOfType>(); + middlewareAgent.Middlewares.Count().Should().Be(1); + var reply = await middlewareAgent.SendAsync("hello"); + reply.GetContent().Should().Be("[middleware 0] hello"); + reply = await echoAgent.SendAsync("hello"); + reply.GetContent().Should().Be("hello"); + + // when multiple middleware are added, they will be executed in LIFO order + middlewareAgent = middlewareAgent.RegisterMiddleware(async (messages, options, agent, ct) => + { + var lastMessage = messages.Last() as TextMessage; + lastMessage!.Content = $"[middleware 1] {lastMessage.Content}"; + return await agent.GenerateReplyAsync(messages, options, ct); + }); + + middlewareAgent.Middlewares.Count().Should().Be(2); + reply = await middlewareAgent.SendAsync("hello"); + reply.GetContent().Should().Be("[middleware 0] [middleware 1] hello"); + + // test short cut + // short cut middleware will not call next middleware + middlewareAgent = middlewareAgent.RegisterMiddleware(async (messages, options, agent, ct) => + { + var lastMessage = messages.Last() as TextMessage; + lastMessage!.Content = $"[middleware shortcut] {lastMessage.Content}"; + return lastMessage; + }); + + reply = await middlewareAgent.SendAsync("hello"); + reply.GetContent().Should().Be("[middleware shortcut] hello"); + + middlewareAgent.Middlewares.Count().Should().Be(3); + } +} diff --git a/dotnet/test/AutoGen.Tests/MiddlewareTest.cs b/dotnet/test/AutoGen.Tests/MiddlewareTest.cs new file mode 100644 index 00000000000..6c1c89a33c1 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/MiddlewareTest.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MiddlewareTest.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using FluentAssertions; +using Xunit; + +namespace AutoGen.Tests; + +public partial class MiddlewareTest +{ + [Function] + public async Task Echo(string message) + { + return $"[FUNC] {message}"; + } + + [Fact] + public async Task HumanInputMiddlewareTestAsync() + { + var agent = new EchoAgent("echo"); + var neverAskUserInputMW = new HumanInputMiddleware(mode: HumanInputMode.NEVER); + + var neverInputAgent = agent.RegisterMiddleware(neverAskUserInputMW); + var reply = await neverInputAgent.SendAsync("hello"); + reply.GetContent()!.Should().Be("hello"); + reply.From.Should().Be("echo"); + + var alwaysAskUserInputMW = new HumanInputMiddleware( + mode: HumanInputMode.ALWAYS, + getInput: () => "input"); + + var alwaysInputAgent = agent.RegisterMiddleware(alwaysAskUserInputMW); + reply = await alwaysInputAgent.SendAsync("hello"); + reply.GetContent()!.Should().Be("input"); + reply.From.Should().Be("echo"); + + // test auto mode + // if the reply from echo is not terminate message, return the original reply + var autoAskUserInputMW = new HumanInputMiddleware( + mode: HumanInputMode.AUTO, + isTermination: async (messages, ct) => messages.Last()?.GetContent() == "terminate", + getInput: () => "input", + exitKeyword: "exit"); + var autoInputAgent = agent.RegisterMiddleware(autoAskUserInputMW); + reply = await autoInputAgent.SendAsync("hello"); + reply.GetContent()!.Should().Be("hello"); + + // if the reply from echo is terminate message, asking user for input + reply = await autoInputAgent.SendAsync("terminate"); + reply.GetContent()!.Should().Be("input"); + + // if the reply from echo is terminate message, and user input is exit, return the TERMINATE message + autoAskUserInputMW = new HumanInputMiddleware( + mode: HumanInputMode.AUTO, + isTermination: async (messages, ct) => messages.Last().GetContent() == "terminate", + getInput: () => "exit", + exitKeyword: "exit"); + autoInputAgent = agent.RegisterMiddleware(autoAskUserInputMW); + + reply = await autoInputAgent.SendAsync("terminate"); + reply.IsGroupChatTerminateMessage().Should().BeTrue(); + } + + [Fact] + public async Task FunctionCallMiddlewareTestAsync() + { + var agent = new EchoAgent("echo"); + var args = new EchoSchema { message = "hello" }; + var argsJson = JsonSerializer.Serialize(args) ?? throw new InvalidOperationException("Failed to serialize args"); + var functionCall = new FunctionCall("echo", argsJson); + var functionCallAgent = agent.RegisterMiddleware(async (messages, options, agent, ct) => + { + if (options?.Functions is null) + { + return await agent.GenerateReplyAsync(messages, options, ct); + } + + return new ToolCallMessage(functionCall.Name, functionCall.Arguments, from: agent.Name); + }); + + // test 1 + // middleware should invoke function call if the message is a function call message + var mw = new FunctionCallMiddleware( + functionMap: new Dictionary>> { { "echo", EchoWrapper } }); + + var testAgent = agent.RegisterMiddleware(mw); + var functionCallMessage = new ToolCallMessage(functionCall.Name, functionCall.Arguments, from: "user"); + var reply = await testAgent.SendAsync(functionCallMessage); + reply.Should().BeOfType(); + reply.GetContent()!.Should().Be("[FUNC] hello"); + reply.From.Should().Be("echo"); + + // test 2 + // middleware should invoke function call if agent reply is a function call message + mw = new FunctionCallMiddleware( + functions: [this.EchoFunctionContract], + functionMap: new Dictionary>> { { "echo", EchoWrapper } }); + testAgent = functionCallAgent.RegisterMiddleware(mw); + reply = await testAgent.SendAsync("hello"); + reply.GetContent()!.Should().Be("[FUNC] hello"); + reply.From.Should().Be("echo"); + + // test 3 + // middleware should return original reply if the reply from agent is not a function call message + mw = new FunctionCallMiddleware( + functionMap: new Dictionary>> { { "echo", EchoWrapper } }); + testAgent = agent.RegisterMiddleware(mw); + reply = await testAgent.SendAsync("hello"); + reply.GetContent()!.Should().Be("hello"); + reply.From.Should().Be("echo"); + + // test 4 + // middleware should return an error message if the function name is not available when invoking the function from previous agent reply + mw = new FunctionCallMiddleware( + functionMap: new Dictionary>> { { "echo2", EchoWrapper } }); + testAgent = agent.RegisterMiddleware(mw); + reply = await testAgent.SendAsync(functionCallMessage); + reply.GetContent()!.Should().Be("Function echo is not available. Available functions are: echo2"); + } +} diff --git a/dotnet/test/AutoGen.Tests/OpenAIChatAgentTest.cs b/dotnet/test/AutoGen.Tests/OpenAIChatAgentTest.cs new file mode 100644 index 00000000000..8626618fea7 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/OpenAIChatAgentTest.cs @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIChatAgentTest.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; +using Azure.AI.OpenAI; +using FluentAssertions; + +namespace AutoGen.Tests; + +public partial class OpenAIChatAgentTest +{ + /// + /// Get the weather for a location. + /// + /// location + /// + [Function] + public async Task GetWeatherAsync(string location) + { + return $"The weather in {location} is sunny."; + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task BasicConversationTestAsync() + { + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + var openaiClient = new OpenAIClient(new Uri(endpoint), new Azure.AzureKeyCredential(key)); + var openAIChatAgent = new OpenAIChatAgent( + openAIClient: openaiClient, + name: "assistant", + modelName: "gpt-35-turbo-16k"); + + // By default, OpenAIChatClient supports the following message types + // - IMessage + var chatMessageContent = MessageEnvelope.Create(new ChatRequestUserMessage("Hello")); + var reply = await openAIChatAgent.SendAsync(chatMessageContent); + + reply.Should().BeOfType>(); + reply.As>().From.Should().Be("assistant"); + reply.As>().Content.Role.Should().Be(ChatRole.Assistant); + + // test streaming + var streamingReply = await openAIChatAgent.GenerateStreamingReplyAsync(new[] { chatMessageContent }); + + await foreach (var streamingMessage in streamingReply) + { + streamingMessage.Should().BeOfType>(); + streamingMessage.As>().From.Should().Be("assistant"); + } + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task OpenAIChatMessageContentConnectorTestAsync() + { + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + var openaiClient = new OpenAIClient(new Uri(endpoint), new Azure.AzureKeyCredential(key)); + var openAIChatAgent = new OpenAIChatAgent( + openAIClient: openaiClient, + name: "assistant", + modelName: "gpt-35-turbo-16k"); + + MiddlewareStreamingAgent assistant = openAIChatAgent + .RegisterMessageConnector(); + + var messages = new IMessage[] + { + MessageEnvelope.Create(new ChatRequestUserMessage("Hello")), + new TextMessage(Role.Assistant, "Hello", from: "user"), + new MultiModalMessage(Role.Assistant, + [ + new TextMessage(Role.Assistant, "Hello", from: "user"), + ], + from: "user"), + new Message(Role.Assistant, "Hello", from: "user"), // Message type is going to be deprecated, please use TextMessage instead + }; + + foreach (var message in messages) + { + var reply = await assistant.SendAsync(message); + + reply.Should().BeOfType(); + reply.As().From.Should().Be("assistant"); + } + + // test streaming + foreach (var message in messages) + { + var reply = await assistant.GenerateStreamingReplyAsync([message]); + + await foreach (var streamingMessage in reply) + { + streamingMessage.Should().BeOfType(); + streamingMessage.As().From.Should().Be("assistant"); + } + } + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task OpenAIChatAgentToolCallTestAsync() + { + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + var openaiClient = new OpenAIClient(new Uri(endpoint), new Azure.AzureKeyCredential(key)); + var openAIChatAgent = new OpenAIChatAgent( + openAIClient: openaiClient, + name: "assistant", + modelName: "gpt-35-turbo-16k"); + + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [this.GetWeatherAsyncFunctionContract]); + MiddlewareStreamingAgent assistant = openAIChatAgent + .RegisterMessageConnector(); + + assistant.Middlewares.Count().Should().Be(1); + assistant.StreamingMiddlewares.Count().Should().Be(1); + var functionCallAgent = assistant + .RegisterMiddleware(functionCallMiddleware); + + var question = "What's the weather in Seattle"; + var messages = new IMessage[] + { + MessageEnvelope.Create(new ChatRequestUserMessage(question)), + new TextMessage(Role.Assistant, question, from: "user"), + new MultiModalMessage(Role.Assistant, + [ + new TextMessage(Role.Assistant, question, from: "user"), + ], + from: "user"), + new Message(Role.Assistant, question, from: "user"), // Message type is going to be deprecated, please use TextMessage instead + }; + + foreach (var message in messages) + { + var reply = await functionCallAgent.SendAsync(message); + + reply.Should().BeOfType(); + reply.As().From.Should().Be("assistant"); + reply.As().ToolCalls.Count().Should().Be(1); + reply.As().ToolCalls.First().FunctionName.Should().Be(this.GetWeatherAsyncFunctionContract.Name); + } + + // test streaming + foreach (var message in messages) + { + var reply = await functionCallAgent.GenerateStreamingReplyAsync([message]); + ToolCallMessage? toolCallMessage = null; + await foreach (var streamingMessage in reply) + { + streamingMessage.Should().BeOfType(); + streamingMessage.As().From.Should().Be("assistant"); + if (toolCallMessage is null) + { + toolCallMessage = new ToolCallMessage(streamingMessage.As()); + } + else + { + toolCallMessage.Update(streamingMessage.As()); + } + } + + toolCallMessage.Should().NotBeNull(); + toolCallMessage!.From.Should().Be("assistant"); + toolCallMessage.ToolCalls.Count().Should().Be(1); + toolCallMessage.ToolCalls.First().FunctionName.Should().Be(this.GetWeatherAsyncFunctionContract.Name); + } + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task OpenAIChatAgentToolCallInvokingTestAsync() + { + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + var openaiClient = new OpenAIClient(new Uri(endpoint), new Azure.AzureKeyCredential(key)); + var openAIChatAgent = new OpenAIChatAgent( + openAIClient: openaiClient, + name: "assistant", + modelName: "gpt-35-turbo-16k"); + + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [this.GetWeatherAsyncFunctionContract], + functionMap: new Dictionary>> { { this.GetWeatherAsyncFunctionContract.Name!, this.GetWeatherAsyncWrapper } }); + MiddlewareStreamingAgent assistant = openAIChatAgent + .RegisterMessageConnector(); + + var functionCallAgent = assistant + .RegisterMiddleware(functionCallMiddleware); + + var question = "What's the weather in Seattle"; + var messages = new IMessage[] + { + MessageEnvelope.Create(new ChatRequestUserMessage(question)), + new TextMessage(Role.Assistant, question, from: "user"), + new MultiModalMessage(Role.Assistant, + [ + new TextMessage(Role.Assistant, question, from: "user"), + ], + from: "user"), + new Message(Role.Assistant, question, from: "user"), // Message type is going to be deprecated, please use TextMessage instead + }; + + foreach (var message in messages) + { + var reply = await functionCallAgent.SendAsync(message); + + reply.Should().BeOfType>(); + reply.From.Should().Be("assistant"); + reply.GetToolCalls()!.Count().Should().Be(1); + reply.GetToolCalls()!.First().FunctionName.Should().Be(this.GetWeatherAsyncFunctionContract.Name); + reply.GetContent()!.ToLower().Should().Contain("seattle"); + } + + // test streaming + foreach (var message in messages) + { + var reply = await functionCallAgent.GenerateStreamingReplyAsync([message]); + await foreach (var streamingMessage in reply) + { + if (streamingMessage is not IMessage) + { + streamingMessage.Should().BeOfType(); + streamingMessage.As().From.Should().Be("assistant"); + } + else + { + streamingMessage.Should().BeOfType>(); + streamingMessage.As().GetContent()!.ToLower().Should().Contain("seattle"); + } + } + } + } +} diff --git a/dotnet/test/AutoGen.Tests/OpenAIMessageTests.cs b/dotnet/test/AutoGen.Tests/OpenAIMessageTests.cs new file mode 100644 index 00000000000..dd66ae98503 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/OpenAIMessageTests.cs @@ -0,0 +1,377 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIMessageTests.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using ApprovalTests; +using ApprovalTests.Namers; +using ApprovalTests.Reporters; +using AutoGen.OpenAI; +using Azure.AI.OpenAI; +using FluentAssertions; +using Xunit; + +namespace AutoGen.Tests; + +public class OpenAIMessageTests +{ + private readonly JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions + { + WriteIndented = true, + IgnoreReadOnlyProperties = false, + }; + + [Fact] + [UseReporter(typeof(DiffReporter))] + [UseApprovalSubdirectory("ApprovalTests")] + public void BasicMessageTest() + { + IMessage[] messages = [ + new TextMessage(Role.System, "You are a helpful AI assistant"), + new TextMessage(Role.User, "Hello", "user"), + new TextMessage(Role.Assistant, "How can I help you?", from: "assistant"), + new Message(Role.System, "You are a helpful AI assistant"), + new Message(Role.User, "Hello", "user"), + new Message(Role.Assistant, "How can I help you?", from: "assistant"), + new Message(Role.Function, "result", "user"), + new Message(Role.Assistant, null, "assistant") + { + FunctionName = "functionName", + FunctionArguments = "functionArguments", + }, + new ImageMessage(Role.User, "https://example.com/image.png", "user"), + new MultiModalMessage(Role.Assistant, + [ + new TextMessage(Role.User, "Hello", "user"), + new ImageMessage(Role.User, "https://example.com/image.png", "user"), + ], "user"), + new ToolCallMessage("test", "test", "assistant"), + new ToolCallResultMessage("result", "test", "test", "user"), + new ToolCallResultMessage( + [ + new ToolCall("result", "test", "test"), + new ToolCall("result", "test", "test"), + ], "user"), + new ToolCallMessage( + [ + new ToolCall("test", "test"), + new ToolCall("test", "test"), + ], "assistant"), + new AggregateMessage( + message1: new ToolCallMessage("test", "test", "assistant"), + message2: new ToolCallResultMessage("result", "test", "test", "assistant"), "assistant"), + ]; + var openaiMessageConnectorMiddleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant"); + + var oaiMessages = messages.Select(m => (m, openaiMessageConnectorMiddleware.ProcessIncomingMessages(agent, [m]))); + VerifyOAIMessages(oaiMessages); + } + + [Fact] + public void ToOpenAIChatRequestMessageTest() + { + var agent = new EchoAgent("assistant"); + var middleware = new OpenAIChatRequestMessageConnector(); + + // user message + IMessage message = new TextMessage(Role.User, "Hello", "user"); + var oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); + + oaiMessages.Count().Should().Be(1); + oaiMessages.First().Should().BeOfType(); + var userMessage = (ChatRequestUserMessage)oaiMessages.First(); + userMessage.Content.Should().Be("Hello"); + + // user message test 2 + // even if Role is assistant, it should be converted to user message because it is from the user + message = new TextMessage(Role.Assistant, "Hello", "user"); + oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); + + oaiMessages.Count().Should().Be(1); + oaiMessages.First().Should().BeOfType(); + userMessage = (ChatRequestUserMessage)oaiMessages.First(); + userMessage.Content.Should().Be("Hello"); + + // user message with multimodal content + // image + message = new ImageMessage(Role.User, "https://example.com/image.png", "user"); + oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); + + oaiMessages.Count().Should().Be(1); + oaiMessages.First().Should().BeOfType(); + userMessage = (ChatRequestUserMessage)oaiMessages.First(); + userMessage.Content.Should().BeNullOrEmpty(); + userMessage.MultimodalContentItems.Count().Should().Be(1); + userMessage.MultimodalContentItems.First().Should().BeOfType(); + + // text and image + message = new MultiModalMessage( + Role.User, + [ + new TextMessage(Role.User, "Hello", "user"), + new ImageMessage(Role.User, "https://example.com/image.png", "user"), + ], "user"); + oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); + + oaiMessages.Count().Should().Be(1); + oaiMessages.First().Should().BeOfType(); + userMessage = (ChatRequestUserMessage)oaiMessages.First(); + userMessage.Content.Should().BeNullOrEmpty(); + userMessage.MultimodalContentItems.Count().Should().Be(2); + userMessage.MultimodalContentItems.First().Should().BeOfType(); + + // assistant text message + message = new TextMessage(Role.Assistant, "How can I help you?", "assistant"); + oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); + + oaiMessages.Count().Should().Be(1); + oaiMessages.First().Should().BeOfType(); + var assistantMessage = (ChatRequestAssistantMessage)oaiMessages.First(); + assistantMessage.Content.Should().Be("How can I help you?"); + + // assistant text message with single tool call + message = new ToolCallMessage("test", "test", "assistant"); + oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); + + oaiMessages.Count().Should().Be(1); + oaiMessages.First().Should().BeOfType(); + assistantMessage = (ChatRequestAssistantMessage)oaiMessages.First(); + assistantMessage.Content.Should().BeNullOrEmpty(); + assistantMessage.ToolCalls.Count().Should().Be(1); + assistantMessage.ToolCalls.First().Should().BeOfType(); + + // user should not suppose to send tool call message + message = new ToolCallMessage("test", "test", "user"); + Func action = () => middleware.ProcessIncomingMessages(agent, [message]).First(); + action.Should().Throw().WithMessage("ToolCallMessage is not supported when message.From is not the same with agent"); + + // assistant text message with multiple tool calls + message = new ToolCallMessage( + toolCalls: + [ + new ToolCall("test", "test"), + new ToolCall("test", "test"), + ], "assistant"); + + oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); + + oaiMessages.Count().Should().Be(1); + oaiMessages.First().Should().BeOfType(); + assistantMessage = (ChatRequestAssistantMessage)oaiMessages.First(); + assistantMessage.Content.Should().BeNullOrEmpty(); + assistantMessage.ToolCalls.Count().Should().Be(2); + + // tool call result message + message = new ToolCallResultMessage("result", "test", "test", "user"); + oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); + + oaiMessages.Count().Should().Be(1); + oaiMessages.First().Should().BeOfType(); + var toolCallMessage = (ChatRequestToolMessage)oaiMessages.First(); + toolCallMessage.Content.Should().Be("result"); + + // tool call result message with multiple tool calls + message = new ToolCallResultMessage( + toolCalls: + [ + new ToolCall("result", "test", "test"), + new ToolCall("result", "test", "test"), + ], "user"); + + oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); + + oaiMessages.Count().Should().Be(2); + oaiMessages.First().Should().BeOfType(); + toolCallMessage = (ChatRequestToolMessage)oaiMessages.First(); + toolCallMessage.Content.Should().Be("test"); + oaiMessages.Last().Should().BeOfType(); + toolCallMessage = (ChatRequestToolMessage)oaiMessages.Last(); + toolCallMessage.Content.Should().Be("test"); + + // aggregate message test + // aggregate message with tool call and tool call result will be returned by GPT agent if the tool call is automatically invoked inside agent + message = new AggregateMessage( + message1: new ToolCallMessage("test", "test", "assistant"), + message2: new ToolCallResultMessage("result", "test", "test", "assistant"), "assistant"); + + oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); + + oaiMessages.Count().Should().Be(2); + oaiMessages.First().Should().BeOfType(); + assistantMessage = (ChatRequestAssistantMessage)oaiMessages.First(); + assistantMessage.Content.Should().BeNullOrEmpty(); + assistantMessage.ToolCalls.Count().Should().Be(1); + + oaiMessages.Last().Should().BeOfType(); + toolCallMessage = (ChatRequestToolMessage)oaiMessages.Last(); + toolCallMessage.Content.Should().Be("result"); + + // aggregate message test 2 + // if the aggregate message is from user, it should be converted to user message + message = new AggregateMessage( + message1: new ToolCallMessage("test", "test", "user"), + message2: new ToolCallResultMessage("result", "test", "test", "user"), "user"); + + oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); + + oaiMessages.Count().Should().Be(1); + oaiMessages.First().Should().BeOfType(); + userMessage = (ChatRequestUserMessage)oaiMessages.First(); + userMessage.Content.Should().Be("result"); + + // aggregate message test 3 + // if the aggregate message is from user and contains multiple tool call results, it should be converted to user message + message = new AggregateMessage( + message1: new ToolCallMessage( + toolCalls: + [ + new ToolCall("test", "test"), + new ToolCall("test", "test"), + ], from: "user"), + message2: new ToolCallResultMessage( + toolCalls: + [ + new ToolCall("result", "test", "test"), + new ToolCall("result", "test", "test"), + ], from: "user"), "user"); + + oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); + oaiMessages.Count().Should().Be(2); + oaiMessages.First().Should().BeOfType(); + oaiMessages.Last().Should().BeOfType(); + + // system message + message = new TextMessage(Role.System, "You are a helpful AI assistant"); + oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); + oaiMessages.Count().Should().Be(1); + oaiMessages.First().Should().BeOfType(); + } + + [Fact] + public void ToOpenAIChatRequestMessageShortCircuitTest() + { + var agent = new EchoAgent("assistant"); + var middleware = new OpenAIChatRequestMessageConnector(); + ChatRequestMessage[] messages = + [ + new ChatRequestUserMessage("Hello"), + new ChatRequestAssistantMessage("How can I help you?"), + new ChatRequestSystemMessage("You are a helpful AI assistant"), + new ChatRequestFunctionMessage("result", "functionName"), + new ChatRequestToolMessage("test", "test"), + ]; + + foreach (var oaiMessage in messages) + { + IMessage message = new MessageEnvelope(oaiMessage); + var oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); + oaiMessages.Count().Should().Be(1); + oaiMessages.First().Should().Be(oaiMessage); + } + } + private void VerifyOAIMessages(IEnumerable<(IMessage, IEnumerable)> messages) + { + var jsonObjects = messages.Select(pair => + { + var (originalMessage, ms) = pair; + var objs = new List(); + foreach (var m in ms) + { + object? obj = null; + if (m is ChatRequestUserMessage userMessage) + { + obj = new + { + Role = userMessage.Role.ToString(), + Content = userMessage.Content, + MultiModaItem = userMessage.MultimodalContentItems?.Select(item => + { + return item switch + { + ChatMessageImageContentItem imageContentItem => new + { + Type = "Image", + ImageUrl = imageContentItem.ImageUrl, + } as object, + ChatMessageTextContentItem textContentItem => new + { + Type = "Text", + Text = textContentItem.Text, + } as object, + _ => throw new System.NotImplementedException(), + }; + }), + }; + } + + if (m is ChatRequestAssistantMessage assistantMessage) + { + obj = new + { + Role = assistantMessage.Role.ToString(), + Content = assistantMessage.Content, + TooCall = assistantMessage.ToolCalls.Select(tc => + { + return tc switch + { + ChatCompletionsFunctionToolCall functionToolCall => new + { + Type = "Function", + Name = functionToolCall.Name, + Arguments = functionToolCall.Arguments, + Id = functionToolCall.Id, + } as object, + _ => throw new System.NotImplementedException(), + }; + }), + FunctionCallName = assistantMessage.FunctionCall?.Name, + FunctionCallArguments = assistantMessage.FunctionCall?.Arguments, + }; + } + + if (m is ChatRequestSystemMessage systemMessage) + { + obj = new + { + Role = systemMessage.Role.ToString(), + Content = systemMessage.Content, + }; + } + + if (m is ChatRequestFunctionMessage functionMessage) + { + obj = new + { + Role = functionMessage.Role.ToString(), + Content = functionMessage.Content, + Name = functionMessage.Name, + }; + } + + if (m is ChatRequestToolMessage toolCallMessage) + { + obj = new + { + Role = toolCallMessage.Role.ToString(), + Content = toolCallMessage.Content, + ToolCallId = toolCallMessage.ToolCallId, + }; + } + + objs.Add(obj ?? throw new System.NotImplementedException()); + } + + return new + { + OriginalMessage = originalMessage.ToString(), + ConvertedMessages = objs, + }; + }); + + var json = JsonSerializer.Serialize(jsonObjects, this.jsonSerializerOptions); + Approvals.Verify(json); + } +} diff --git a/dotnet/test/AutoGen.Tests/RegisterReplyAgentTest.cs b/dotnet/test/AutoGen.Tests/RegisterReplyAgentTest.cs new file mode 100644 index 00000000000..d4866ad8736 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/RegisterReplyAgentTest.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// RegisterReplyAgentTest.cs + +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace AutoGen.Tests +{ + public class RegisterReplyAgentTest + { + [Fact] + public async Task RegisterReplyTestAsync() + { + IAgent echoAgent = new EchoAgent("echo"); + echoAgent = echoAgent + .RegisterReply(async (conversations, ct) => new TextMessage(Role.Assistant, "I'm your father", from: echoAgent.Name)); + + var msg = new Message(Role.User, "hey"); + var reply = await echoAgent.SendAsync(msg); + reply.Should().BeOfType(); + reply.GetContent().Should().Be("I'm your father"); + reply.GetRole().Should().Be(Role.Assistant); + reply.From.Should().Be("echo"); + } + } +} diff --git a/dotnet/test/AutoGen.Tests/SemanticKernelAgentTest.cs b/dotnet/test/AutoGen.Tests/SemanticKernelAgentTest.cs new file mode 100644 index 00000000000..2e5b56f8091 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/SemanticKernelAgentTest.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SemanticKernelAgentTest.cs + +using System; +using System.Linq; +using System.Threading.Tasks; +using AutoGen.SemanticKernel; +using AutoGen.SemanticKernel.Extension; +using FluentAssertions; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace AutoGen.Tests; + +public partial class SemanticKernelAgentTest +{ + /// + /// Get the weather for a location. + /// + /// location + /// + [Function] + public async Task GetWeatherAsync(string location) + { + return $"The weather in {location} is sunny."; + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task BasicConversationTestAsync() + { + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + var builder = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion("gpt-35-turbo-16k", endpoint, key); + + var kernel = builder.Build(); + + var skAgent = new SemanticKernelAgent(kernel, "assistant"); + + var chatMessageContent = MessageEnvelope.Create(new ChatMessageContent(AuthorRole.Assistant, "Hello")); + var reply = await skAgent.SendAsync(chatMessageContent); + + reply.Should().BeOfType>(); + reply.As>().From.Should().Be("assistant"); + + // test streaming + var streamingReply = await skAgent.GenerateStreamingReplyAsync(new[] { chatMessageContent }); + + await foreach (var streamingMessage in streamingReply) + { + streamingMessage.Should().BeOfType>(); + streamingMessage.As>().From.Should().Be("assistant"); + } + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task SemanticKernelChatMessageContentConnectorTestAsync() + { + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + var builder = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion("gpt-35-turbo-16k", endpoint, key); + + var kernel = builder.Build(); + + var connector = new SemanticKernelChatMessageContentConnector(); + var skAgent = new SemanticKernelAgent(kernel, "assistant") + .RegisterStreamingMiddleware(connector) + .RegisterMiddleware(connector); + + var messages = new IMessage[] + { + MessageEnvelope.Create(new ChatMessageContent(AuthorRole.Assistant, "Hello")), + new TextMessage(Role.Assistant, "Hello", from: "user"), + new MultiModalMessage(Role.Assistant, + [ + new TextMessage(Role.Assistant, "Hello", from: "user"), + ], + from: "user"), + }; + + foreach (var message in messages) + { + var reply = await skAgent.SendAsync(message); + + reply.Should().BeOfType(); + reply.As().From.Should().Be("assistant"); + } + + // test streaming + foreach (var message in messages) + { + var reply = await skAgent.GenerateStreamingReplyAsync([message]); + + await foreach (var streamingMessage in reply) + { + streamingMessage.Should().BeOfType(); + streamingMessage.As().From.Should().Be("assistant"); + } + } + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task SemanticKernelPluginTestAsync() + { + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + var builder = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion("gpt-35-turbo-16k", endpoint, key); + + var parameters = this.GetWeatherAsyncFunctionContract.Parameters!.Select(p => new KernelParameterMetadata(p.Name!) + { + Description = p.Description, + DefaultValue = p.DefaultValue, + IsRequired = p.IsRequired, + ParameterType = p.ParameterType, + }); + var function = KernelFunctionFactory.CreateFromMethod(this.GetWeatherAsync, this.GetWeatherAsyncFunctionContract.Name, this.GetWeatherAsyncFunctionContract.Description, parameters); + builder.Plugins.AddFromFunctions("plugins", [function]); + var kernel = builder.Build(); + + var skAgent = new SemanticKernelAgent(kernel, "assistant") + .RegisterMessageConnector(); + + skAgent.Middlewares.Count().Should().Be(1); + skAgent.StreamingMiddlewares.Count().Should().Be(1); + + var question = "What is the weather in Seattle?"; + var reply = await skAgent.SendAsync(question); + + reply.GetContent()!.ToLower().Should().Contain("seattle"); + reply.GetContent()!.ToLower().Should().Contain("sunny"); + } +} diff --git a/dotnet/test/AutoGen.Tests/SingleAgentTest.cs b/dotnet/test/AutoGen.Tests/SingleAgentTest.cs new file mode 100644 index 00000000000..d314b391bae --- /dev/null +++ b/dotnet/test/AutoGen.Tests/SingleAgentTest.cs @@ -0,0 +1,325 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SingleAgentTest.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoGen.OpenAI; +using Azure.AI.OpenAI; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace AutoGen.Tests +{ + public partial class SingleAgentTest + { + private ITestOutputHelper _output; + public SingleAgentTest(ITestOutputHelper output) + { + _output = output; + } + + private ILLMConfig CreateAzureOpenAIGPT35TurboConfig() + { + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new ArgumentException("AZURE_OPENAI_API_KEY is not set"); + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new ArgumentException("AZURE_OPENAI_ENDPOINT is not set"); + return new AzureOpenAIConfig(endpoint, "gpt-35-turbo-16k", key); + } + + private ILLMConfig CreateOpenAIGPT4VisionConfig() + { + var key = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new ArgumentException("OPENAI_API_KEY is not set"); + return new OpenAIConfig(key, "gpt-4-vision-preview"); + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task GPTAgentTestAsync() + { + var config = this.CreateAzureOpenAIGPT35TurboConfig(); + + var agent = new GPTAgent("gpt", "You are a helpful AI assistant", config); + + await UpperCaseTest(agent); + await UpperCaseStreamingTestAsync(agent); + } + + [ApiKeyFact("OPENAI_API_KEY", "AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task GPTAgentVisionTestAsync() + { + var visionConfig = this.CreateOpenAIGPT4VisionConfig(); + var visionAgent = new GPTAgent( + name: "gpt", + systemMessage: "You are a helpful AI assistant", + config: visionConfig, + temperature: 0); + + var gpt3Config = this.CreateAzureOpenAIGPT35TurboConfig(); + var gpt3Agent = new GPTAgent( + name: "gpt3", + systemMessage: "You are a helpful AI assistant, return highest label from conversation", + config: gpt3Config, + temperature: 0, + functions: new[] { this.GetHighestLabelFunction }, + functionMap: new Dictionary>> + { + { nameof(GetHighestLabel), this.GetHighestLabelWrapper }, + }); + + var imageUri = new Uri(@"https://microsoft.github.io/autogen/assets/images/level2algebra-659ba95286432d9945fc89e84d606797.png"); + var oaiMessage = new ChatRequestUserMessage( + new ChatMessageTextContentItem("which label has the highest inference cost"), + new ChatMessageImageContentItem(imageUri)); + var multiModalMessage = new MultiModalMessage(Role.User, + [ + new TextMessage(Role.User, "which label has the highest inference cost", from: "user"), + new ImageMessage(Role.User, imageUri, from: "user"), + ], + from: "user"); + + var imageMessage = new ImageMessage(Role.User, imageUri, from: "user"); + + IMessage[] messages = [ + MessageEnvelope.Create(oaiMessage), + multiModalMessage, + imageMessage, + ]; + foreach (var message in messages) + { + var response = await visionAgent.SendAsync(message); + response.From.Should().Be(visionAgent.Name); + + var labelResponse = await gpt3Agent.SendAsync(response); + labelResponse.From.Should().Be(gpt3Agent.Name); + labelResponse.GetToolCalls()!.First().FunctionName.Should().Be(nameof(GetHighestLabel)); + } + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task GPTFunctionCallAgentTestAsync() + { + var config = this.CreateAzureOpenAIGPT35TurboConfig(); + var agentWithFunction = new GPTAgent("gpt", "You are a helpful AI assistant", config, 0, functions: new[] { this.EchoAsyncFunction }); + + await EchoFunctionCallTestAsync(agentWithFunction); + await UpperCaseTest(agentWithFunction); + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task AssistantAgentFunctionCallTestAsync() + { + var config = this.CreateAzureOpenAIGPT35TurboConfig(); + + var llmConfig = new ConversableAgentConfig + { + Temperature = 0, + FunctionContracts = new[] + { + this.EchoAsyncFunctionContract, + }, + ConfigList = new[] + { + config, + }, + }; + + var assistantAgent = new AssistantAgent( + name: "assistant", + llmConfig: llmConfig); + + await EchoFunctionCallTestAsync(assistantAgent); + await UpperCaseTest(assistantAgent); + } + + + [Fact] + public async Task AssistantAgentDefaultReplyTestAsync() + { + var assistantAgent = new AssistantAgent( + llmConfig: null, + name: "assistant", + defaultReply: "hello world"); + + var reply = await assistantAgent.SendAsync("hi"); + + reply.GetContent().Should().Be("hello world"); + reply.GetRole().Should().Be(Role.Assistant); + reply.From.Should().Be(assistantAgent.Name); + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task AssistantAgentFunctionCallSelfExecutionTestAsync() + { + var config = this.CreateAzureOpenAIGPT35TurboConfig(); + var llmConfig = new ConversableAgentConfig + { + FunctionContracts = new[] + { + this.EchoAsyncFunctionContract, + }, + ConfigList = new[] + { + config, + }, + }; + var assistantAgent = new AssistantAgent( + name: "assistant", + llmConfig: llmConfig, + functionMap: new Dictionary>> + { + { nameof(EchoAsync), this.EchoAsyncWrapper }, + }); + + await EchoFunctionCallExecutionTestAsync(assistantAgent); + await UpperCaseTest(assistantAgent); + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task GPTAgentFunctionCallSelfExecutionTestAsync() + { + var config = this.CreateAzureOpenAIGPT35TurboConfig(); + var agent = new GPTAgent( + name: "gpt", + systemMessage: "You are a helpful AI assistant", + config: config, + temperature: 0, + functions: new[] { this.EchoAsyncFunction }, + functionMap: new Dictionary>> + { + { nameof(EchoAsync), this.EchoAsyncWrapper }, + }); + + await EchoFunctionCallExecutionStreamingTestAsync(agent); + await EchoFunctionCallExecutionTestAsync(agent); + await UpperCaseTest(agent); + } + + /// + /// echo when asked. + /// + /// message to echo + [FunctionAttribute] + public async Task EchoAsync(string message) + { + return $"[ECHO] {message}"; + } + + /// + /// return the label name with hightest inference cost + /// + /// + /// + [FunctionAttribute] + public async Task GetHighestLabel(string labelName, string color) + { + return $"[HIGHEST_LABEL] {labelName} {color}"; + } + + public async Task EchoFunctionCallTestAsync(IAgent agent) + { + var message = new TextMessage(Role.System, "You are a helpful AI assistant that call echo function"); + var helloWorld = new TextMessage(Role.User, "echo Hello world"); + + var reply = await agent.SendAsync(chatHistory: new[] { message, helloWorld }); + + reply.From.Should().Be(agent.Name); + reply.GetToolCalls()!.First().FunctionName.Should().Be(nameof(EchoAsync)); + } + + public async Task EchoFunctionCallExecutionTestAsync(IAgent agent) + { + var message = new TextMessage(Role.System, "You are a helpful AI assistant that echo whatever user says"); + var helloWorld = new TextMessage(Role.User, "echo Hello world"); + + var reply = await agent.SendAsync(chatHistory: new[] { message, helloWorld }); + + reply.GetContent().Should().Be("[ECHO] Hello world"); + reply.From.Should().Be(agent.Name); + reply.Should().BeOfType>(); + } + + public async Task EchoFunctionCallExecutionStreamingTestAsync(IStreamingAgent agent) + { + var message = new TextMessage(Role.System, "You are a helpful AI assistant that echo whatever user says"); + var helloWorld = new TextMessage(Role.User, "echo Hello world"); + var option = new GenerateReplyOptions + { + Temperature = 0, + }; + var replyStream = await agent.GenerateStreamingReplyAsync(messages: new[] { message, helloWorld }, option); + var answer = "[ECHO] Hello world"; + IStreamingMessage? finalReply = default; + await foreach (var reply in replyStream) + { + reply.From.Should().Be(agent.Name); + finalReply = reply; + } + + if (finalReply is AggregateMessage aggregateMessage) + { + var toolCallResultMessage = aggregateMessage.Message2; + toolCallResultMessage.ToolCalls.First().Result.Should().Be(answer); + toolCallResultMessage.From.Should().Be(agent.Name); + toolCallResultMessage.ToolCalls.First().FunctionName.Should().Be(nameof(EchoAsync)); + } + else + { + throw new Exception("unexpected message type"); + } + } + + public async Task UpperCaseTest(IAgent agent) + { + var message = new TextMessage(Role.System, "You are a helpful AI assistant that convert user message to upper case"); + var uppCaseMessage = new TextMessage(Role.User, "abcdefg"); + + var reply = await agent.SendAsync(chatHistory: new[] { message, uppCaseMessage }); + + reply.GetContent().Should().Contain("ABCDEFG"); + reply.From.Should().Be(agent.Name); + } + + public async Task UpperCaseStreamingTestAsync(IStreamingAgent agent) + { + var message = new TextMessage(Role.System, "You are a helpful AI assistant that convert user message to upper case"); + var helloWorld = new TextMessage(Role.User, "a b c d e f g h i j k l m n"); + var option = new GenerateReplyOptions + { + Temperature = 0, + }; + var replyStream = await agent.GenerateStreamingReplyAsync(messages: new[] { message, helloWorld }, option); + var answer = "A B C D E F G H I J K L M N"; + TextMessage? finalReply = default; + await foreach (var reply in replyStream) + { + if (reply is TextMessageUpdate update) + { + update.From.Should().Be(agent.Name); + + if (finalReply is null) + { + finalReply = new TextMessage(update); + } + else + { + finalReply.Update(update); + } + + continue; + } + else if (reply is TextMessage textMessage) + { + finalReply = textMessage; + continue; + } + + throw new Exception("unexpected message type"); + } + + finalReply!.Content.Should().Contain(answer); + finalReply!.Role.Should().Be(Role.Assistant); + finalReply!.From.Should().Be(agent.Name); + } + } +} diff --git a/dotnet/test/AutoGen.Tests/TwoAgentTest.cs b/dotnet/test/AutoGen.Tests/TwoAgentTest.cs new file mode 100644 index 00000000000..91437eaa618 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/TwoAgentTest.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// TwoAgentTest.cs +#pragma warning disable xUnit1013 +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoGen.OpenAI; +using FluentAssertions; +using Xunit.Abstractions; + +namespace AutoGen.Tests; + +public partial class TwoAgentTest +{ + private ITestOutputHelper _output; + public TwoAgentTest(ITestOutputHelper output) + { + _output = output; + } + + [Function] + public async Task GetWeather(string city) + { + return $"[GetWeatherFunction] The weather in {city} is sunny"; + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task TwoAgentWeatherChatTestAsync() + { + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new ArgumentException("AZURE_OPENAI_API_KEY is not set"); + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new ArgumentException("AZURE_OPENAI_ENDPOINT is not set"); + var deploymentName = "gpt-35-turbo-16k"; + var config = new AzureOpenAIConfig(endpoint, deploymentName, key); + + var assistant = new AssistantAgent( + "assistant", + llmConfig: new ConversableAgentConfig + { + ConfigList = new[] { config }, + FunctionContracts = new[] + { + this.GetWeatherFunctionContract, + }, + }) + .RegisterMiddleware(async (msgs, option, agent, ct) => + { + var reply = await agent.GenerateReplyAsync(msgs, option, ct); + var format = reply.FormatMessage(); + _output.WriteLine(format); + + return reply; + }); + + var user = new UserProxyAgent( + name: "user", + functionMap: new Dictionary>> + { + { this.GetWeatherFunction.Name, this.GetWeatherWrapper }, + }) + .RegisterMiddleware(async (msgs, option, agent, ct) => + { + var lastMessage = msgs.Last(); + if (lastMessage.GetToolCalls()?.FirstOrDefault()?.FunctionName != null) + { + return await agent.GenerateReplyAsync(msgs, option, ct); + } + else + { + // terminate message + return new Message(Role.Assistant, GroupChatExtension.TERMINATE); + } + }) + .RegisterMiddleware(async (msgs, option, agent, ct) => + { + var reply = await agent.GenerateReplyAsync(msgs, option, ct); + var format = reply.FormatMessage(); + _output.WriteLine(format); + + return reply; + }); + + var chatHistory = (await user.InitiateChatAsync(assistant, "what's weather in New York", 10)).ToArray(); + + // the last message should be terminated message + chatHistory.Last().IsGroupChatTerminateMessage().Should().BeTrue(); + + // the third last message should be the weather message from function + chatHistory[^3].GetContent().Should().Be("[GetWeatherFunction] The weather in New York is sunny"); + + // the # of messages should be 5 + chatHistory.Length.Should().Be(5); + } + + public async Task TwoAgentGetWeatherFunctionCallTestAsync(IAgent user, IAgent assistant) + { + var question = new TextMessage(Role.Assistant, "what's the weather in Seattle", from: user.Name); + var assistantReply = await assistant.SendAsync(question); + assistantReply.Should().BeOfType(); + var toolCallResult = await user.SendAsync(chatHistory: [question, assistantReply]); + toolCallResult.Should().BeOfType(); + var finalReply = await assistant.SendAsync(chatHistory: [question, assistantReply, toolCallResult]); + finalReply.Should().BeOfType(); + finalReply.GetContent()!.ToLower().Should().Contain("sunny"); + } +} diff --git a/dotnet/test/AutoGen.Tests/WorkflowTest.cs b/dotnet/test/AutoGen.Tests/WorkflowTest.cs new file mode 100644 index 00000000000..d57cf2126c4 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/WorkflowTest.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// WorkflowTest.cs + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace AutoGen.Tests; + +public class WorkflowTest +{ + [Fact] + public async Task TransitionTestAsync() + { + var alice = new EchoAgent("alice"); + var bob = new EchoAgent("bob"); + + var aliceToBob = Transition.Create(alice, bob, async (from, to, messages) => + { + if (messages.Any(m => m.GetContent() == "Hello")) + { + return true; + } + + return false; + }); + + var canTransit = await aliceToBob.CanTransitionAsync([]); + canTransit.Should().BeFalse(); + + canTransit = await aliceToBob.CanTransitionAsync(new[] { new Message(Role.Assistant, "Hello") }); + canTransit.Should().BeTrue(); + + // if no function is provided, it should always return true + var aliceToBobNoFunction = Transition.Create(alice, bob); + canTransit = await aliceToBobNoFunction.CanTransitionAsync(new[] { new Message(Role.Assistant, "Hello") }); + canTransit.Should().BeTrue(); + } + + [Fact] + public async Task WorkflowBasicTestAsync() + { + var alice = new EchoAgent("alice"); + var bob = new EchoAgent("bob"); + var charlie = new EchoAgent("charlie"); + + // alice can speak to bob + // bob can speak to charlie + // charlie can speak to alice + + var aliceToBob = Transition.Create(alice, bob); + var bobToCharlie = Transition.Create(bob, charlie); + var charlieToAlice = Transition.Create(charlie, alice); + var workflow = new Graph([aliceToBob, bobToCharlie, charlieToAlice]); + IAgent currentAgent = alice; + var agentNames = new List(); + do + { + agentNames.Add(currentAgent.Name!); + var nextAgents = await workflow.TransitToNextAvailableAgentsAsync(currentAgent, []); + nextAgents.Count().Should().Be(1); + currentAgent = nextAgents.First(); + } + while (currentAgent != alice); + + agentNames.Should().BeEquivalentTo(["alice", "bob", "charlie"]); + } +} diff --git a/dotnet/website/.gitignore b/dotnet/website/.gitignore new file mode 100644 index 00000000000..8d5bc9f4490 --- /dev/null +++ b/dotnet/website/.gitignore @@ -0,0 +1,12 @@ +############### +# folder # +############### +/**/DROP/ +/**/TEMP/ +/**/packages/ +/**/bin/ +/**/obj/ + +# build artifacts for web +_site/ +api/ diff --git a/dotnet/website/README.md b/dotnet/website/README.md new file mode 100644 index 00000000000..fd587ad2807 --- /dev/null +++ b/dotnet/website/README.md @@ -0,0 +1,13 @@ +## How to build and run the website + +### Prerequisites +- dotnet 7.0 or later + +### Build +Firstly, go to autogen/dotnet folder and run the following command to build the website: +```bash +dotnet tool restore +dotnet tool run docfx website/docfx.json --serve +``` + +After the command is executed, you can open your browser and navigate to `http://localhost:8080` to view the website. \ No newline at end of file diff --git a/dotnet/website/articles/Agent-overview.md b/dotnet/website/articles/Agent-overview.md new file mode 100644 index 00000000000..08aa7a93da1 --- /dev/null +++ b/dotnet/website/articles/Agent-overview.md @@ -0,0 +1,44 @@ +`Agent` is one of the most fundamental concepts in AutoGen.Net. In AutoGen.Net, you construct a single agent to process a specific task, and you extend an agent using [Middlewares](./Middleware-overview.md), and you construct a multi-agent workflow using [GroupChat](./Group-chat-overview.md). + +> [!NOTE] +> Every agent in AutoGen.Net implements @AutoGen.Core.IAgent, for agent that supports streaming reply, it also implements @AutoGen.Core.IStreamingAgent. + +## Create an agent +- Create an @AutoGen.AssistantAgent: [Create an assistant agent](./Create-an-agent.md) +- Create an @AutoGen.OpenAI.OpenAIChatAgent: [Create an OpenAI chat agent](./OpenAIChatAgent-simple-chat.md) +- Create a @AutoGen.SemanticKernel.SemanticKernelAgent: [Create a semantic kernel agent](./SemanticKernelAgent-simple-chat.md) +- Create a @AutoGen.LMStudio.LMStudioAgent: [Connect to LM Studio](./Consume-LLM-server-from-LM-Studio.md) +- Create your own agent: [Create your own agent](./Create-your-own-agent.md) + +## Chat with an agent +To chat with an agent, typically you can invoke @AutoGen.Core.IAgent.GenerateReplyAsync*. On top of that, you can also use one of the extension methods like @AutoGen.Core.AgentExtension.SendAsync* as shortcuts. + +> [!NOTE] +> AutoGen provides a list of built-in message types like @AutoGen.Core.TextMessage, @AutoGen.Core.ImageMessage, @AutoGen.Core.MultiModalMessage, @AutoGen.Core.ToolCallMessage, @AutoGen.Core.ToolCallResultMessage, etc. You can use these message types to chat with an agent. For further details, see [built-in messages](./Built-in-messages.md). + +- Send a @AutoGen.Core.TextMessage to an agent via @AutoGen.Core.IAgent.GenerateReplyAsync*: +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/AgentCodeSnippet.cs?name=ChatWithAnAgent_GenerateReplyAsync)] + +- Send a message to an agent via @AutoGen.Core.AgentExtension.SendAsync*: +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/AgentCodeSnippet.cs?name=ChatWithAnAgent_SendAsync)] + +## Streaming chat +If an agent implements @AutoGen.Core.IStreamingAgent, you can use @AutoGen.Core.IStreamingAgent.GenerateStreamingReplyAsync* to chat with the agent in a streaming way. You would need to process the streaming updates on your side though. + +- Send a @AutoGen.Core.TextMessage to an agent via @AutoGen.Core.IStreamingAgent.GenerateStreamingReplyAsync*, and print the streaming updates to console: +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/AgentCodeSnippet.cs?name=ChatWithAnAgent_GenerateStreamingReplyAsync)] + +## Register middleware to an agent +@AutoGen.Core.IMiddleware and @AutoGen.Core.IStreamingMiddleware are used to extend the behavior of @AutoGen.Core.IAgent.GenerateReplyAsync* and @AutoGen.Core.IStreamingAgent.GenerateStreamingReplyAsync*. You can register middleware to an agent to customize the behavior of the agent on things like function call support, converting message of different types, print message, gather user input, etc. + +- Middleware overview: [Middleware overview](./Middleware-overview.md) +- Write message to console: [Print message middleware](./Print-message-middleware.md) +- Convert message type: [SemanticKernelChatMessageContentConnector](./SemanticKernelAgent-support-more-messages.md) and [OpenAIChatRequestMessageConnector](./OpenAIChatAgent-support-more-messages.md) +- Create your own middleware: [Create your own middleware](./Create-your-own-middleware.md) + +## Group chat +You can construct a multi-agent workflow using @AutoGen.Core.IGroupChat. In AutoGen.Net, there are two type of group chat: +@AutoGen.Core.SequentialGroupChat: Orchestrates the agents in the group chat in a fix, sequential order. +@AutoGen.Core.GroupChat: Provide more dynamic yet controllable way to orchestrate the agents in the group chat. + +For further details, see [Group chat overview](./Group-chat-overview.md). \ No newline at end of file diff --git a/dotnet/website/articles/AutoGen-Mistral-Overview.md b/dotnet/website/articles/AutoGen-Mistral-Overview.md new file mode 100644 index 00000000000..df5e154d05e --- /dev/null +++ b/dotnet/website/articles/AutoGen-Mistral-Overview.md @@ -0,0 +1,26 @@ +## AutoGen.Mistral overview + +AutoGen.Mistral provides the following agent(s) to connect to [Mistral.AI](https://mistral.ai/) platform. +- @AutoGen.Mistral.MistralClientAgent: A slim wrapper agent over @AutoGen.Mistral.MistralClient. + +### Get started with AutoGen.Mistral + +To get started with AutoGen.Mistral, follow the [installation guide](Installation.md) to make sure you add the AutoGen feed correctly. Then add the `AutoGen.Mistral` package to your project file. + +```bash +dotnet add package AutoGen.Mistral +``` + +>[!NOTE] +> You need to provide an api-key to use Mistral models which will bring additional cost while using. you can get the api key from [Mistral.AI](https://mistral.ai/). + +### Example + +Import the required namespace +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs?name=using_statement)] + +Create a @AutoGen.Mistral.MistralClientAgent and start chatting! +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs?name=create_mistral_agent)] + +Use @AutoGen.Core.IStreamingAgent.GenerateStreamingReplyAsync* to stream the chat completion. +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs?name=streaming_chat)] \ No newline at end of file diff --git a/dotnet/website/articles/AutoGen-OpenAI-Overview.md b/dotnet/website/articles/AutoGen-OpenAI-Overview.md new file mode 100644 index 00000000000..f46cbcc455c --- /dev/null +++ b/dotnet/website/articles/AutoGen-OpenAI-Overview.md @@ -0,0 +1,17 @@ +## AutoGen.OpenAI Overview + +AutoGen.OpenAI provides the following agents over openai models: +- @AutoGen.OpenAI.OpenAIChatAgent: A slim wrapper agent over `OpenAIClient`. This agent only support `IMessage` message type. To support more message types like @AutoGen.Core.TextMessage, register the agent with @AutoGen.OpenAI.OpenAIChatRequestMessageConnector. +- @AutoGen.OpenAI.GPTAgent: An agent that build on top of @AutoGen.OpenAI.OpenAIChatAgent with more message types support like @AutoGen.Core.TextMessage, @AutoGen.Core.ImageMessage, @AutoGen.Core.MultiModalMessage and function call support. Essentially, it is equivalent to @AutoGen.OpenAI.OpenAIChatAgent with @AutoGen.Core.FunctionCallMiddleware and @AutoGen.OpenAI.OpenAIChatRequestMessageConnector registered. + +### Get start with AutoGen.OpenAI + +To get start with AutoGen.OpenAI, firstly, follow the [installation guide](Installation.md) to make sure you add the AutoGen feed correctly. Then add `AutoGen.OpenAI` package to your project file. + +```xml + + + +``` + + diff --git a/dotnet/website/articles/AutoGen-SemanticKernel-Overview.md b/dotnet/website/articles/AutoGen-SemanticKernel-Overview.md new file mode 100644 index 00000000000..581430b268d --- /dev/null +++ b/dotnet/website/articles/AutoGen-SemanticKernel-Overview.md @@ -0,0 +1,17 @@ +## AutoGen.SemanticKernel Overview + +AutoGen.SemanticKernel is a package that provides seamless integration with Semantic Kernel. It provides the following agent: +- @AutoGen.SemanticKernel.SemanticKernelAgent: A slim wrapper agent over `Kernel` that only support original `ChatMessageContent` type via `IMessage`. To support more AutoGen built-in message type, register the agent with @AutoGen.SemanticKernel.SemanticKernelChatMessageContentConnector. + +AutoGen.SemanticKernel also provides the following middleware: +- @AutoGen.SemanticKernel.SemanticKernelChatMessageContentConnector: A connector that convert the message from AutoGen built-in message types to `ChatMessageContent` and vice versa. At the current stage, it only supports conversation between @AutoGen.Core.TextMessage, @AutoGen.Core.ImageMessage and @AutoGen.Core.MultiModalMessage. Function call message type like @AutoGen.Core.ToolCallMessage and @AutoGen.Core.ToolCallResultMessage are not supported yet. + +### Get start with AutoGen.SemanticKernel + +To get start with AutoGen.SemanticKernel, firstly, follow the [installation guide](Installation.md) to make sure you add the AutoGen feed correctly. Then add `AutoGen.SemanticKernel` package to your project file. + +```xml + + + +``` \ No newline at end of file diff --git a/dotnet/website/articles/Built-in-messages.md b/dotnet/website/articles/Built-in-messages.md new file mode 100644 index 00000000000..2767091bd76 --- /dev/null +++ b/dotnet/website/articles/Built-in-messages.md @@ -0,0 +1,34 @@ +## An overview of built-in @AutoGen.Core.IMessage types + +Start from 0.0.9, AutoGen introduces the @AutoGen.Core.IMessage and @AutoGen.Core.IMessage`1 types to provide a unified message interface for different agents. The @AutoGen.Core.IMessage is a non-generic interface that represents a message. The @AutoGen.Core.IMessage`1 is a generic interface that represents a message with a specific `T` where `T` can be any type. + +Besides, AutoGen also provides a set of built-in message types that implement the @AutoGen.Core.IMessage and @AutoGen.Core.IMessage`1 interfaces. These built-in message types are designed to cover different types of messages as much as possilbe. The built-in message types include: + +> [!NOTE] +> The minimal requirement for an agent to be used as admin in @AutoGen.Core.GroupChat is to support @AutoGen.Core.TextMessage. + +- @AutoGen.Core.TextMessage: A message that contains a piece of text. +- @AutoGen.Core.ImageMessage: A message that contains an image. +- @AutoGen.Core.MultiModalMessage: A message that contains multiple modalities like text, image, etc. +- @AutoGen.Core.ToolCallMessage: A message that represents a function call request. +- @AutoGen.Core.ToolCallResultMessage: A message that represents a function call result. +- @AutoGen.Core.AggregateMessage`2: A message that represents an aggregate message that contains multiple sub-messages. This type of message is used by @AutoGen.Core.FunctionCallMiddleware to aggregate both @AutoGen.Core.ToolCallMessage and @AutoGen.Core.ToolCallResultMessage into a single message. +- @AutoGen.Core.MessageEnvelope`1: A message that represents an envelope that contains a message of any type. +- @AutoGen.Core.Message: The original message type before 0.0.9. This message type is reserved for backward compatibility. It is recommended to replace it with a more specific message type like @AutoGen.Core.TextMessage, @AutoGen.Core.ImageMessage, etc. + +### Streaming message support +AutoGen also introduces @AutoGen.Core.IStreamingMessage and @AutoGen.Core.IStreamingMessage`1 which are used in streaming call api. The following built-in message types implement the @AutoGen.Core.IStreamingMessage and @AutoGen.Core.IStreamingMessage`1 interfaces: + +> [!NOTE] +> All @AutoGen.Core.IMessage is also a @AutoGen.Core.IStreamingMessage. That means you can return an @AutoGen.Core.IMessage from a streaming call method. It's also recommended to return the final updated result instead of the last update as the last message in the streaming call method to indicate the end of the stream, which saves caller's effort of assembling the final result from multiple updates. +- @AutoGen.Core.TextMessageUpdate: A message that contains a piece of text update. +- @AutoGen.Core.ToolCallMessageUpdate: A message that contains a function call request update. + +#### Usage + +The below code snippet shows how to print a streaming update to console and update the final result on the caller side. +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/BuildInMessageCodeSnippet.cs?name=StreamingCallCodeSnippet)] + +If the agent returns a final result instead of the last update as the last message in the streaming call method, the caller can directly use the final result without assembling the final result from multiple updates. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/BuildInMessageCodeSnippet.cs?name=StreamingCallWithFinalMessage)] \ No newline at end of file diff --git a/dotnet/website/articles/Consume-LLM-server-from-LM-Studio.md b/dotnet/website/articles/Consume-LLM-server-from-LM-Studio.md new file mode 100644 index 00000000000..dff384a2678 --- /dev/null +++ b/dotnet/website/articles/Consume-LLM-server-from-LM-Studio.md @@ -0,0 +1,20 @@ +## Consume LLM server from LM Studio +You can use @AutoGen.LMStudio.LMStudioAgent from `AutoGen.LMStudio` package to consume openai-like API from LMStudio local server. + +### What's LM Studio +[LM Studio](https://lmstudio.ai/) is an app that allows you to deploy and inference hundreds of thousands of open-source language model on your local machine. It provides an in-app chat ui plus an openai-like API to interact with the language model programmatically. + +### Installation +- Install LM studio if you haven't done so. You can find the installation guide [here](https://lmstudio.ai/) +- Add `AutoGen.LMStudio` to your project. +```xml + + + +``` + +### Usage +The following code shows how to use `LMStudioAgent` to write a piece of C# code to calculate 100th of fibonacci. Before running the code, make sure you have local server from LM Studio running on `localhost:1234`. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example08_LMStudio.cs?name=lmstudio_using_statements)] +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example08_LMStudio.cs?name=lmstudio_example_1)] diff --git a/dotnet/website/articles/Create-a-user-proxy-agent.md b/dotnet/website/articles/Create-a-user-proxy-agent.md new file mode 100644 index 00000000000..44441ed3499 --- /dev/null +++ b/dotnet/website/articles/Create-a-user-proxy-agent.md @@ -0,0 +1,16 @@ +## UserProxyAgent + +[`UserProxyAgent`](../api/AutoGen.UserProxyAgent.yml) is a special type of agent that can be used to proxy user input to another agent or group of agents. It supports the following human input modes: +- `ALWAYS`: Always ask user for input. +- `NEVER`: Never ask user for input. In this mode, the agent will use the default response (if any) to respond to the message. Or using underlying LLM model to generate response if provided. +- `AUTO`: Only ask user for input when conversation is terminated by the other agent(s). Otherwise, use the default response (if any) to respond to the message. Or using underlying LLM model to generate response if provided. + +> [!TIP] +> You can also set up `humanInputMode` when creating `AssistantAgent` to enable/disable human input. `UserProxyAgent` is equivalent to `AssistantAgent` with `humanInputMode` set to `ALWAYS`. Similarly, `AssistantAgent` is equivalent to `UserProxyAgent` with `humanInputMode` set to `NEVER`. + +### Create a `UserProxyAgent` with `HumanInputMode` set to `ALWAYS` + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/UserProxyAgentCodeSnippet.cs?name=code_snippet_1)] + +When running the code, the user proxy agent will ask user for input and use the input as response. +![code output](../images/articles/CreateUserProxyAgent/image-1.png) \ No newline at end of file diff --git a/dotnet/website/articles/Create-an-agent.md b/dotnet/website/articles/Create-an-agent.md new file mode 100644 index 00000000000..1b56666daa1 --- /dev/null +++ b/dotnet/website/articles/Create-an-agent.md @@ -0,0 +1,11 @@ +## AssistantAgent + +[`AssistantAgent`](../api/AutoGen.AssistantAgent.yml) is a built-in agent in `AutoGen` that acts as an AI assistant. It uses LLM to generate response to user input. It also supports function call if the underlying LLM model supports it (e.g. `gpt-3.5-turbo-0613`). + +## Create an `AssistantAgent` using OpenAI model. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/CreateAnAgent.cs?name=code_snippet_1)] + +## Create an `AssistantAgent` using Azure OpenAI model. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/CreateAnAgent.cs?name=code_snippet_2)] diff --git a/dotnet/website/articles/Create-type-safe-function-call.md b/dotnet/website/articles/Create-type-safe-function-call.md new file mode 100644 index 00000000000..82bc5e84405 --- /dev/null +++ b/dotnet/website/articles/Create-type-safe-function-call.md @@ -0,0 +1,41 @@ +## Type-safe function call + +`AutoGen` provides a source generator to easness the trouble of manually craft function definition and function call wrapper from a function. To use this feature, simply add the `AutoGen.SourceGenerator` package to your project and decorate your function with @AutoGen.Core.FunctionAttribute. + +```bash +dotnet add package AutoGen.SourceGenerator +``` + +> [!NOTE] +> It's recommended to enable structural xml document support by setting `GenerateDocumentationFile` property to true in your project file. This allows source generator to leverage the documentation of the function when generating the function definition. + +```xml + + + true + +``` + +Then, create a `public partial` class to host the methods you want to use in AutoGen agents. The method has to be a `public` instance method and its return type must be `Task`. After the methods is defined, mark them with @AutoGen.FunctionAttribute attribute: + +> [!NOTE] +> A `public partial` class is required for the source generator to generate code. +> The method has to be a `public` instance method and its return type must be `Task`. +> Mark the method with @AutoGen.Core.FunctionAttribute attribute. + +Firstly, import the required namespaces: + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/TypeSafeFunctionCallCodeSnippet.cs?name=weather_report_using_statement)] + +Then, create a `WeatherReport` function and mark it with @AutoGen.Core.FunctionAttribute: + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/TypeSafeFunctionCallCodeSnippet.cs?name=weather_report)] + +The source generator will generate the @AutoGen.Core.FunctionContract and function call wrapper for `WeatherReport` in another partial class based on its signature and structural comments. The @AutoGen.Core.FunctionContract is introduced by [#1736](https://github.com/microsoft/autogen/pull/1736) and contains all the necessary metadata such as function name, parameters, and return type. It is LLM independent and can be used to generate openai function definition or semantic kernel function. The function call wrapper is a helper class that provides a type-safe way to call the function. + +> [!NOTE] +> If you are using VSCode as your editor, you may need to restart the editor to see the generated code. + +The following code shows how to generate openai function definition from the @AutoGen.Core.FunctionContract and call the function using the function call wrapper. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/TypeSafeFunctionCallCodeSnippet.cs?name=weather_report_consume)] diff --git a/dotnet/website/articles/Create-your-own-agent.md b/dotnet/website/articles/Create-your-own-agent.md new file mode 100644 index 00000000000..a4548817c7f --- /dev/null +++ b/dotnet/website/articles/Create-your-own-agent.md @@ -0,0 +1 @@ +## Coming soon \ No newline at end of file diff --git a/dotnet/website/articles/Create-your-own-middleware.md b/dotnet/website/articles/Create-your-own-middleware.md new file mode 100644 index 00000000000..a4548817c7f --- /dev/null +++ b/dotnet/website/articles/Create-your-own-middleware.md @@ -0,0 +1 @@ +## Coming soon \ No newline at end of file diff --git a/dotnet/website/articles/Function-call-middleware.md b/dotnet/website/articles/Function-call-middleware.md new file mode 100644 index 00000000000..12c3c041535 --- /dev/null +++ b/dotnet/website/articles/Function-call-middleware.md @@ -0,0 +1 @@ +# Coming soon \ No newline at end of file diff --git a/dotnet/website/articles/Function-call-overview.md b/dotnet/website/articles/Function-call-overview.md new file mode 100644 index 00000000000..e8dfc54cd78 --- /dev/null +++ b/dotnet/website/articles/Function-call-overview.md @@ -0,0 +1,52 @@ +## Overview of function call + +In some LLM models, you can provide a list of function definitions to the model. The function definition is usually essentially an OpenAPI schema object which describes the function, its parameters and return value. And these function definitions tells the model what "functions" are available to be used to resolve the user's request. This feature greatly extend the capability of LLM models by enabling them to "execute" arbitrary function as long as it can be described as a function definition. + +Below is an example of a function definition for getting weather report for a city: + +> [!NOTE] +> To use function call, the underlying LLM model must support function call as well for the best experience. +> The model used in the example below is `gpt-3.5-turbo-0613`. +```json +{ + "name": "GetWeather", + "description": "Get the weather report for a city", + "parameters": { + "city": { + "type": "string", + "description": "The city name" + }, + "required": ["city"] + }, +} +``` + + + +When the model receives a message, it will intelligently decide whether to use function call or not based on the message received. If the model decides to use function call, it will generate a function call which can be used to invoke the actual function. The function call is a json object which contains the function name and its arguments. + +Below is an example of a function call object for getting weather report for Seattle: + +```json +{ + "name": "GetWeather", + "arguments": { + "city": "Seattle" + } +} +``` + +And when the function call is return to the caller, it can be used to invoke the actual function to get the weather report for Seattle. + +### Create type-safe function contract and function call wrapper use AutoGen.SourceGenerator +AutoGen provides a source generator to easness the trouble of manually craft function contract and function call wrapper from a function. To use this feature, simply add the `AutoGen.SourceGenerator` package to your project and decorate your function with `Function` attribute. + +For more information, please check out [Create type-safe function](Create-type-safe-function-call.md). + +### Use function call in an agent +AutoGen provides first-class support for function call in its agent story. Usually there are three ways to enable a function call in an agent. +- Pass function definitions when creating an agent. This only works if the agent supports pass function call from its constructor. +- Passing function definitions in @AutoGen.Core.GenerateReplyOptions when invoking an agent +- Register an agent with @AutoGen.Core.FunctionCallMiddleware to process and invoke function calls. + +For more information, please check out [Use function call in an agent](Use-function-call.md). \ No newline at end of file diff --git a/dotnet/website/articles/Group-chat-overview.md b/dotnet/website/articles/Group-chat-overview.md new file mode 100644 index 00000000000..6db7c64ab95 --- /dev/null +++ b/dotnet/website/articles/Group-chat-overview.md @@ -0,0 +1,8 @@ +@AutoGen.Core.IGroupChat is a fundamental feature in AutoGen. It provides a way to organize multiple agents under the same context and work together to resolve a given task. + +In AutoGen, there are two types of group chat: +- @AutoGen.Core.RoundRobinGroupChat : This group chat runs agents in a round-robin sequence. The chat history plus the most recent reply from the previous agent will be passed to the next agent. +- @AutoGen.Core.GroupChat : This group chat provides a more dynamic yet controlable way to determine the next speaker agent. You can either use a llm agent as group admin, or use a @AutoGen.Core.Graph, which is introduced by [this PR](https://github.com/microsoft/autogen/pull/1761), or both to determine the next speaker agent. + +> [!NOTE] +> In @AutoGen.Core.GroupChat, when only the group admin is used to determine the next speaker agent, it's recommented to use a more powerful llm model, such as `gpt-4` to ensure the best experience. \ No newline at end of file diff --git a/dotnet/website/articles/Group-chat.md b/dotnet/website/articles/Group-chat.md new file mode 100644 index 00000000000..058f4f2521d --- /dev/null +++ b/dotnet/website/articles/Group-chat.md @@ -0,0 +1,73 @@ +@AutoGen.Core.GroupChat invokes agents in a dynamic way. On one hand, It relies on its admin agent to intellegently determines the next speaker based on conversation context, and on the other hand, it also allows you to control the conversation flow by using a @AutoGen.Core.Graph. This makes it a more dynamic yet controlable way to determine the next speaker agent. You can use @AutoGen.Core.GroupChat to create a dynamic group chat with multiple agents working together to resolve a given task. + +> [!NOTE] +> In @AutoGen.Core.GroupChat, when only the group admin is used to determine the next speaker agent, it's recommented to use a more powerful llm model, such as `gpt-4` to ensure the best experience. + +## Use @AutoGen.Core.GroupChat to implement a code interpreter chat flow +The following example shows how to create a dynamic group chat with @AutoGen.Core.GroupChat. In this example, we will create a dynamic group chat with 4 agents: `admin`, `coder`, `reviewer` and `runner`. Each agent has its own role in the group chat: + +### Code interpreter group chat +- `admin`: create task for group to work on and terminate the conversation when task is completed. In this example, the task to resolve is to calculate the 39th Fibonacci number. +- `coder`: a dotnet coder who can write code to resolve tasks. +- `reviewer`: a dotnet code reviewer who can review code written by `coder`. In this example, `reviewer` will examine if the code written by `coder` follows the condition below: + - has only one csharp code block. + - use top-level statements. + - is dotnet code snippet. + - print the result of the code snippet to console. +- `runner`: a dotnet code runner who can run code written by `coder` and print the result. + +```mermaid +flowchart LR + subgraph Group Chat + B[Amin] + C[Coder] + D[Reviewer] + E[Runner] + end +``` + +> [!NOTE] +> The complete code of this example can be found in `Example07_Dynamic_GroupChat_Calculate_Fibonacci` + +### Create group chat + +The code below shows how to create a dynamic group chat with @AutoGen.Core.GroupChat. In this example, we will create a dynamic group chat with 4 agents: `admin`, `coder`, `reviewer` and `runner`. In this case we don't pass a workflow to the group chat, so the group chat will use driven by the admin agent. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs?name=create_group_chat)] + +> [!TIP] +> You can set up initial context for the group chat using @AutoGen.Core.GroupChatExtension.SendIntroduction*. The initial context can help group admin orchestrates the conversation flow. + +Output: + +![GroupChat](../images/articles/DynamicGroupChat/dynamicChat.gif) + +### Below are break-down of how agents are created and their roles in the group chat. + +- Create admin agent + +The code below shows how to create `admin` agent. `admin` agent will create a task for group to work on and terminate the conversation when task is completed. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs?name=create_admin)] + +- Create coder agent + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs?name=create_coder)] + +- Create reviewer agent + +The code below shows how to create `reviewer` agent. `reviewer` agent is a dotnet code reviewer who can review code written by `coder`. In this example, a `function` is used to examine if the code written by `coder` follows the condition. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs?name=reviewer_function)] + +> [!TIP] +> You can use @AutoGen.Core.FunctionAttribute to generate type-safe function definition and function call wrapper for the function. For more information, please check out [Create type safe function call](./Create-type-safe-function-call.md). + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs?name=create_reviewer)] + +- Create runner agent + +> [!TIP] +> `AutoGen` provides a built-in support for running code snippet. For more information, please check out [Execute code snippet](./Run-dotnet-code.md). + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs?name=create_runner)] diff --git a/dotnet/website/articles/Installation.md b/dotnet/website/articles/Installation.md new file mode 100644 index 00000000000..59699a957d6 --- /dev/null +++ b/dotnet/website/articles/Installation.md @@ -0,0 +1,63 @@ +### Current version: + +[![NuGet version](https://badge.fury.io/nu/AutoGen.Core.svg)](https://badge.fury.io/nu/AutoGen.Core) + +AutoGen.Net provides the following packages, you can choose to install one or more of them based on your needs: + +- `AutoGen`: The one-in-all package. This package has dependencies over `AutoGen.Core`, `AutoGen.OpenAI`, `AutoGen.LMStudio`, `AutoGen.SemanticKernel` and `AutoGen.SourceGenerator`. +- `AutoGen.Core`: The core package, this package provides the abstraction for message type, agent and group chat. +- `AutoGen.OpenAI`: This package provides the integration agents over openai models. +- `AutoGen.Mistral`: This package provides the integration agents for Mistral.AI models. +- `AutoGen.LMStudio`: This package provides the integration agents from LM Studio. +- `AutoGen.SemanticKernel`: This package provides the integration agents over semantic kernel. +- `AutoGen.SourceGenerator`: This package carries a source generator that adds support for type-safe function definition generation. +- `AutoGen.DotnetInteractive`: This packages carries dotnet interactive support to execute dotnet code snippet. + +>[!Note] +> Help me choose +> - If you just want to install one package and enjoy the core features of AutoGen, choose `AutoGen`. +> - If you want to leverage AutoGen's abstraction only and want to avoid introducing any other dependencies, like `Azure.AI.OpenAI` or `Semantic Kernel`, choose `AutoGen.Core`. You will need to implement your own agent, but you can still use AutoGen core features like group chat, built-in message type, workflow and middleware. +>- If you want to use AutoGen with openai, choose `AutoGen.OpenAI`, similarly, choose `AutoGen.LMStudio` or `AutoGen.SemanticKernel` if you want to use agents from LM Studio or semantic kernel. +>- If you just want the type-safe source generation for function call and don't want any other features, which even include the AutoGen's abstraction, choose `AutoGen.SourceGenerator`. + +Then, install the package using the following command: + +```bash +dotnet add package AUTOGEN_PACKAGES +``` + +### Consume nightly build +To consume nightly build, you can add one of the following feeds to your `NuGet.config` or global nuget config: +- ![Static Badge](https://img.shields.io/badge/public-blue?style=flat) ![Static Badge](https://img.shields.io/badge/github-grey?style=flat): https://nuget.pkg.github.com/microsoft/index.json +- ![Static Badge](https://img.shields.io/badge/public-blue?style=flat) ![Static Badge](https://img.shields.io/badge/myget-grey?style=flat): https://www.myget.org/F/agentchat/api/v3/index.json +- ![Static Badge](https://img.shields.io/badge/internal-blue?style=flat) ![Static Badge](https://img.shields.io/badge/azure_devops-grey?style=flat) : https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/AutoGen/nuget/v3/index.json + +To add a local `NuGet.config`, create a file named `NuGet.config` in the root of your project and add the following content: +```xml + + + + + + + + + + + +``` + +To add the feed to your global nuget config. You can do this by running the following command in your terminal: +```bash +dotnet nuget add source FEED_URL --name AutoGen + +# dotnet-tools contains Microsoft.DotNet.Interactive.VisualStudio package, which is used by AutoGen.DotnetInteractive +dotnet nuget add source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json --name dotnet-tools +``` + +Once you have added the feed, you can install the nightly-build package using the following command: +```bash +dotnet add package AUTOGEN_PACKAGES VERSION +``` + + diff --git a/dotnet/website/articles/Middleware-overview.md b/dotnet/website/articles/Middleware-overview.md new file mode 100644 index 00000000000..42355de33e6 --- /dev/null +++ b/dotnet/website/articles/Middleware-overview.md @@ -0,0 +1,27 @@ +`Middleware` is a key feature in AutoGen.Net that enables you to customize the behavior of @AutoGen.Core.IAgent.GenerateReplyAsync*. It's similar to the middleware concept in ASP.Net and is widely used in AutoGen.Net for various scenarios, such as function call support, converting message of different types, print message, gather user input, etc. + +Here are a few examples of how middleware is used in AutoGen.Net: +- @AutoGen.AssistantAgent is essentially an agent with @AutoGen.Core.FunctionCallMiddleware, @AutoGen.HumanInputMiddleware and default reply middleware. +- @AutoGen.OpenAI.GPTAgent is essentially an @AutoGen.OpenAI.OpenAIChatAgent with @AutoGen.Core.FunctionCallMiddleware and @AutoGen.OpenAI.OpenAIChatRequestMessageConnector. + +## Use middleware in an agent +To use middleware in an existing agent, you can either create a @AutoGen.Core.MiddlewareAgent on top of the original agent or register middleware functions to the original agent. + +### Create @AutoGen.Core.MiddlewareAgent on top of the original agent +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/MiddlewareAgentCodeSnippet.cs?name=create_middleware_agent_with_original_agent)] + +### Register middleware functions to the original agent +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/MiddlewareAgentCodeSnippet.cs?name=register_middleware_agent)] + +## Short-circuit the next agent +The example below shows how to short-circuit the inner agent + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/MiddlewareAgentCodeSnippet.cs?name=short_circuit_middleware_agent)] + +> [!Note] +> When multiple middleware functions are registered, the order of middleware functions is first registered, last invoked. + +## Streaming middleware +You can also modify the behavior of @AutoGen.Core.IStreamingAgent.GenerateStreamingReplyAsync* by registering streaming middleware to it. One example is @AutoGen.OpenAI.OpenAIChatRequestMessageConnector which converts `StreamingChatCompletionsUpdate` to one of `AutoGen.Core.TextMessageUpdate` or `AutoGen.Core.ToolCallMessageUpdate`. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/MiddlewareAgentCodeSnippet.cs?name=register_streaming_middleware)] \ No newline at end of file diff --git a/dotnet/website/articles/MistralChatAgent-count-token-usage.md b/dotnet/website/articles/MistralChatAgent-count-token-usage.md new file mode 100644 index 00000000000..b7f025aa11d --- /dev/null +++ b/dotnet/website/articles/MistralChatAgent-count-token-usage.md @@ -0,0 +1,28 @@ +The following example shows how to create a `MistralAITokenCounterMiddleware` @AutoGen.Core.IMiddleware and count the token usage when chatting with @AutoGen.Mistral.MistralClientAgent. + +### Overview +To collect the token usage for the entire chat session, one easy solution is simply collect all the responses from agent and sum up the token usage for each response. To collect all the agent responses, we can create a middleware which simply saves all responses to a list and register it with the agent. To get the token usage information for each response, because in the example we are using @AutoGen.Mistral.MistralClientAgent, we can simply get the token usage from the response object. + +> [!NOTE] +> You can find the complete example in the [Example13_OpenAIAgent_JsonMode](https://github.com/microsoft/autogen/tree/dotnet/dotnet/sample/AutoGen.BasicSamples/Example14_MistralClientAgent_TokenCount.cs). + +- Step 1: Adding using statement +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example14_MistralClientAgent_TokenCount.cs?name=using_statements)] + +- Step 2: Create a `MistralAITokenCounterMiddleware` class which implements @AutoGen.Core.IMiddleware. This middleware will collect all the responses from the agent and sum up the token usage for each response. +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example14_MistralClientAgent_TokenCount.cs?name=token_counter_middleware)] + +- Step 3: Create a `MistralClientAgent` +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example14_MistralClientAgent_TokenCount.cs?name=create_mistral_client_agent)] + +- Step 4: Register the `MistralAITokenCounterMiddleware` with the `MistralClientAgent`. Note that the order of each middlewares matters. The token counter middleware needs to be registered before `mistralMessageConnector` because it collects response only when the responding message type is `IMessage` while the `mistralMessageConnector` will convert `IMessage` to one of @AutoGen.Core.TextMessage, @AutoGen.Core.ToolCallMessage or @AutoGen.Core.ToolCallResultMessage. +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example14_MistralClientAgent_TokenCount.cs?name=register_middleware)] + +- Step 5: Chat with the `MistralClientAgent` and get the token usage information from the response object. +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example14_MistralClientAgent_TokenCount.cs?name=chat_with_agent)] + +### Output +When running the example, the completion token count will be printed to the console. +```bash +Completion token count: 1408 # might be different based on the response +``` \ No newline at end of file diff --git a/dotnet/website/articles/MistralChatAgent-use-function-call.md b/dotnet/website/articles/MistralChatAgent-use-function-call.md new file mode 100644 index 00000000000..56ea0ffd08e --- /dev/null +++ b/dotnet/website/articles/MistralChatAgent-use-function-call.md @@ -0,0 +1,41 @@ +## Use tool in MistralChatAgent + +The following example shows how to enable tool support in @AutoGen.Mistral.MistralClientAgent by creating a `GetWeatherAsync` function and passing it to the agent. + +Firstly, you need to install the following packages: +```bash +dotnet add package AutoGen.Mistral +dotnet add package AutoGen.SourceGenerator +``` + +> [!Note] +> Tool support is only available in some mistral models. Please refer to the [link](https://docs.mistral.ai/capabilities/function_calling/#available-models) for tool call support in mistral models. + +> [!Note] +> The `AutoGen.SourceGenerator` package carries a source generator that adds support for type-safe function definition generation. For more information, please check out [Create type-safe function](./Create-type-safe-function-call.md). + +> [!NOTE] +> If you are using VSCode as your editor, you may need to restart the editor to see the generated code. + +Import the required namespace +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs?name=using_statement)] + +Then define a public partial `MistralAgentFunction` class and `GetWeather` method. The `GetWeather` method is a simple function that returns the weather of a given location that marked with @AutoGen.Core.FunctionAttribute. Marking the class as `public partial` together with the @AutoGen.Core.FunctionAttribute attribute allows the source generator to generate the @AutoGen.Core.FunctionContract for the `GetWeather` method. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs?name=weather_function)] + +Then create an @AutoGen.Mistral.MistralClientAgent and register it with @AutoGen.Mistral.Extension.MistralAgentExtension.RegisterMessageConnector* so it can support @AutoGen.Core.ToolCallMessage and @AutoGen.Core.ToolCallResultMessage. These message types are necessary to use @AutoGen.Core.FunctionCallMiddleware, which provides support for processing and invoking function calls. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs?name=create_mistral_function_call_agent)] + +Then create an @AutoGen.Core.FunctionCallMiddleware with `GetWeather` function When creating the middleware, we also pass a `functionMap` object which means the function will be automatically invoked when the agent replies a `GetWeather` function call. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs?name=create_get_weather_function_call_middleware)] + +After the function call middleware is created, register it with the agent so the `GetWeather` function will be passed to agent during chat completion. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs?name=register_function_call_middleware)] + +Finally, you can chat with the @AutoGen.Mistral.MistralClientAgent about weather! The agent will automatically invoke the `GetWeather` function to "get" the weather information and return the result. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs?name=send_message_with_function_call)] \ No newline at end of file diff --git a/dotnet/website/articles/OpenAIChatAgent-simple-chat.md b/dotnet/website/articles/OpenAIChatAgent-simple-chat.md new file mode 100644 index 00000000000..867aff24af9 --- /dev/null +++ b/dotnet/website/articles/OpenAIChatAgent-simple-chat.md @@ -0,0 +1,11 @@ +The following example shows how to create an @AutoGen.OpenAI.OpenAIChatAgent and chat with it. + +Firsly, import the required namespaces: +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs?name=using_statement)] + +Then, create an @AutoGen.OpenAI.OpenAIChatAgent and chat with it: +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs?name=create_openai_chat_agent)] + +@AutoGen.OpenAI.OpenAIChatAgent also supports streaming chat via @AutoGen.Core.IAgent.GenerateStreamingReplyAsync*. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs?name=create_openai_chat_agent_streaming)] \ No newline at end of file diff --git a/dotnet/website/articles/OpenAIChatAgent-support-more-messages.md b/dotnet/website/articles/OpenAIChatAgent-support-more-messages.md new file mode 100644 index 00000000000..af6e60682b2 --- /dev/null +++ b/dotnet/website/articles/OpenAIChatAgent-support-more-messages.md @@ -0,0 +1,6 @@ +By default, @AutoGen.OpenAI.OpenAIChatAgent only supports the @AutoGen.Core.IMessage type where `T` is original request or response message from `Azure.AI.OpenAI`. To support more AutoGen built-in message types like @AutoGen.Core.TextMessage, @AutoGen.Core.ImageMessage, @AutoGen.Core.MultiModalMessage and so on, you can register the agent with @AutoGen.OpenAI.OpenAIChatRequestMessageConnector. The @AutoGen.OpenAI.OpenAIChatRequestMessageConnector will convert the message from AutoGen built-in message types to `Azure.AI.OpenAI.ChatRequestMessage` and vice versa. + +import the required namespaces: +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs?name=using_statement)] + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs?name=register_openai_chat_message_connector)] \ No newline at end of file diff --git a/dotnet/website/articles/OpenAIChatAgent-use-function-call.md b/dotnet/website/articles/OpenAIChatAgent-use-function-call.md new file mode 100644 index 00000000000..da12ae9e90a --- /dev/null +++ b/dotnet/website/articles/OpenAIChatAgent-use-function-call.md @@ -0,0 +1,33 @@ +The following example shows how to create a `GetWeatherAsync` function and pass it to @AutoGen.OpenAI.OpenAIChatAgent. + +Firstly, you need to install the following packages: +```xml + + + + +``` + +> [!Note] +> The `AutoGen.SourceGenerator` package carries a source generator that adds support for type-safe function definition generation. For more information, please check out [Create type-safe function](./Create-type-safe-function-call.md). + +> [!NOTE] +> If you are using VSCode as your editor, you may need to restart the editor to see the generated code. + +Firstly, import the required namespaces: +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs?name=using_statement)] + +Then, define a public partial class: `Function` with `GetWeather` method +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs?name=weather_function)] + +Then, create an @AutoGen.OpenAI.OpenAIChatAgent and register it with @AutoGen.OpenAI.OpenAIChatRequestMessageConnector so it can support @AutoGen.Core.ToolCallMessage and @AutoGen.Core.ToolCallResultMessage. These message types are necessary to use @AutoGen.Core.FunctionCallMiddleware, which provides support for processing and invoking function calls. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs?name=openai_chat_agent_get_weather_function_call)] + +Then, create an @AutoGen.Core.FunctionCallMiddleware with `GetWeather` function and register it with the agent above. When creating the middleware, we also pass a `functionMap` to @AutoGen.Core.FunctionCallMiddleware, which means the function will be automatically invoked when the agent replies a `GetWeather` function call. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs?name=create_function_call_middleware)] + +Finally, you can chat with the @AutoGen.OpenAI.OpenAIChatAgent and invoke the `GetWeather` function. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs?name=chat_agent_send_function_call)] \ No newline at end of file diff --git a/dotnet/website/articles/OpenAIChatAgent-use-json-mode.md b/dotnet/website/articles/OpenAIChatAgent-use-json-mode.md new file mode 100644 index 00000000000..4d69340f585 --- /dev/null +++ b/dotnet/website/articles/OpenAIChatAgent-use-json-mode.md @@ -0,0 +1,31 @@ +The following example shows how to enable JSON mode in @AutoGen.OpenAI.OpenAIChatAgent. + +## What is JSON mode? +JSON mode is a new feature in OpenAI which allows you to instruct model to always respond with a valid JSON object. This is useful when you want to constrain the model output to JSON format only. + +> [!NOTE] +> Currently, JOSN mode is only supported by `gpt-4-turbo-preview` and `gpt-3.5-turbo-0125`. For more information (and limitations) about JSON mode, please visit [OpenAI API documentation](https://platform.openai.com/docs/guides/text-generation/json-mode). + +## How to enable JSON mode in OpenAIChatAgent. + +> [!NOTE] +> You can find the complete example in the [Example13_OpenAIAgent_JsonMode](https://github.com/microsoft/autogen/tree/dotnet/dotnet/sample/AutoGen.BasicSamples/Example13_OpenAIAgent_JsonMode.cs). + +To enable JSON mode for @AutoGen.OpenAI.OpenAIChatAgent, set `responseFormat` to `ChatCompletionsResponseFormat.JsonObject` when creating the agent. Note that when enabling JSON mode, you also need to instruct the agent to output JSON format in its system message. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example13_OpenAIAgent_JsonMode.cs?name=create_agent)] + +After enabling JSON mode, the `openAIClientAgent` will always respond in JSON format when it receives a message. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example13_OpenAIAgent_JsonMode.cs?name=chat_with_agent)] + +When running the example, the output from `openAIClientAgent` will be a valid JSON object which can be parsed as `Person` class defined below. Note that in the output, the `address` field is missing because the address information is not provided in user input. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example13_OpenAIAgent_JsonMode.cs?name=person_class)] + +The output will be: +```bash +Name: John +Age: 25 +Done +``` \ No newline at end of file diff --git a/dotnet/website/articles/Print-message-middleware.md b/dotnet/website/articles/Print-message-middleware.md new file mode 100644 index 00000000000..b0115970d77 --- /dev/null +++ b/dotnet/website/articles/Print-message-middleware.md @@ -0,0 +1,27 @@ +@AutoGen.Core.PrintMessageMiddleware is a built-in @AutoGen.Core.IMiddleware that pretty print @AutoGen.Core.IMessage to console. + +> [!NOTE] +> @AutoGen.Core.PrintMessageMiddleware support the following @AutoGen.Core.IMessage types: +> - @AutoGen.Core.TextMessage +> - @AutoGen.Core.MultiModalMessage +> - @AutoGen.Core.ToolCallMessage +> - @AutoGen.Core.ToolCallResultMessage +> - @AutoGen.Core.Message +> - (streaming) @AutoGen.Core.TextMessageUpdate +> - (streaming) @AutoGen.Core.ToolCallMessageUpdate + +## Use @AutoGen.Core.PrintMessageMiddleware in an agent +You can use @AutoGen.Core.PrintMessageMiddlewareExtension.RegisterPrintMessage* to register the @AutoGen.Core.PrintMessageMiddleware to an agent. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/PrintMessageMiddlewareCodeSnippet.cs?name=PrintMessageMiddleware)] + +@AutoGen.Core.PrintMessageMiddlewareExtension.RegisterPrintMessage* will format the message and print it to console +![image](../images/articles/PrintMessageMiddleware/printMessage.png) + +## Streaming message support + +@AutoGen.Core.PrintMessageMiddleware also supports streaming message types like @AutoGen.Core.TextMessageUpdate and @AutoGen.Core.ToolCallMessageUpdate. If you register @AutoGen.Core.PrintMessageMiddleware to a @AutoGen.Core.IStreamingAgent, it will format the streaming message and print it to console if the message is of supported type. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/PrintMessageMiddlewareCodeSnippet.cs?name=print_message_streaming)] + +![image](../images/articles/PrintMessageMiddleware/streamingoutput.gif) diff --git a/dotnet/website/articles/Roundrobin-chat.md b/dotnet/website/articles/Roundrobin-chat.md new file mode 100644 index 00000000000..20fd19b4d79 --- /dev/null +++ b/dotnet/website/articles/Roundrobin-chat.md @@ -0,0 +1,33 @@ +@AutoGen.Core.RoundRobinGroupChat is a group chat that invokes agents in a round-robin order. It's useful when you want to call multiple agents in a fixed sequence. For example, asking search agent to retrieve related information followed by a summarization agent to summarize the information. Beside, it also used by @AutoGen.Core.AgentExtension.SendAsync(AutoGen.Core.IAgent,AutoGen.Core.IAgent,System.String,System.Collections.Generic.IEnumerable{AutoGen.Core.IMessage},System.Int32,System.Threading.CancellationToken) in two agent chat. + +### Use @AutoGen.Core.RoundRobinGroupChat to implement a search-summarize chat flow + +```mermaid +flowchart LR + A[User] -->|Ask a question| B[Search Agent] + B -->|Retrieve information| C[Summarization Agent] + C -->|Summarize result| A[User] +``` + +> [!NOTE] +> Complete code can be found in [Example11_Sequential_GroupChat_Example](https://github.com/microsoft/autogen/blob/dotnet/dotnet/sample/AutoGen.BasicSamples/Example11_Sequential_GroupChat_Example.cs); + +Step 1: Add required using statements + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example11_Sequential_GroupChat_Example.cs?name=using_statement)] + +Step 2: Create a `bingSearch` agent using @AutoGen.SemanticKernel.SemanticKernelAgent + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example11_Sequential_GroupChat_Example.cs?name=CreateBingSearchAgent)] + +Step 3: Create a `summarization` agent using @AutoGen.SemanticKernel.SemanticKernelAgent + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example11_Sequential_GroupChat_Example.cs?name=CreateSummarizerAgent)] + +Step 4: Create a @AutoGen.Core.RoundRobinGroupChat and add `bingSearch` and `summarization` agents to it + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example11_Sequential_GroupChat_Example.cs?name=Sequential_GroupChat_Example)] + +Output: + +![Searcher-Summarizer](../images/articles/SequentialGroupChat/SearcherSummarizer.gif) \ No newline at end of file diff --git a/dotnet/website/articles/Run-dotnet-code.md b/dotnet/website/articles/Run-dotnet-code.md new file mode 100644 index 00000000000..e3d8fa78a0b --- /dev/null +++ b/dotnet/website/articles/Run-dotnet-code.md @@ -0,0 +1,32 @@ +`AutoGen` provides a built-in feature to run code snippet from agent response. Currently the following languages are supported: +- dotnet + +More languages will be supported in the future. + +## What is a code snippet? +A code snippet in agent response is a code block with a language identifier. For example: + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs?name=code_snippet_1_3)] + +## Why running code snippet is useful? +The ability of running code snippet can greatly extend the ability of an agent. Because it enables agent to resolve tasks by writing code and run it, which is much more powerful than just returning a text response. + +For example, in data analysis scenario, agent can resolve tasks like "What is the average of the sales amount of the last 7 days?" by firstly write a code snippet to query the sales amount of the last 7 days, then calculate the average and then run the code snippet to get the result. + +> [!WARNING] +> Running arbitrary code snippet from agent response could bring risks to your system. Using this feature with caution. + +## How to run dotnet code snippet? +The built-in feature of running dotnet code snippet is provided by [dotnet-interactive](https://github.com/dotnet/interactive). To run dotnet code snippet, you need to install the following package to your project, which provides the intergraion with dotnet-interactive: + +```xml + +``` + +Then you can use @AutoGen.DotnetInteractive.AgentExtension.RegisterDotnetCodeBlockExectionHook(AutoGen.IAgent,InteractiveService,System.String,System.String) to register a `reply hook` to run dotnet code snippet. The hook will check if a csharp code snippet is present in the most recent message from history, and run the code snippet if it is present. + +The following code snippet shows how to register a dotnet code snippet execution hook: + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs?name=code_snippet_0_1)] +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs?name=code_snippet_1_1)] +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs?name=code_snippet_1_2)] diff --git a/dotnet/website/articles/SemanticKernelAgent-simple-chat.md b/dotnet/website/articles/SemanticKernelAgent-simple-chat.md new file mode 100644 index 00000000000..9b16ceb4e18 --- /dev/null +++ b/dotnet/website/articles/SemanticKernelAgent-simple-chat.md @@ -0,0 +1,9 @@ +You can chat with @AutoGen.SemanticKernel.SemanticKernelAgent using both streaming and non-streaming methods and use native `ChatMessageContent` type via `IMessage`. + +The following example shows how to create an @AutoGen.SemanticKernel.SemanticKernelAgent and chat with it using non-streaming method: + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/SemanticKernelCodeSnippet.cs?name=create_semantic_kernel_agent)] + +@AutoGen.SemanticKernel.SemanticKernelAgent also supports streaming chat via @AutoGen.Core.IStreamingAgent.GenerateStreamingReplyAsync*. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/SemanticKernelCodeSnippet.cs?name=create_semantic_kernel_agent_streaming)] diff --git a/dotnet/website/articles/SemanticKernelAgent-support-more-messages.md b/dotnet/website/articles/SemanticKernelAgent-support-more-messages.md new file mode 100644 index 00000000000..3dfcf5b0d38 --- /dev/null +++ b/dotnet/website/articles/SemanticKernelAgent-support-more-messages.md @@ -0,0 +1,10 @@ +@AutoGen.SemanticKernel.SemanticKernelAgent only supports the original `ChatMessageContent` type via `IMessage`. To support more AutoGen built-in message types like @AutoGen.Core.TextMessage, @AutoGen.Core.ImageMessage, @AutoGen.Core.MultiModalMessage, you can register the agent with @AutoGen.SemanticKernel.SemanticKernelChatMessageContentConnector. The @AutoGen.SemanticKernel.SemanticKernelChatMessageContentConnector will convert the message from AutoGen built-in message types to `ChatMessageContent` and vice versa. +> [!NOTE] +> At the current stage, @AutoGen.SemanticKernel.SemanticKernelChatMessageContentConnector only supports conversation for the followng built-in @AutoGen.Core.IMessage +> - @AutoGen.Core.TextMessage +> - @AutoGen.Core.ImageMessage +> - @AutoGen.Core.MultiModalMessage +> +> Function call message type like @AutoGen.Core.ToolCallMessage and @AutoGen.Core.ToolCallResultMessage are not supported yet. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/SemanticKernelCodeSnippet.cs?name=register_semantic_kernel_chat_message_content_connector)] \ No newline at end of file diff --git a/dotnet/website/articles/Two-agent-chat.md b/dotnet/website/articles/Two-agent-chat.md new file mode 100644 index 00000000000..2fe5f8401e1 --- /dev/null +++ b/dotnet/website/articles/Two-agent-chat.md @@ -0,0 +1,19 @@ +In `AutoGen`, you can start a conversation between two agents using @AutoGen.Core.AgentExtension.InitiateChatAsync* or one of @AutoGen.Core.AgentExtension.SendAsync* APIs. When conversation starts, the sender agent will firstly send a message to receiver agent, then receiver agent will generate a reply and send it back to sender agent. This process will repeat until either one of the agent sends a termination message or the maximum number of turns is reached. + +> [!NOTE] +> A termination message is an @AutoGen.Core.IMessage which content contains the keyword: @AutoGen.Core.GroupChatExtension.TERMINATE. To determine if a message is a terminate message, you can use @AutoGen.Core.GroupChatExtension.IsGroupChatTerminateMessage*. + +## A basic example + +The following example shows how to start a conversation between the teacher agent and student agent, where the student agent starts the conversation by asking teacher to create math questions. + +> [!TIP] +> You can use @AutoGen.Core.PrintMessageMiddlewareExtension.RegisterPrintMessage* to pretty print the message replied by the agent. + +> [!NOTE] +> The conversation is terminated when teacher agent sends a message containing the keyword: @AutoGen.Core.GroupChatExtension.TERMINATE. + +> [!NOTE] +> The teacher agent uses @AutoGen.Core.MiddlewareExtension.RegisterPostProcess* to register a post process function which returns a hard-coded termination message when a certain condition is met. Comparing with putting the @AutoGen.Core.GroupChatExtension.TERMINATE keyword in the prompt, this approach is more robust especially when a weaker LLM model is used. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example02_TwoAgent_MathChat.cs?name=code_snippet_1)] diff --git a/dotnet/website/articles/Use-function-call.md b/dotnet/website/articles/Use-function-call.md new file mode 100644 index 00000000000..8c0f172e7da --- /dev/null +++ b/dotnet/website/articles/Use-function-call.md @@ -0,0 +1,43 @@ +## Use function call in AutoGen agent + +Typically, there are three ways to pass a function definition to an agent to enable function call: +- Pass function definitions when creating an agent. This only works if the agent supports pass function call from its constructor. +- Passing function definitions in @AutoGen.Core.GenerateReplyOptions when invoking an agent +- Register an agent with @AutoGen.Core.FunctionCallMiddleware to process and invoke function calls. + +> [!NOTE] +> To use function call, the underlying LLM model must support function call as well for the best experience. If the model does not support function call, it's likely that the function call will be ignored and the model will reply with a normal response even if a function call is passed to it. + +## Pass function definitions when creating an agent +In some agents like @AutoGen.AssistantAgent or @AutoGen.OpenAI.GPTAgent, you can pass function definitions when creating the agent + +Suppose the `TypeSafeFunctionCall` is defined in the following code snippet: +[!code-csharp[TypeSafeFunctionCall](../../sample/AutoGen.BasicSamples/CodeSnippet/TypeSafeFunctionCallCodeSnippet.cs?name=weather_report)] + +You can then pass the `WeatherReport` to the agent when creating it: +[!code-csharp[assistant agent](../../sample/AutoGen.BasicSamples/CodeSnippet/FunctionCallCodeSnippet.cs?name=code_snippet_4)] + +## Passing function definitions in @AutoGen.Core.GenerateReplyOptions when invoking an agent +You can also pass function definitions in @AutoGen.Core.GenerateReplyOptions when invoking an agent. This is useful when you want to override the function definitions passed to the agent when creating it. + +[!code-csharp[assistant agent](../../sample/AutoGen.BasicSamples/CodeSnippet/FunctionCallCodeSnippet.cs?name=overrider_function_contract)] + +## Register an agent with @AutoGen.Core.FunctionCallMiddleware to process and invoke function calls +You can also register an agent with @AutoGen.Core.FunctionCallMiddleware to process and invoke function calls. This is useful when you want to process and invoke function calls in a more flexible way. + +[!code-csharp[assistant agent](../../sample/AutoGen.BasicSamples/CodeSnippet/FunctionCallCodeSnippet.cs?name=register_function_call_middleware)] + +## Invoke function call inside an agent +To invoke a function instead of returning the function call object, you can pass its function call wrapper to the agent via `functionMap`. + +You can then pass the `WeatherReportWrapper` to the agent via `functionMap`: +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/FunctionCallCodeSnippet.cs?name=code_snippet_6)] + +When a function call object is returned, the agent will invoke the function and uses the return value as response rather than returning the function call object. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/FunctionCallCodeSnippet.cs?name=code_snippet_6_1)] + +## Invoke function call by another agent +You can also use another agent to invoke the function call from one agent. This is a useful pattern in two-agent chat, where one agent is used as a function proxy to invoke the function call from another agent. Once the function call is invoked, the result can be returned to the original agent for further processing. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/FunctionCallCodeSnippet.cs?name=two_agent_weather_chat)] \ No newline at end of file diff --git a/dotnet/website/articles/Use-graph-in-group-chat.md b/dotnet/website/articles/Use-graph-in-group-chat.md new file mode 100644 index 00000000000..1cc97e50fe6 --- /dev/null +++ b/dotnet/website/articles/Use-graph-in-group-chat.md @@ -0,0 +1,25 @@ +Sometimes, you may want to add more control on how the next agent is selected in a @AutoGen.Core.GroupChat based on the task you want to resolve. For example, in the previous [code writing example](./Group-chat.md), the original code interpreter workflow can be improved by the following diagram because it's not necessary for `admin` to directly talk to `reviewer`, nor it's necessary for `coder` to talk to `runner`. + +```mermaid +flowchart TD + A[Admin] -->|Ask coder to write code| B[Coder] + B -->|Ask Reviewer to review code| C[Reviewer] + C -->|Ask Runner to run code| D[Runner] + D -->|Send result if succeed| A[Admin] + D -->|Ask coder to fix if failed| B[Coder] + C -->|Ask coder to fix if not approved| B[Coder] +``` + +By having @AutoGen.Core.GroupChat to follow a specific graph flow, we can bring prior knowledge to group chat and make the conversation more efficient and robust. This is where @AutoGen.Core.Graph comes in. + +### Create a graph +The following code shows how to create a graph that represents the diagram above. The graph doesn't need to be a finite state machine where each state can only have one legitimate next state. Instead, it can be a directed graph where each state can have multiple legitimate next states. And if there are multiple legitimate next states, the `admin` agent of @AutoGen.Core.GroupChat will decide which one to go based on the conversation context. + +> [!TIP] +> @AutoGen.Core.Graph supports conditional transitions. To create a conditional transition, you can pass a lambda function to `canTransitionAsync` when creating a @AutoGen.Core.Transition. The lambda function should return a boolean value indicating if the transition can be taken. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs?name=create_workflow)] + +Once the graph is created, you can pass it to the group chat. The group chat will then use the graph along with admin agent to orchestrate the conversation flow. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs?name=create_group_chat_with_workflow)] \ No newline at end of file diff --git a/dotnet/website/articles/getting-start.md b/dotnet/website/articles/getting-start.md new file mode 100644 index 00000000000..53cc7c9758f --- /dev/null +++ b/dotnet/website/articles/getting-start.md @@ -0,0 +1,24 @@ +### Get start with AutoGen for dotnet +[![dotnet-ci](https://github.com/microsoft/autogen/actions/workflows/dotnet-build.yml/badge.svg)](https://github.com/microsoft/autogen/actions/workflows/dotnet-build.yml) +[![Discord](https://img.shields.io/discord/1153072414184452236?logo=discord&style=flat)](https://discord.gg/pAbnFJrkgZ) +[![NuGet version](https://badge.fury.io/nu/AutoGen.Core.svg)](https://badge.fury.io/nu/AutoGen.Core) + +Firstly, add `AutoGen` package to your project. + +```bash +dotnet add package AutoGen +``` + +> [!NOTE] +> For more information about installing packages, please check out the [installation guide](Installation.md). + +Then you can start with the following code snippet to create a conversable agent and chat with it. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/GetStartCodeSnippet.cs?name=snippet_GetStartCodeSnippet)] +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/GetStartCodeSnippet.cs?name=code_snippet_1)] + +### Examples +You can find more examples under the [sample project](https://github.com/microsoft/autogen/tree/dotnet/dotnet/sample/AutoGen.BasicSamples). + +### Report a bug or request a feature +You can report a bug or request a feature by creating a new issue in the [github issue](https://github.com/microsoft/autogen/issues) and specifying label the label "donet" diff --git a/dotnet/website/articles/toc.yml b/dotnet/website/articles/toc.yml new file mode 100644 index 00000000000..42d34f12af4 --- /dev/null +++ b/dotnet/website/articles/toc.yml @@ -0,0 +1,91 @@ +- name: Getting start + items: + - name: Installation + href: Installation.md + - name: agent + items: + - name: agent overview + href: Agent-overview.md + - name: assistant agent + href: Create-an-agent.md + - name: user proxy agent + href: Create-a-user-proxy-agent.md + - name: Chat with an agent using user proxy agent + href: Two-agent-chat.md + # - name: Create your own agent + # href: Create-your-own-agent.md + - name: built-in messages + href: Built-in-messages.md + - name: function call + items: + - name: Function call overview + href: Function-call-overview.md + - name: Create type-safe function call using AutoGen.SourceGenerator + href: Create-type-safe-function-call.md + - name: Use function call in an agent + href: Use-function-call.md + - name: middleware + items: + - name: middleware overview + href: Middleware-overview.md + - name: built-in middleware and use case + items: + - name: print message + href: Print-message-middleware.md + # - name: function call + # href: Function-call-middleware.md + - name: group chat + items: + - name: group chat overview + href: Group-chat-overview.md + - name: round robin group chat + href: Roundrobin-chat.md + - name: dynamic group chat + href: Group-chat.md + - name: use graph to control dynamic group chat + href: Use-graph-in-group-chat.md + +- name: AutoGen.DotnetInteractive + items: + - name: Execute code snippet + href: Run-dotnet-code.md + +- name: AutoGen.OpenAI + items: + - name: Overview + href: AutoGen-OpenAI-Overview.md + - name: Examples + items: + - name: Simple chat and streaming chat + href: OpenAIChatAgent-simple-chat.md + - name: Support more AutoGen built-in messages + href: OpenAIChatAgent-support-more-messages.md + - name: Use function call in OpenAIChatAgent + href: OpenAIChatAgent-use-function-call.md + - name: Use json mode in OpenAIChatAgent + href: OpenAIChatAgent-use-json-mode.md + +- name: AutoGen.SemanticKernel + items: + - name: Overview + href: AutoGen-SemanticKernel-Overview.md + - name: Simple chat and streaming chat + href: SemanticKernelAgent-simple-chat.md + - name: Use SemanticKernelChatMessageContentConnector to support more AutoGen built-in messages + href: SemanticKernelAgent-support-more-messages.md +- name: AutoGen.Mistral + items: + - name: Overview + href: AutoGen-Mistral-Overview.md + - name: Examples + items: + - name: Use function call in MistralChatAgent + href: MistralChatAgent-use-function-call.md + - name: Count token usage in MistralChatAgent + href: MistralChatAgent-count-token-usage.md + +- name: AutoGen.LMStudio + items: + - name: Consume LLM server from LM Studio + href: Consume-LLM-server-from-LM-Studio.md + diff --git a/dotnet/website/docfx.json b/dotnet/website/docfx.json new file mode 100644 index 00000000000..e06f9797c1f --- /dev/null +++ b/dotnet/website/docfx.json @@ -0,0 +1,68 @@ +{ + "metadata": [ + { + "src": [ + { + "files": ["src/**/*.csproj"], + "src": "../" + } + ], + "dest": "api", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "allowCompilationErrors": false, + "filter": "filterConfig.yml" + } + ], + "build": { + "content": [ + { + "files": [ + "api/**.yml", + "api/index.md" + ] + }, + { + "files": [ + "articles/**.md", + "articles/**/toc.yml", + "toc.yml", + "*.md" + ] + } + ], + "resource": [ + { + "files": [ + "images/**" + ] + } + ], + "output": "_site", + "globalMetadataFiles": [], + "fileMetadataFiles": [], + "template": [ + "default", + "modern", + "template" + ], + "globalMetadata":{ + "_appTitle": "AutoGen for .NET", + "_appName": "AutoGen for .NET", + "_appLogoPath": "images/ag.ico", + "_appFooter": "AutoGen for .NET", + "_appFaviconPath": "images/ag.ico", + "_gitContribute": { + "repo": "https://github.com/microsoft/autogen.git", + "branch": "dotnet" + } + }, + "postProcessors": [], + "keepFileLink": false, + "disableGitFeatures": false + } +} \ No newline at end of file diff --git a/dotnet/website/filterConfig.yml b/dotnet/website/filterConfig.yml new file mode 100644 index 00000000000..936ecbc6718 --- /dev/null +++ b/dotnet/website/filterConfig.yml @@ -0,0 +1,3 @@ +apiRules: +- exclude: + uidRegex: ^AutoGen.SourceGenerator \ No newline at end of file diff --git a/dotnet/website/images/ag.ico b/dotnet/website/images/ag.ico new file mode 100644 index 0000000000000000000000000000000000000000..f1789673b09252f61aedc8932f2dfecb8cd68e8d GIT binary patch literal 3126 zcmY*bX>3(h5PqO2XhFKr_O*TOqwU-8eYX{g1T`W`SQ3_me?&4pJ3V*qoNs2nnK}0i7}h7s zA%A@^`|)$zzYdN>tni2Thr8)d1rA46IGkmgn6o)d8R5ty7G{H_h*4mHS#dOKxI_tKvGpYih9hdi53MjR z!BJIrqrG~ilz+s{K?MbO{?=)ESR!t*~j(2oK8(jNp+<;6OxjW zTuF(^@iDP!DQVhn~A34;jdak`I2A zI+*rkcwT+!#hMev)7}|aUsKWA(%5pPCV#=?Zx&C@{V;ds=ZojRH=?e#CLHoc$HaH< z+V$k&;<~!(zBxGoua{2Z+>@h^#f+-98&lK4Gb$7a_K1nAIJ9N;^69m8XQH0z^!BLX zKkq5Jbnc{O8i&fuW=$A!yy{2?$J0M<{%+^G{JaUn%F8zQ?cFQr4*-*FN1Bd3E@eaK z5rPbuNOq=tQ}9Lk_LYrIS2EqMWuGmmJ$1OTp}Kq2bLY-hR~_D6T3Xz}(ecdj@_Toh zZ(Tost@%Q>Wd(czf(t&9i$UiltwIi4Mhuzgra`au11)#&-+$%hem~?dy?(X6;?UlX z4#$nF7n&MR@7%flsSZy+xO=JP;;9>#PPaX79XV_WdMDE0bd? z-z5r_keZU#+p82Uz2{r+$Bm+dI} z>SDvm_}F+?vTJ_chv>U2SI*7No0%QX8aaIE`I?H0ml{xv;GYF)mpUn8BQvu&PT9y< zGcQb#8Sn?9yLD^5S=(^73V=%%&Q3{kp+Zue2@md_hotq+wdTg^rgK#{S}x**7kQ!w(0l!AopZ?4PW~1?FIk*e*5vC z4<7&ZTT}h95rYTd;G$#pAh^k%l0zF2@lX<$Bnr)bFc9>(Q~RTHEPqB%IhKW9M1jN2 z85a}GbZ4X{#m2;WGrhvI-9$nQ*uV!4=Rc5HNZJc{WIAjb5s2aU1%m#dI;NSjqh^eu zP!J7Cw`HRZ#WiuXKC#BNO7XdtGek2^eSMU|mKu=Hr6y5JLZEDYtThn&p!{$a>v~+< zYq+G~?R9G}Xi-RRhvho0uzY{QQHv(#G=D45~&<)LFMz#{4Zh~2}=L~ literal 0 HcmV?d00001 diff --git a/dotnet/website/images/ag.svg b/dotnet/website/images/ag.svg new file mode 100644 index 00000000000..eba3ee95281 --- /dev/null +++ b/dotnet/website/images/ag.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dotnet/website/images/articles/CreateUserProxyAgent/image-1.png b/dotnet/website/images/articles/CreateUserProxyAgent/image-1.png new file mode 100644 index 00000000000..fd467c44af7 --- /dev/null +++ b/dotnet/website/images/articles/CreateUserProxyAgent/image-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91813a034edc3918a27758296d77150d1c8d650911847bdc6a42cca79307714a +size 9009 diff --git a/dotnet/website/images/articles/DynamicGroupChat/dynamicChat.gif b/dotnet/website/images/articles/DynamicGroupChat/dynamicChat.gif new file mode 100644 index 00000000000..d756f674114 --- /dev/null +++ b/dotnet/website/images/articles/DynamicGroupChat/dynamicChat.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cba3069e9669a1b8013f0b2fa4d191c1d7b0b7919b1664f1f8ec98a90c7a2b2 +size 411517 diff --git a/dotnet/website/images/articles/PrintMessageMiddleware/printMessage.png b/dotnet/website/images/articles/PrintMessageMiddleware/printMessage.png new file mode 100644 index 00000000000..db31ade0de8 --- /dev/null +++ b/dotnet/website/images/articles/PrintMessageMiddleware/printMessage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ec3bc40d4e3c1228d5799e448a34521998e7abb700bc978afc790389805ecb4 +size 86924 diff --git a/dotnet/website/images/articles/PrintMessageMiddleware/streamingoutput.gif b/dotnet/website/images/articles/PrintMessageMiddleware/streamingoutput.gif new file mode 100644 index 00000000000..a2afd4f5847 --- /dev/null +++ b/dotnet/website/images/articles/PrintMessageMiddleware/streamingoutput.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95feb667fe74177506435ca52fcf183fb187a3a407fac0b3b220bd9e8da721c7 +size 547023 diff --git a/dotnet/website/images/articles/SequentialGroupChat/SearcherSummarizer.gif b/dotnet/website/images/articles/SequentialGroupChat/SearcherSummarizer.gif new file mode 100644 index 00000000000..250bf00b8dc --- /dev/null +++ b/dotnet/website/images/articles/SequentialGroupChat/SearcherSummarizer.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6d8a5a534efaf49ecc796ad3ca8e62fb7a236b55d894bda7a0c258564195b5d +size 620269 diff --git a/dotnet/website/index.md b/dotnet/website/index.md new file mode 100644 index 00000000000..3bc691523e9 --- /dev/null +++ b/dotnet/website/index.md @@ -0,0 +1,4 @@ +--- +_disableTocFilter: true +--- +[!INCLUDE [](./articles/getting-start.md)] \ No newline at end of file diff --git a/dotnet/website/toc.yml b/dotnet/website/toc.yml new file mode 100644 index 00000000000..3931f5e7947 --- /dev/null +++ b/dotnet/website/toc.yml @@ -0,0 +1,14 @@ +- name: Docs + href: articles/ + +- name: API Reference + href: api/ + +- name: Update Log + href: update.md + +- name: Other Languages + dropdown: true + items: + - name: Python + href: https://microsoft.github.io/autogen/ diff --git a/dotnet/website/update.md b/dotnet/website/update.md new file mode 100644 index 00000000000..a97b9480514 --- /dev/null +++ b/dotnet/website/update.md @@ -0,0 +1,45 @@ +##### Update on 0.0.12 (2024-04-22) +- Add AutoGen.Mistral package to support Mistral.AI models + +##### Update on 0.0.11 (2024-04-10) +- Add link to Discord channel in nuget's readme.md +- Document improvements +- In `AutoGen.OpenAI`, update `Azure.AI.OpenAI` to 1.0.0-beta.15 and add support for json mode and deterministic output in `OpenAIChatAgent` [Issue #2346](https://github.com/microsoft/autogen/issues/2346) +- In `AutoGen.SemanticKernel`, update `SemanticKernel` package to 1.7.1 +- [API Breaking Change] Rename `PrintMessageMiddlewareExtension.RegisterPrintFormatMessageHook' to `PrintMessageMiddlewareExtension.RegisterPrintMessage`. +##### Update on 0.0.10 (2024-03-12) +- Rename `Workflow` to `Graph` +- Rename `AddInitializeMessage` to `SendIntroduction` +- Rename `SequentialGroupChat` to `RoundRobinGroupChat` +##### Update on 0.0.9 (2024-03-02) +- Refactor over @AutoGen.Message and introducing `TextMessage`, `ImageMessage`, `MultiModalMessage` and so on. PR [#1676](https://github.com/microsoft/autogen/pull/1676) +- Add `AutoGen.SemanticKernel` to support seamless integration with Semantic Kernel +- Move the agent contract abstraction to `AutoGen.Core` package. The `AutoGen.Core` package provides the abstraction for message type, agent and group chat and doesn't contain dependencies over `Azure.AI.OpenAI` or `Semantic Kernel`. This is useful when you want to leverage AutoGen's abstraction only and want to avoid introducing any other dependencies. +- Move `GPTAgent`, `OpenAIChatAgent` and all openai-dependencies to `AutoGen.OpenAI` +##### Update on 0.0.8 (2024-02-28) +- Fix [#1804](https://github.com/microsoft/autogen/pull/1804) +- Streaming support for IAgent [#1656](https://github.com/microsoft/autogen/pull/1656) +- Streaming support for middleware via `MiddlewareStreamingAgent` [#1656](https://github.com/microsoft/autogen/pull/1656) +- Graph chat support with conditional transition workflow [#1761](https://github.com/microsoft/autogen/pull/1761) +- AutoGen.SourceGenerator: Generate `FunctionContract` from `FunctionAttribute` [#1736](https://github.com/microsoft/autogen/pull/1736) +##### Update on 0.0.7 (2024-02-11) +- Add `AutoGen.LMStudio` to support comsume openai-like API from LMStudio local server +##### Update on 0.0.6 (2024-01-23) +- Add `MiddlewareAgent` +- Use `MiddlewareAgent` to implement existing agent hooks (RegisterPreProcess, RegisterPostProcess, RegisterReply) +- Remove `AutoReplyAgent`, `PreProcessAgent`, `PostProcessAgent` because they are replaced by `MiddlewareAgent` +##### Update on 0.0.5 +- Simplify `IAgent` interface by removing `ChatLLM` Property +- Add `GenerateReplyOptions` to `IAgent.GenerateReplyAsync` which allows user to specify or override the options when generating reply + +##### Update on 0.0.4 +- Move out dependency of Semantic Kernel +- Add type `IChatLLM` as connector to LLM + +##### Update on 0.0.3 +- In AutoGen.SourceGenerator, rename FunctionAttribution to FunctionAttribute +- In AutoGen, refactor over ConversationAgent, UserProxyAgent, and AssistantAgent + +##### Update on 0.0.2 +- update Azure.OpenAI.AI to 1.0.0-beta.12 +- update Semantic kernel to 1.0.1 \ No newline at end of file From 1b8d65df0a54354b5fec152f9aa4162827a7fb2d Mon Sep 17 00:00:00 2001 From: Audel Rouhi Date: Sun, 28 Apr 2024 06:43:02 -0700 Subject: [PATCH 13/30] 2447 fix pgvector tests and notebook (#2455) * Re-added missing notebook * Test installing postgres * Error handle the connection. * Fixed import. * Fixed import. * Fixed creation of collection without client. * PGVector portion working. OpenAI untested. * Fixed prints. * Added output. * Fixed pre-commits. * Run pgvector notebook * Improve efficiency of get_collection * Fix delete_collection * Fixed issues with pytests and validated functions. * Validated pytests. * Fixed pre-commits * Separated extra_requires to allow more logic. Retrieve_chat base dependencies included on pgvector and qdrant. * Fixed extra newline. * Added username and password fields. * URL Encode the connection string parameters to support symbols like % * Fixed pre-commits. * Added pgvector service * pgvector doesn't have health intervals. * Switched to colon based key values. * Run on Ubuntu only. Linux is only option with container service support. * Using default credentials instead. * Fix postgres setup * Fix postgres setup * Don't skip tests on win and mac * Fix command error * Try apt install postgresql * Assert table does not exist when deleted. * Raise value error on a empty list or None value provided for IDs * pre-commit * Add install pgvector * Add install pgvector * Reorg test files, create a separate job for test pgvector * Fix format * Fix env format * Simplify job name, enable test_retrieve_config * Fix test_retrieve_config * Corrected behavior for get_docs_by_ids with no ids returning all docs. * Corrected behavior for get_docs_by_ids with no ids returning all docs. * Fixed pre-commits. * Added return values for all functions. * Validated distance search is implemented correctly. * Validated all pytests * Removed print. * Added default clause. * Make ids optional * Fix test, make it more robust * Bump version of openai for the vector_store support * Added support for choosing the sentence transformer model. * Added error handling for model name entered. * Updated model info. * Added model_name db_config param. * pre-commit fixes and last link fix. * Use secrets password. * fix: link fixed * updated tests * Updated config_list. * pre-commit fix. * Added chat_result to all output. Unable to re-run notebooks. * Pre-commit fix detected this requirement. * Fix python 3.8 and 3.9 not supported for macos * Fix python 3.8 and 3.9 not supported for macos * Fix format * Reran notebook with MetaLlama3Instruct7BQ4_k_M * added gpt model. * Reran notebook --------- Co-authored-by: Li Jiang Co-authored-by: Hk669 --- .github/workflows/contrib-openai.yml | 25 +- .github/workflows/contrib-tests.yml | 78 +- .../agentchat/contrib/vectordb/pgvectordb.py | 280 ++- notebook/agentchat_RetrieveChat.ipynb | 39 +- ...agentchat_function_call_code_writing.ipynb | 1 + .../agentchat_pgvector_RetrieveChat.ipynb | 2155 +++-------------- setup.py | 69 +- .../test_pgvector_retrievechat.py | 39 +- .../test_qdrant_retrievechat.py | 6 +- .../{ => retrievechat}/test_retrievechat.py | 30 +- .../contrib/vectordb/test_pgvectordb.py | 30 +- 11 files changed, 784 insertions(+), 1968 deletions(-) rename test/agentchat/contrib/{ => retrievechat}/test_pgvector_retrievechat.py (77%) rename test/agentchat/contrib/{ => retrievechat}/test_qdrant_retrievechat.py (95%) rename test/agentchat/contrib/{ => retrievechat}/test_retrievechat.py (75%) diff --git a/.github/workflows/contrib-openai.yml b/.github/workflows/contrib-openai.yml index c60a45b3ad1..73c2197c27e 100644 --- a/.github/workflows/contrib-openai.yml +++ b/.github/workflows/contrib-openai.yml @@ -24,6 +24,21 @@ jobs: python-version: ["3.10"] runs-on: ${{ matrix.os }} environment: openai1 + services: + pgvector: + image: ankane/pgvector + env: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + POSTGRES_HOST_AUTH_METHOD: trust + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 steps: # checkout to pr branch - name: Checkout @@ -41,15 +56,10 @@ jobs: pip install -e . python -c "import autogen" pip install coverage pytest-asyncio - - name: Install PostgreSQL - run: | - sudo apt install postgresql -y - - name: Start PostgreSQL service - run: sudo service postgresql start - name: Install packages for test when needed run: | pip install docker - pip install -e .[retrievechat-qdrant,retrievechat-pgvector] + pip install -e .[retrievechat,retrievechat-qdrant,retrievechat-pgvector] - name: Coverage env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -57,13 +67,14 @@ jobs: AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }} OAI_CONFIG_LIST: ${{ secrets.OAI_CONFIG_LIST }} run: | - coverage run -a -m pytest test/agentchat/contrib/test_retrievechat.py::test_retrievechat test/agentchat/contrib/test_qdrant_retrievechat.py::test_retrievechat test/agentchat/contrib/test_pgvector_retrievechat.py::test_retrievechat + coverage run -a -m pytest -k test_retrievechat test/agentchat/contrib/retrievechat coverage xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: file: ./coverage.xml flags: unittests + CompressionTest: strategy: matrix: diff --git a/.github/workflows/contrib-tests.yml b/.github/workflows/contrib-tests.yml index 9766addcdcb..46c8433e1f7 100644 --- a/.github/workflows/contrib-tests.yml +++ b/.github/workflows/contrib-tests.yml @@ -27,7 +27,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-2019] + os: [macos-latest, windows-2019] python-version: ["3.9", "3.10", "3.11"] exclude: - os: macos-latest @@ -45,30 +45,82 @@ jobs: - name: Install qdrant_client when python-version is 3.10 if: matrix.python-version == '3.10' run: | - pip install .[retrievechat-qdrant] + pip install -e .[retrievechat-qdrant] + - name: Install packages and dependencies for RetrieveChat + run: | + pip install -e .[retrievechat] + - name: Set AUTOGEN_USE_DOCKER based on OS + shell: bash + run: | + if [[ ${{ matrix.os }} != ubuntu-latest ]]; then + echo "AUTOGEN_USE_DOCKER=False" >> $GITHUB_ENV + fi + - name: Coverage + run: | + pip install coverage>=5.3 + coverage run -a -m pytest test/test_retrieve_utils.py test/agentchat/contrib/retrievechat test/agentchat/contrib/vectordb --skip-openai + coverage xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: unittests + + RetrieveChatTest-Ubuntu: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11"] + services: + pgvector: + image: ankane/pgvector + env: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + POSTGRES_HOST_AUTH_METHOD: trust + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install packages and dependencies for all tests + run: | + python -m pip install --upgrade pip wheel + pip install pytest + - name: Install qdrant_client when python-version is 3.10 + if: matrix.python-version == '3.10' + run: | + pip install -e .[retrievechat-qdrant] + - name: Install pgvector when on linux + run: | + pip install -e .[retrievechat-pgvector] - name: Install unstructured when python-version is 3.9 and on linux + if: matrix.python-version == '3.9' run: | sudo apt-get update sudo apt-get install -y tesseract-ocr poppler-utils pip install unstructured[all-docs]==0.13.0 - - name: Install and Start PostgreSQL - runs-on: ubuntu-latest + - name: Install packages and dependencies for RetrieveChat run: | - sudo apt install postgresql -y - sudo service postgresql start - - name: Install packages and dependencies for PGVector - run: | - pip install -e .[retrievechat-pgvector] + pip install -e .[retrievechat] - name: Set AUTOGEN_USE_DOCKER based on OS shell: bash run: | - if [[ ${{ matrix.os }} != ubuntu-latest ]]; then - echo "AUTOGEN_USE_DOCKER=False" >> $GITHUB_ENV - fi + echo "AUTOGEN_USE_DOCKER=False" >> $GITHUB_ENV - name: Coverage run: | pip install coverage>=5.3 - coverage run -a -m pytest test/test_retrieve_utils.py test/agentchat/contrib/test_retrievechat.py test/agentchat/contrib/test_qdrant_retrievechat.py test/agentchat/contrib/vectordb --skip-openai + coverage run -a -m pytest test/test_retrieve_utils.py test/agentchat/contrib/retrievechat test/agentchat/contrib/vectordb --skip-openai coverage xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 diff --git a/autogen/agentchat/contrib/vectordb/pgvectordb.py b/autogen/agentchat/contrib/vectordb/pgvectordb.py index ae9d5cbbbec..b5db55f7eb1 100644 --- a/autogen/agentchat/contrib/vectordb/pgvectordb.py +++ b/autogen/agentchat/contrib/vectordb/pgvectordb.py @@ -1,5 +1,6 @@ import os import re +import urllib.parse from typing import Callable, List import numpy as np @@ -33,7 +34,8 @@ class Collection: embedding_function (Callable): The embedding function used to generate the vector representation. metadata (Optional[dict]): The metadata of the collection. get_or_create (Optional): The flag indicating whether to get or create the collection. - + model_name: (Optional str) | Sentence embedding model to use. Models can be chosen from: + https://huggingface.co/models?library=sentence-transformers """ def __init__( @@ -43,6 +45,7 @@ def __init__( embedding_function: Callable = None, metadata=None, get_or_create=None, + model_name="all-MiniLM-L6-v2", ): """ Initialize the Collection object. @@ -53,46 +56,76 @@ def __init__( embedding_function: The embedding function used to generate the vector representation. metadata: The metadata of the collection. get_or_create: The flag indicating whether to get or create the collection. - + model_name: | Sentence embedding model to use. Models can be chosen from: + https://huggingface.co/models?library=sentence-transformers Returns: None """ self.client = client self.embedding_function = embedding_function + self.model_name = model_name self.name = self.set_collection_name(collection_name) self.require_embeddings_or_documents = False self.ids = [] - self.embedding_function = ( - SentenceTransformer("all-MiniLM-L6-v2") if embedding_function is None else embedding_function - ) + try: + self.embedding_function = ( + SentenceTransformer(self.model_name) if embedding_function is None else embedding_function + ) + except Exception as e: + logger.error( + f"Validate the model name entered: {self.model_name} " + f"from https://huggingface.co/models?library=sentence-transformers\nError: {e}" + ) + raise e self.metadata = metadata if metadata else {"hnsw:space": "ip", "hnsw:construction_ef": 32, "hnsw:M": 16} self.documents = "" self.get_or_create = get_or_create - def set_collection_name(self, collection_name): + def set_collection_name(self, collection_name) -> str: name = re.sub("-", "_", collection_name) self.name = name return self.name - def add(self, ids: List[ItemID], embeddings: List, metadatas: List, documents: List): + def add(self, ids: List[ItemID], documents: List, embeddings: List = None, metadatas: List = None) -> None: """ Add documents to the collection. Args: ids (List[ItemID]): A list of document IDs. - embeddings (List): A list of document embeddings. - metadatas (List): A list of document metadatas. + embeddings (List): A list of document embeddings. Optional + metadatas (List): A list of document metadatas. Optional documents (List): A list of documents. Returns: None """ cursor = self.client.cursor() - sql_values = [] - for doc_id, embedding, metadata, document in zip(ids, embeddings, metadatas, documents): - sql_values.append((doc_id, embedding, metadata, document)) - sql_string = f"INSERT INTO {self.name} (id, embedding, metadata, document) " f"VALUES (%s, %s, %s, %s);" + if embeddings is not None and metadatas is not None: + for doc_id, embedding, metadata, document in zip(ids, embeddings, metadatas, documents): + metadata = re.sub("'", '"', str(metadata)) + sql_values.append((doc_id, embedding, metadata, document)) + sql_string = ( + f"INSERT INTO {self.name} (id, embedding, metadatas, documents)\n" f"VALUES (%s, %s, %s, %s);\n" + ) + elif embeddings is not None: + for doc_id, embedding, document in zip(ids, embeddings, documents): + sql_values.append((doc_id, embedding, document)) + sql_string = f"INSERT INTO {self.name} (id, embedding, documents) " f"VALUES (%s, %s, %s);\n" + elif metadatas is not None: + for doc_id, metadata, document in zip(ids, metadatas, documents): + metadata = re.sub("'", '"', str(metadata)) + embedding = self.embedding_function.encode(document) + sql_values.append((doc_id, metadata, embedding, document)) + sql_string = ( + f"INSERT INTO {self.name} (id, metadatas, embedding, documents)\n" f"VALUES (%s, %s, %s, %s);\n" + ) + else: + for doc_id, document in zip(ids, documents): + embedding = self.embedding_function.encode(document) + sql_values.append((doc_id, document, embedding)) + sql_string = f"INSERT INTO {self.name} (id, documents, embedding)\n" f"VALUES (%s, %s, %s);\n" + logger.debug(f"Add SQL String:\n{sql_string}\n{sql_values}") cursor.executemany(sql_string, sql_values) cursor.close() @@ -155,7 +188,7 @@ def upsert(self, ids: List[ItemID], documents: List, embeddings: List = None, me cursor.executemany(sql_string, sql_values) cursor.close() - def count(self): + def count(self) -> int: """ Get the total number of documents in the collection. @@ -173,7 +206,32 @@ def count(self): total = None return total - def get(self, ids=None, include=None, where=None, limit=None, offset=None): + def table_exists(self, table_name: str) -> bool: + """ + Check if a table exists in the PostgreSQL database. + + Args: + table_name (str): The name of the table to check. + + Returns: + bool: True if the table exists, False otherwise. + """ + + cursor = self.client.cursor() + cursor.execute( + """ + SELECT EXISTS ( + SELECT 1 + FROM information_schema.tables + WHERE table_name = %s + ) + """, + (table_name,), + ) + exists = cursor.fetchone()[0] + return exists + + def get(self, ids=None, include=None, where=None, limit=None, offset=None) -> List[Document]: """ Retrieve documents from the collection. @@ -188,39 +246,65 @@ def get(self, ids=None, include=None, where=None, limit=None, offset=None): List: The retrieved documents. """ cursor = self.client.cursor() + + # Initialize variables for query components + select_clause = "SELECT id, metadatas, documents, embedding" + from_clause = f"FROM {self.name}" + where_clause = "" + limit_clause = "" + offset_clause = "" + + # Handle include clause if include: - query = f'SELECT (id, {", ".join(map(str, include))}, embedding) FROM {self.name}' - else: - query = f"SELECT * FROM {self.name}" + select_clause = f"SELECT id, {', '.join(include)}, embedding" + + # Handle where clause if ids: - query = f"{query} WHERE id IN {ids}" + where_clause = f"WHERE id IN ({', '.join(['%s' for _ in ids])})" elif where: - query = f"{query} WHERE {where}" - if offset: - query = f"{query} OFFSET {offset}" + where_clause = f"WHERE {where}" + + # Handle limit and offset clauses if limit: - query = f"{query} LIMIT {limit}" - retreived_documents = [] + limit_clause = "LIMIT %s" + if offset: + offset_clause = "OFFSET %s" + + # Construct the full query + query = f"{select_clause} {from_clause} {where_clause} {limit_clause} {offset_clause}" + + retrieved_documents = [] try: - cursor.execute(query) + # Execute the query with the appropriate values + if ids is not None: + cursor.execute(query, ids) + else: + query_params = [] + if limit: + query_params.append(limit) + if offset: + query_params.append(offset) + cursor.execute(query, query_params) + retrieval = cursor.fetchall() for retrieved_document in retrieval: - retreived_documents.append( + retrieved_documents.append( Document( - id=retrieved_document[0][0], - metadata=retrieved_document[0][1], - content=retrieved_document[0][2], - embedding=retrieved_document[0][3], + id=retrieved_document[0].strip(), + metadata=retrieved_document[1], + content=retrieved_document[2], + embedding=retrieved_document[3], ) ) - except (psycopg.errors.UndefinedTable, psycopg.errors.UndefinedColumn): - logger.info(f"Error executing select on non-existent table: {self.name}. Creating it instead.") + except (psycopg.errors.UndefinedTable, psycopg.errors.UndefinedColumn) as e: + logger.info(f"Error executing select on non-existent table: {self.name}. Creating it instead. Error: {e}") self.create_collection(collection_name=self.name) logger.info(f"Created table {self.name}") + cursor.close() - return retreived_documents + return retrieved_documents - def update(self, ids: List, embeddings: List, metadatas: List, documents: List): + def update(self, ids: List, embeddings: List, metadatas: List, documents: List) -> None: """ Update documents in the collection. @@ -300,6 +384,7 @@ def query( n_results: int = 10, distance_type: str = "euclidean", distance_threshold: float = -1, + include_embedding: bool = False, ) -> QueryResults: """ Query documents in the collection. @@ -310,21 +395,25 @@ def query( n_results (int): The maximum number of results to return. distance_type (Optional[str]): Distance search type - euclidean or cosine distance_threshold (Optional[float]): Distance threshold to limit searches + include_embedding (Optional[bool]): Include embedding values in QueryResults Returns: QueryResults: The query results. """ if collection_name: self.name = collection_name + clause = "ORDER BY" if distance_threshold == -1: distance_threshold = "" + clause = "ORDER BY" elif distance_threshold > 0: distance_threshold = f"< {distance_threshold}" + clause = "WHERE" cursor = self.client.cursor() results = [] - for query in query_texts: - vector = self.embedding_function.encode(query, convert_to_tensor=False).tolist() + for query_text in query_texts: + vector = self.embedding_function.encode(query_text, convert_to_tensor=False).tolist() if distance_type.lower() == "cosine": index_function = "<=>" elif distance_type.lower() == "euclidean": @@ -333,15 +422,16 @@ def query( index_function = "<#>" else: index_function = "<->" - query = ( - f"SELECT id, documents, embedding, metadatas FROM {self.name}\n" - f"ORDER BY embedding {index_function} '{str(vector)}'::vector {distance_threshold}\n" + f"SELECT id, documents, embedding, metadatas " + f"FROM {self.name} " + f"{clause} embedding {index_function} '{str(vector)}' {distance_threshold} " f"LIMIT {n_results}" ) cursor.execute(query) + result = [] for row in cursor.fetchall(): - fetched_document = Document(id=row[0], content=row[1], embedding=row[2], metadata=row[3]) + fetched_document = Document(id=row[0].strip(), content=row[1], embedding=row[2], metadata=row[3]) fetched_document_array = self.convert_string_to_array(array_string=fetched_document.get("embedding")) if distance_type.lower() == "cosine": distance = self.cosine_distance(fetched_document_array, vector) @@ -351,9 +441,11 @@ def query( distance = self.inner_product_distance(fetched_document_array, vector) else: distance = self.euclidean_distance(fetched_document_array, vector) - results.append((fetched_document, distance)) + if not include_embedding: + fetched_document = Document(id=row[0].strip(), content=row[1], metadata=row[3]) + result.append((fetched_document, distance)) + results.append(result) cursor.close() - results = [results] logger.debug(f"Query Results: {results}") return results @@ -375,7 +467,7 @@ def convert_string_to_array(array_string) -> List[float]: array = [float(num) for num in array_string.split()] return array - def modify(self, metadata, collection_name: str = None): + def modify(self, metadata, collection_name: str = None) -> None: """ Modify metadata for the collection. @@ -394,7 +486,7 @@ def modify(self, metadata, collection_name: str = None): ) cursor.close() - def delete(self, ids: List[ItemID], collection_name: str = None): + def delete(self, ids: List[ItemID], collection_name: str = None) -> None: """ Delete documents from the collection. @@ -408,10 +500,11 @@ def delete(self, ids: List[ItemID], collection_name: str = None): if collection_name: self.name = collection_name cursor = self.client.cursor() - cursor.execute(f"DELETE FROM {self.name} WHERE id IN ({ids});") + id_placeholders = ", ".join(["%s" for _ in ids]) + cursor.execute(f"DELETE FROM {self.name} WHERE id IN ({id_placeholders});", ids) cursor.close() - def delete_collection(self, collection_name: str = None): + def delete_collection(self, collection_name: str = None) -> None: """ Delete the entire collection. @@ -427,7 +520,7 @@ def delete_collection(self, collection_name: str = None): cursor.execute(f"DROP TABLE IF EXISTS {self.name}") cursor.close() - def create_collection(self, collection_name: str = None): + def create_collection(self, collection_name: str = None) -> None: """ Create a new collection. @@ -468,9 +561,12 @@ def __init__( host: str = None, port: int = None, dbname: str = None, + username: str = None, + password: str = None, connect_timeout: int = 10, embedding_function: Callable = None, metadata: dict = None, + model_name: str = "all-MiniLM-L6-v2", ) -> None: """ Initialize the vector database. @@ -482,6 +578,8 @@ def __init__( host: str | The host to connect to. Default is None. port: int | The port to connect to. Default is None. dbname: str | The database name to connect to. Default is None. + username: str | The database username to use. Default is None. + password: str | The database user password to use. Default is None. connect_timeout: int | The timeout to set for the connection. Default is 10. embedding_function: Callable | The embedding function used to generate the vector representation of the documents. Default is None. @@ -489,20 +587,48 @@ def __init__( setting: {"hnsw:space": "ip", "hnsw:construction_ef": 30, "hnsw:M": 16}. Creates Index on table using hnsw (embedding vector_l2_ops) WITH (m = hnsw:M) ef_construction = "hnsw:construction_ef". For more info: https://github.com/pgvector/pgvector?tab=readme-ov-file#hnsw - kwargs: dict | Additional keyword arguments. + model_name: str | Sentence embedding model to use. Models can be chosen from: + https://huggingface.co/models?library=sentence-transformers Returns: None """ - if connection_string: - self.client = psycopg.connect(conninfo=connection_string, autocommit=True) - elif host and port and dbname: - self.client = psycopg.connect( - host=host, port=port, dbname=dbname, connect_timeout=connect_timeout, autocommit=True + try: + if connection_string: + parsed_connection = urllib.parse.urlparse(connection_string) + encoded_username = urllib.parse.quote(parsed_connection.username, safe="") + encoded_password = urllib.parse.quote(parsed_connection.password, safe="") + encoded_host = urllib.parse.quote(parsed_connection.hostname, safe="") + encoded_database = urllib.parse.quote(parsed_connection.path[1:], safe="") + connection_string_encoded = ( + f"{parsed_connection.scheme}://{encoded_username}:{encoded_password}" + f"@{encoded_host}:{parsed_connection.port}/{encoded_database}" + ) + self.client = psycopg.connect(conninfo=connection_string_encoded, autocommit=True) + elif host and port and dbname: + self.client = psycopg.connect( + host=host, + port=port, + dbname=dbname, + username=username, + password=password, + connect_timeout=connect_timeout, + autocommit=True, + ) + except psycopg.Error as e: + logger.error("Error connecting to the database: ", e) + raise e + self.model_name = model_name + try: + self.embedding_function = ( + SentenceTransformer(self.model_name) if embedding_function is None else embedding_function ) - self.embedding_function = ( - SentenceTransformer("all-MiniLM-L6-v2") if embedding_function is None else embedding_function - ) + except Exception as e: + logger.error( + f"Validate the model name entered: {self.model_name} " + f"from https://huggingface.co/models?library=sentence-transformers\nError: {e}" + ) + raise e self.metadata = metadata self.client.execute("CREATE EXTENSION IF NOT EXISTS vector") register_vector(self.client) @@ -535,10 +661,12 @@ def create_collection( collection = None if collection is None: collection = Collection( + client=self.client, collection_name=collection_name, embedding_function=self.embedding_function, get_or_create=get_or_create, metadata=self.metadata, + model_name=self.model_name, ) collection.set_collection_name(collection_name=collection_name) collection.create_collection(collection_name=collection_name) @@ -546,16 +674,30 @@ def create_collection( elif overwrite: self.delete_collection(collection_name) collection = Collection( + client=self.client, collection_name=collection_name, embedding_function=self.embedding_function, get_or_create=get_or_create, metadata=self.metadata, + model_name=self.model_name, ) collection.set_collection_name(collection_name=collection_name) collection.create_collection(collection_name=collection_name) return collection elif get_or_create: return collection + elif not collection.table_exists(table_name=collection_name): + collection = Collection( + client=self.client, + collection_name=collection_name, + embedding_function=self.embedding_function, + get_or_create=get_or_create, + metadata=self.metadata, + model_name=self.model_name, + ) + collection.set_collection_name(collection_name=collection_name) + collection.create_collection(collection_name=collection_name) + return collection else: raise ValueError(f"Collection {collection_name} already exists.") @@ -578,9 +720,13 @@ def get_collection(self, collection_name: str = None) -> Collection: f"No collection is specified. Using current active collection {self.active_collection.name}." ) else: - self.active_collection = Collection( - client=self.client, collection_name=collection_name, embedding_function=self.embedding_function - ) + if not (self.active_collection and self.active_collection.name == collection_name): + self.active_collection = Collection( + client=self.client, + collection_name=collection_name, + embedding_function=self.embedding_function, + model_name=self.model_name, + ) return self.active_collection def delete_collection(self, collection_name: str) -> None: @@ -593,16 +739,20 @@ def delete_collection(self, collection_name: str) -> None: Returns: None """ - self.active_collection.delete_collection(collection_name) + if self.active_collection: + self.active_collection.delete_collection(collection_name) + else: + collection = self.get_collection(collection_name) + collection.delete_collection(collection_name) if self.active_collection and self.active_collection.name == collection_name: self.active_collection = None def _batch_insert( self, collection: Collection, embeddings=None, ids=None, metadatas=None, documents=None, upsert=False - ): + ) -> None: batch_size = int(PGVECTOR_MAX_BATCH_SIZE) default_metadata = {"hnsw:space": "ip", "hnsw:construction_ef": 32, "hnsw:M": 16} - default_metadatas = [default_metadata] + default_metadatas = [default_metadata] * min(batch_size, len(documents)) for i in range(0, len(documents), min(batch_size, len(documents))): end_idx = i + min(batch_size, len(documents) - i) collection_kwargs = { @@ -715,12 +865,14 @@ def retrieve_docs( logger.debug(f"Retrieve Docs Results:\n{results}") return results - def get_docs_by_ids(self, ids: List[ItemID], collection_name: str = None, include=None, **kwargs) -> List[Document]: + def get_docs_by_ids( + self, ids: List[ItemID] = None, collection_name: str = None, include=None, **kwargs + ) -> List[Document]: """ Retrieve documents from the collection of the vector database based on the ids. Args: - ids: List[ItemID] | A list of document ids. + ids: List[ItemID] | A list of document ids. If None, will return all the documents. Default is None. collection_name: str | The name of the collection. Default is None. include: List[str] | The fields to include. Default is None. If None, will include ["metadatas", "documents"], ids will always be included. diff --git a/notebook/agentchat_RetrieveChat.ipynb b/notebook/agentchat_RetrieveChat.ipynb index c0c681350f2..adb13ac47bd 100644 --- a/notebook/agentchat_RetrieveChat.ipynb +++ b/notebook/agentchat_RetrieveChat.ipynb @@ -48,14 +48,14 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "models to use: ['gpt-35-turbo', 'gpt-35-turbo-0613']\n" + "models to use: ['gpt-3.5-turbo-0125']\n" ] } ], @@ -73,7 +73,9 @@ "# a vector database instance\n", "from autogen.retrieve_utils import TEXT_FORMATS\n", "\n", - "config_list = autogen.config_list_from_json(env_or_file=\"OAI_CONFIG_LIST\")\n", + "config_list = [\n", + " {\"model\": \"gpt-3.5-turbo-0125\", \"api_key\": \"\", \"api_type\": \"openai\"},\n", + "]\n", "\n", "assert len(config_list) > 0\n", "print(\"models to use: \", [config_list[i][\"model\"] for i in range(len(config_list))])" @@ -105,7 +107,7 @@ "output_type": "stream", "text": [ "Accepted file formats for `docs_path`:\n", - "['ppt', 'jsonl', 'csv', 'yaml', 'rst', 'htm', 'pdf', 'tsv', 'doc', 'docx', 'pptx', 'msg', 'yml', 'xml', 'md', 'json', 'txt', 'epub', 'org', 'xlsx', 'log', 'html', 'odt', 'rtf']\n" + "['odt', 'xml', 'pdf', 'docx', 'html', 'md', 'htm', 'csv', 'rst', 'org', 'ppt', 'doc', 'log', 'json', 'epub', 'jsonl', 'pptx', 'yml', 'xlsx', 'tsv', 'txt', 'yaml', 'msg', 'rtf']\n" ] } ], @@ -118,16 +120,7 @@ "cell_type": "code", "execution_count": 3, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages/transformers/utils/generic.py:311: UserWarning: torch.utils._pytree._register_pytree_node is deprecated. Please use torch.utils._pytree.register_pytree_node instead.\n", - " torch.utils._pytree._register_pytree_node(\n" - ] - } - ], + "outputs": [], "source": [ "# 1. create an RetrieveAssistantAgent instance named \"assistant\"\n", "assistant = RetrieveAssistantAgent(\n", @@ -500,7 +493,7 @@ "# The conversation continues until the termination condition is met, in RetrieveChat, the termination condition when no human-in-loop is no code block detected.\n", "# With human-in-loop, the conversation will continue until the user says \"exit\".\n", "code_problem = \"How can I use FLAML to perform a classification task and use spark to do parallel training. Train 30 seconds and force cancel jobs if time limit is reached.\"\n", - "ragproxyagent.initiate_chat(\n", + "chat_result = ragproxyagent.initiate_chat(\n", " assistant, message=ragproxyagent.message_generator, problem=code_problem, search_string=\"spark\"\n", ") # search_string is used as an extra filter for the embeddings search, in this case, we only want to search documents that contain \"spark\"." ] @@ -822,7 +815,7 @@ "assistant.reset()\n", "\n", "qa_problem = \"Who is the author of FLAML?\"\n", - "ragproxyagent.initiate_chat(assistant, message=ragproxyagent.message_generator, problem=qa_problem)" + "chat_result = ragproxyagent.initiate_chat(assistant, message=ragproxyagent.message_generator, problem=qa_problem)" ] }, { @@ -1235,7 +1228,7 @@ "# set `human_input_mode` to be `ALWAYS`, so the agent will ask for human input at every step.\n", "ragproxyagent.human_input_mode = \"ALWAYS\"\n", "code_problem = \"how to build a time series forecasting model for stock price using FLAML?\"\n", - "ragproxyagent.initiate_chat(assistant, message=ragproxyagent.message_generator, problem=code_problem)" + "chat_result = ragproxyagent.initiate_chat(assistant, message=ragproxyagent.message_generator, problem=code_problem)" ] }, { @@ -1793,7 +1786,7 @@ "# set `human_input_mode` to be `ALWAYS`, so the agent will ask for human input at every step.\n", "ragproxyagent.human_input_mode = \"ALWAYS\"\n", "qa_problem = \"Is there a function named `tune_automl` in FLAML?\"\n", - "ragproxyagent.initiate_chat(\n", + "chat_result = ragproxyagent.initiate_chat(\n", " assistant, message=ragproxyagent.message_generator, problem=qa_problem\n", ") # type \"exit\" to exit the conversation" ] @@ -2386,7 +2379,9 @@ " assistant.reset()\n", "\n", " qa_problem = questions[i]\n", - " ragproxyagent.initiate_chat(assistant, message=ragproxyagent.message_generator, problem=qa_problem, n_results=30)" + " chat_result = ragproxyagent.initiate_chat(\n", + " assistant, message=ragproxyagent.message_generator, problem=qa_problem, n_results=30\n", + " )" ] }, { @@ -2813,7 +2808,9 @@ " assistant.reset()\n", "\n", " qa_problem = questions[i]\n", - " ragproxyagent.initiate_chat(assistant, message=ragproxyagent.message_generator, problem=qa_problem, n_results=10)" + " chat_result = ragproxyagent.initiate_chat(\n", + " assistant, message=ragproxyagent.message_generator, problem=qa_problem, n_results=10\n", + " )" ] } ], @@ -2839,7 +2836,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.11.9" }, "skip_test": "Requires interactive usage" }, diff --git a/notebook/agentchat_function_call_code_writing.ipynb b/notebook/agentchat_function_call_code_writing.ipynb index 3ae3084d698..92074e4821b 100644 --- a/notebook/agentchat_function_call_code_writing.ipynb +++ b/notebook/agentchat_function_call_code_writing.ipynb @@ -189,6 +189,7 @@ " file.write(\"\".join(file_contents))\n", " return 0, \"Code modified\"\n", "\n", + "\n", "@user_proxy.register_for_execution()\n", "@engineer.register_for_llm(description=\"Create a new file with code.\")\n", "def create_file_with_code(\n", diff --git a/notebook/agentchat_pgvector_RetrieveChat.ipynb b/notebook/agentchat_pgvector_RetrieveChat.ipynb index c0c681350f2..068ea55c7fc 100644 --- a/notebook/agentchat_pgvector_RetrieveChat.ipynb +++ b/notebook/agentchat_pgvector_RetrieveChat.ipynb @@ -5,7 +5,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Using RetrieveChat for Retrieve Augmented Code Generation and Question Answering\n", + "# Using RetrieveChat Powered by PGVector for Retrieve Augmented Code Generation and Question Answering\n", "\n", "AutoGen offers conversable agents powered by LLM, tool or human, which can be used to perform tasks collectively via automated chat. This framework allows tool use and human participation through multi-agent conversation.\n", "Please find documentation about this feature [here](https://microsoft.github.io/autogen/docs/Use-Cases/agent_chat).\n", @@ -17,10 +17,6 @@ "\n", "- [Example 1: Generate code based off docstrings w/o human feedback](#example-1)\n", "- [Example 2: Answer a question based off docstrings w/o human feedback](#example-2)\n", - "- [Example 3: Generate code based off docstrings w/ human feedback](#example-3)\n", - "- [Example 4: Answer a question based off docstrings w/ human feedback](#example-4)\n", - "- [Example 5: Solve comprehensive QA problems with RetrieveChat's unique feature `Update Context`](#example-5)\n", - "- [Example 6: Solve comprehensive QA problems with customized prompt and few-shot learning](#example-6)\n", "\n", "\n", "````{=mdx}\n", @@ -28,12 +24,41 @@ "Some extra dependencies are needed for this notebook, which can be installed via pip:\n", "\n", "```bash\n", - "pip install pyautogen[retrievechat] flaml[automl]\n", + "pip install pyautogen[retrievechat-pgvector] flaml[automl]\n", "```\n", "\n", "For more information, please refer to the [installation guide](/docs/installation/).\n", ":::\n", - "````" + "````\n", + "\n", + "Ensure you have a PGVector instance. \n", + "\n", + "If not, a test version can quickly be deployed using Docker.\n", + "\n", + "`docker-compose.yml`\n", + "```yml\n", + "version: '3.9'\n", + "\n", + "services:\n", + " db:\n", + " hostname: db\n", + " image: ankane/pgvector\n", + " ports:\n", + " - 5432:5432\n", + " restart: always\n", + " environment:\n", + " - POSTGRES_DB=postgres\n", + " - POSTGRES_USER=postgres\n", + " - POSTGRES_PASSWORD=postgres\n", + " - POSTGRES_HOST_AUTH_METHOD=trust\n", + " volumes:\n", + " - ./init.sql:/docker-entrypoint-initdb.d/init.sql\n", + "```\n", + "\n", + "Create `init.sql` file\n", + "```SQL\n", + "CREATE EXTENSION IF NOT EXISTS vector;\n", + "```\n" ] }, { @@ -48,14 +73,14 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "models to use: ['gpt-35-turbo', 'gpt-35-turbo-0613']\n" + "models to use: ['Meta-Llama-3-8B-Instruct-imatrix', 'gpt-3.5-turbo-0125', 'gpt-35-turbo']\n" ] } ], @@ -73,7 +98,22 @@ "# a vector database instance\n", "from autogen.retrieve_utils import TEXT_FORMATS\n", "\n", - "config_list = autogen.config_list_from_json(env_or_file=\"OAI_CONFIG_LIST\")\n", + "config_list = [\n", + " {\n", + " \"model\": \"Meta-Llama-3-8B-Instruct-imatrix\",\n", + " \"api_key\": \"YOUR_API_KEY\",\n", + " \"base_url\": \"http://localhost:8080/v1\",\n", + " \"api_type\": \"openai\",\n", + " },\n", + " {\"model\": \"gpt-3.5-turbo-0125\", \"api_key\": \"YOUR_API_KEY\", \"api_type\": \"openai\"},\n", + " {\n", + " \"model\": \"gpt-35-turbo\",\n", + " \"base_url\": \"...\",\n", + " \"api_type\": \"azure\",\n", + " \"api_version\": \"2023-07-01-preview\",\n", + " \"api_key\": \"...\",\n", + " },\n", + "]\n", "\n", "assert len(config_list) > 0\n", "print(\"models to use: \", [config_list[i][\"model\"] for i in range(len(config_list))])" @@ -97,7 +137,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -105,7 +145,7 @@ "output_type": "stream", "text": [ "Accepted file formats for `docs_path`:\n", - "['ppt', 'jsonl', 'csv', 'yaml', 'rst', 'htm', 'pdf', 'tsv', 'doc', 'docx', 'pptx', 'msg', 'yml', 'xml', 'md', 'json', 'txt', 'epub', 'org', 'xlsx', 'log', 'html', 'odt', 'rtf']\n" + "['org', 'pdf', 'md', 'docx', 'epub', 'rst', 'rtf', 'xml', 'ppt', 'txt', 'jsonl', 'msg', 'htm', 'yaml', 'html', 'xlsx', 'log', 'yml', 'odt', 'tsv', 'doc', 'pptx', 'csv', 'json']\n" ] } ], @@ -116,15 +156,15 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages/transformers/utils/generic.py:311: UserWarning: torch.utils._pytree._register_pytree_node is deprecated. Please use torch.utils._pytree.register_pytree_node instead.\n", - " torch.utils._pytree._register_pytree_node(\n" + "/home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages/torch/cuda/__init__.py:141: UserWarning: CUDA initialization: The NVIDIA driver on your system is too old (found version 11060). Please update your GPU driver by downloading and installing a new version from the URL: http://www.nvidia.com/Download/index.aspx Alternatively, go to: https://pytorch.org to install a PyTorch version that has been compiled with your version of the CUDA driver. (Triggered internally at ../c10/cuda/CUDAFunctions.cpp:108.)\n", + " return torch._C._cuda_getDeviceCount() > 0\n" ] } ], @@ -153,7 +193,7 @@ "ragproxyagent = RetrieveUserProxyAgent(\n", " name=\"ragproxyagent\",\n", " human_input_mode=\"NEVER\",\n", - " max_consecutive_auto_reply=3,\n", + " max_consecutive_auto_reply=1,\n", " retrieve_config={\n", " \"task\": \"code\",\n", " \"docs_path\": [\n", @@ -164,8 +204,18 @@ " \"custom_text_types\": [\"non-existent-type\"],\n", " \"chunk_token_size\": 2000,\n", " \"model\": config_list[0][\"model\"],\n", - " # \"client\": chromadb.PersistentClient(path=\"/tmp/chromadb\"), # deprecated, use \"vector_db\" instead\n", - " \"vector_db\": \"chroma\", # to use the deprecated `client` parameter, set to None and uncomment the line above\n", + " \"vector_db\": \"pgvector\", # PGVector database\n", + " \"collection_name\": \"flaml_collection\",\n", + " \"db_config\": {\n", + " \"connection_string\": \"postgresql://postgres:postgres@localhost:5432/postgres\", # Optional - connect to an external vector database\n", + " # \"host\": postgres, # Optional vector database host\n", + " # \"port\": 5432, # Optional vector database port\n", + " # \"database\": postgres, # Optional vector database name\n", + " # \"username\": postgres, # Optional vector database username\n", + " # \"password\": postgres, # Optional vector database password\n", + " \"model_name\": \"all-MiniLM-L6-v2\", # Sentence embedding model from https://huggingface.co/models?library=sentence-transformers or https://www.sbert.net/docs/pretrained_models.html\n", + " },\n", + " \"get_or_create\": True, # set to False if you don't want to reuse an existing collection\n", " \"overwrite\": False, # set to True if you want to overwrite an existing collection\n", " },\n", " code_execution_config=False, # set to False if you don't want to execute the code\n", @@ -188,14 +238,14 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "2024-04-07 17:30:56,955 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - \u001b[32mUse the existing collection `autogen-docs`.\u001b[0m\n" + "2024-04-25 11:23:53,000 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - \u001b[32mUse the existing collection `flaml_collection`.\u001b[0m\n" ] }, { @@ -209,16 +259,16 @@ "name": "stderr", "output_type": "stream", "text": [ - "2024-04-07 17:30:59,609 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - Found 2 chunks.\u001b[0m\n", - "Number of requested results 20 is greater than number of elements in index 2, updating n_results = 2\n" + "2024-04-25 11:23:54,745 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - Found 2 chunks.\u001b[0m\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "VectorDB returns doc_ids: [['bdfbc921']]\n", + "VectorDB returns doc_ids: [['bdfbc921', '7968cf3c']]\n", "\u001b[32mAdding content of doc bdfbc921 to context.\u001b[0m\n", + "\u001b[32mAdding content of doc 7968cf3c to context.\u001b[0m\n", "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", "\n", "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", @@ -231,7 +281,7 @@ "# your code\n", "```\n", "\n", - "User's question is: How can I use FLAML to perform a classification task and use spark to do parallel training. Train 30 seconds and force cancel jobs if time limit is reached.\n", + "User's question is: How can I use FLAML to perform a classification task and use spark to do parallel training. Train for 30 seconds and force cancel jobs if time limit is reached.\n", "\n", "Context is: # Integrate - Spark\n", "\n", @@ -362,197 +412,7 @@ "```\n", "\n", "[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb)\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "To perform a classification task using FLAML and use Spark to do parallel training for 30 seconds and force cancel jobs if the time limit is reached, you can follow these steps:\n", - "\n", - "1. First, convert your data into Spark dataframe format using `to_pandas_on_spark` function from `flaml.automl.spark.utils` module.\n", - "2. Then, format your data for use SparkML models by using `VectorAssembler`.\n", - "3. Define your AutoML settings, including the `metric`, `time_budget`, and `task`.\n", - "4. Use `AutoML` from `flaml` to run AutoML with SparkML models by setting `use_spark` to `true`, and `estimator_list` to a list of spark-based estimators, like `[\"lgbm_spark\"]`.\n", - "5. Set `n_concurrent_trials` to the desired number of parallel jobs and `force_cancel` to `True` to cancel the jobs if the time limit is reached.\n", - "\n", - "Here's an example code snippet for performing classification using FLAML and Spark:\n", - "\n", - "```python\n", - "import pandas as pd\n", - "from flaml.automl.spark.utils import to_pandas_on_spark\n", - "from pyspark.ml.feature import VectorAssembler\n", - "import flaml\n", - "\n", - "# Creating a dictionary\n", - "data = {\n", - " \"sepal_length\": [5.1, 4.9, 4.7, 4.6, 5.0],\n", - " \"sepal_width\": [3.5, 3.0, 3.2, 3.1, 3.6],\n", - " \"petal_length\": [1.4, 1.4, 1.3, 1.5, 1.4],\n", - " \"petal_width\": [0.2, 0.2, 0.2, 0.2, 0.2],\n", - " \"species\": [\"setosa\", \"setosa\", \"setosa\", \"setosa\", \"setosa\"]\n", - "}\n", - "\n", - "# Creating a pandas DataFrame\n", - "dataframe = pd.DataFrame(data)\n", - "label = \"species\"\n", - "\n", - "# Convert to pandas-on-spark dataframe\n", - "psdf = to_pandas_on_spark(dataframe)\n", - "\n", - "# Format data for SparkML models\n", - "columns = psdf.columns\n", - "feature_cols = [col for col in columns if col != label]\n", - "featurizer = VectorAssembler(inputCols=feature_cols, outputCol=\"features\")\n", - "psdf = featurizer.transform(psdf.to_spark(index_col=\"index\"))[\"index\", \"features\"]\n", - "\n", - "# Define AutoML settings\n", - "settings = {\n", - " \"time_budget\": 30,\n", - " \"metric\": \"accuracy\",\n", - " \"task\": \"classification\",\n", - "}\n", - "\n", - "# Use AutoML with SparkML models and parallel jobs\n", - "automl = flaml.AutoML()\n", - "automl.fit(\n", - " dataframe=psdf,\n", - " label=label,\n", - " estimator_list=[\"lgbm_spark\"],\n", - " use_spark=True,\n", - " n_concurrent_trials=2,\n", - " force_cancel=True,\n", - " **settings,\n", - ")\n", - "```\n", - "\n", - "Note that the above code assumes the data is small enough to train within 30 seconds. If you have a larger dataset, you may need to increase the `time_budget` and adjust the number of parallel jobs accordingly.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "UPDATE CONTEXT\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32mUpdating context and resetting conversation.\u001b[0m\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Number of requested results 60 is greater than number of elements in index 2, updating n_results = 2\n", - "Number of requested results 100 is greater than number of elements in index 2, updating n_results = 2\n", - "Number of requested results 140 is greater than number of elements in index 2, updating n_results = 2\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "VectorDB returns doc_ids: [['bdfbc921']]\n", - "VectorDB returns doc_ids: [['bdfbc921']]\n", - "VectorDB returns doc_ids: [['bdfbc921']]\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Number of requested results 180 is greater than number of elements in index 2, updating n_results = 2\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "VectorDB returns doc_ids: [['bdfbc921']]\n", - "\u001b[32mNo more context, will terminate.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "TERMINATE\n", - "\n", - "--------------------------------------------------------------------------------\n" - ] - }, - { - "data": { - "text/plain": [ - "ChatResult(chat_id=None, chat_history=[{'content': 'TERMINATE', 'role': 'assistant'}], summary='', cost=({'total_cost': 0.007691, 'gpt-35-turbo': {'cost': 0.007691, 'prompt_tokens': 4242, 'completion_tokens': 664, 'total_tokens': 4906}}, {'total_cost': 0}), human_input=[])" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# reset the assistant. Always reset the assistant before starting a new conversation.\n", - "assistant.reset()\n", - "\n", - "# given a problem, we use the ragproxyagent to generate a prompt to be sent to the assistant as the initial message.\n", - "# the assistant receives the message and generates a response. The response will be sent back to the ragproxyagent for processing.\n", - "# The conversation continues until the termination condition is met, in RetrieveChat, the termination condition when no human-in-loop is no code block detected.\n", - "# With human-in-loop, the conversation will continue until the user says \"exit\".\n", - "code_problem = \"How can I use FLAML to perform a classification task and use spark to do parallel training. Train 30 seconds and force cancel jobs if time limit is reached.\"\n", - "ragproxyagent.initiate_chat(\n", - " assistant, message=ragproxyagent.message_generator, problem=code_problem, search_string=\"spark\"\n", - ") # search_string is used as an extra filter for the embeddings search, in this case, we only want to search documents that contain \"spark\"." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Example 2\n", - "\n", - "[Back to top](#table-of-contents)\n", - "\n", - "Use RetrieveChat to answer a question that is not related to code generation.\n", - "\n", - "Problem: Who is the author of FLAML?" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Number of requested results 20 is greater than number of elements in index 2, updating n_results = 2\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "VectorDB returns doc_ids: [['7968cf3c', 'bdfbc921']]\n", - "\u001b[32mAdding content of doc 7968cf3c to context.\u001b[0m\n", - "\u001b[32mAdding content of doc bdfbc921 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", - "context provided by the user.\n", - "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", - "For code generation, you must obey the following rules:\n", - "Rule 1. You MUST NOT install any packages because all the packages needed are already installed.\n", - "Rule 2. You must follow the formats below to write your code:\n", - "```language\n", - "# your code\n", - "```\n", - "\n", - "User's question is: Who is the author of FLAML?\n", - "\n", - "Context is: # Research\n", + "# Research\n", "\n", "For technical details, please check our research publications.\n", "\n", @@ -666,7 +526,26 @@ " booktitle={ArXiv preprint arXiv:2306.01337},\n", "}\n", "```\n", - "# Integrate - Spark\n", + "\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32mAdding content of doc 7968cf3c to context.\u001b[0m\n", + "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", + "\n", + "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", + "context provided by the user.\n", + "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", + "For code generation, you must obey the following rules:\n", + "Rule 1. You MUST NOT install any packages because all the packages needed are already installed.\n", + "Rule 2. You must follow the formats below to write your code:\n", + "```language\n", + "# your code\n", + "```\n", + "\n", + "User's question is: How can I use FLAML to perform a classification task and use spark to do parallel training. Train for 30 seconds and force cancel jobs if time limit is reached.\n", + "\n", + "Context is: # Integrate - Spark\n", "\n", "FLAML has integrated Spark for distributed training. There are two main aspects of integration with Spark:\n", "\n", @@ -795,207 +674,244 @@ "```\n", "\n", "[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb)\n", + "# Research\n", "\n", + "For technical details, please check our research publications.\n", "\n", + "- [FLAML: A Fast and Lightweight AutoML Library](https://www.microsoft.com/en-us/research/publication/flaml-a-fast-and-lightweight-automl-library/). Chi Wang, Qingyun Wu, Markus Weimer, Erkang Zhu. MLSys 2021.\n", "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "The author of FLAML is Chi Wang, along with several co-authors for various publications related to FLAML.\n", + "```bibtex\n", + "@inproceedings{wang2021flaml,\n", + " title={FLAML: A Fast and Lightweight AutoML Library},\n", + " author={Chi Wang and Qingyun Wu and Markus Weimer and Erkang Zhu},\n", + " year={2021},\n", + " booktitle={MLSys},\n", + "}\n", + "```\n", "\n", - "--------------------------------------------------------------------------------\n" - ] - }, - { - "data": { - "text/plain": [ - "ChatResult(chat_id=None, chat_history=[{'content': 'You\\'re a retrieve augmented coding assistant. You answer user\\'s questions based on your own knowledge and the\\ncontext provided by the user.\\nIf you can\\'t answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\\nFor code generation, you must obey the following rules:\\nRule 1. You MUST NOT install any packages because all the packages needed are already installed.\\nRule 2. You must follow the formats below to write your code:\\n```language\\n# your code\\n```\\n\\nUser\\'s question is: Who is the author of FLAML?\\n\\nContext is: # Research\\n\\nFor technical details, please check our research publications.\\n\\n- [FLAML: A Fast and Lightweight AutoML Library](https://www.microsoft.com/en-us/research/publication/flaml-a-fast-and-lightweight-automl-library/). Chi Wang, Qingyun Wu, Markus Weimer, Erkang Zhu. MLSys 2021.\\n\\n```bibtex\\n@inproceedings{wang2021flaml,\\n title={FLAML: A Fast and Lightweight AutoML Library},\\n author={Chi Wang and Qingyun Wu and Markus Weimer and Erkang Zhu},\\n year={2021},\\n booktitle={MLSys},\\n}\\n```\\n\\n- [Frugal Optimization for Cost-related Hyperparameters](https://arxiv.org/abs/2005.01571). Qingyun Wu, Chi Wang, Silu Huang. AAAI 2021.\\n\\n```bibtex\\n@inproceedings{wu2021cfo,\\n title={Frugal Optimization for Cost-related Hyperparameters},\\n author={Qingyun Wu and Chi Wang and Silu Huang},\\n year={2021},\\n booktitle={AAAI},\\n}\\n```\\n\\n- [Economical Hyperparameter Optimization With Blended Search Strategy](https://www.microsoft.com/en-us/research/publication/economical-hyperparameter-optimization-with-blended-search-strategy/). Chi Wang, Qingyun Wu, Silu Huang, Amin Saied. ICLR 2021.\\n\\n```bibtex\\n@inproceedings{wang2021blendsearch,\\n title={Economical Hyperparameter Optimization With Blended Search Strategy},\\n author={Chi Wang and Qingyun Wu and Silu Huang and Amin Saied},\\n year={2021},\\n booktitle={ICLR},\\n}\\n```\\n\\n- [An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models](https://aclanthology.org/2021.acl-long.178.pdf). Susan Xueqing Liu, Chi Wang. ACL 2021.\\n\\n```bibtex\\n@inproceedings{liuwang2021hpolm,\\n title={An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models},\\n author={Susan Xueqing Liu and Chi Wang},\\n year={2021},\\n booktitle={ACL},\\n}\\n```\\n\\n- [ChaCha for Online AutoML](https://www.microsoft.com/en-us/research/publication/chacha-for-online-automl/). Qingyun Wu, Chi Wang, John Langford, Paul Mineiro and Marco Rossi. ICML 2021.\\n\\n```bibtex\\n@inproceedings{wu2021chacha,\\n title={ChaCha for Online AutoML},\\n author={Qingyun Wu and Chi Wang and John Langford and Paul Mineiro and Marco Rossi},\\n year={2021},\\n booktitle={ICML},\\n}\\n```\\n\\n- [Fair AutoML](https://arxiv.org/abs/2111.06495). Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2111.06495 (2021).\\n\\n```bibtex\\n@inproceedings{wuwang2021fairautoml,\\n title={Fair AutoML},\\n author={Qingyun Wu and Chi Wang},\\n year={2021},\\n booktitle={ArXiv preprint arXiv:2111.06495},\\n}\\n```\\n\\n- [Mining Robust Default Configurations for Resource-constrained AutoML](https://arxiv.org/abs/2202.09927). Moe Kayali, Chi Wang. ArXiv preprint arXiv:2202.09927 (2022).\\n\\n```bibtex\\n@inproceedings{kayaliwang2022default,\\n title={Mining Robust Default Configurations for Resource-constrained AutoML},\\n author={Moe Kayali and Chi Wang},\\n year={2022},\\n booktitle={ArXiv preprint arXiv:2202.09927},\\n}\\n```\\n\\n- [Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives](https://openreview.net/forum?id=0Ij9_q567Ma). Shaokun Zhang, Feiran Jia, Chi Wang, Qingyun Wu. ICLR 2023 (notable-top-5%).\\n\\n```bibtex\\n@inproceedings{zhang2023targeted,\\n title={Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives},\\n author={Shaokun Zhang and Feiran Jia and Chi Wang and Qingyun Wu},\\n booktitle={International Conference on Learning Representations},\\n year={2023},\\n url={https://openreview.net/forum?id=0Ij9_q567Ma},\\n}\\n```\\n\\n- [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673). Chi Wang, Susan Xueqing Liu, Ahmed H. Awadallah. ArXiv preprint arXiv:2303.04673 (2023).\\n\\n```bibtex\\n@inproceedings{wang2023EcoOptiGen,\\n title={Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference},\\n author={Chi Wang and Susan Xueqing Liu and Ahmed H. Awadallah},\\n year={2023},\\n booktitle={ArXiv preprint arXiv:2303.04673},\\n}\\n```\\n\\n- [An Empirical Study on Challenging Math Problem Solving with GPT-4](https://arxiv.org/abs/2306.01337). Yiran Wu, Feiran Jia, Shaokun Zhang, Hangyu Li, Erkang Zhu, Yue Wang, Yin Tat Lee, Richard Peng, Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2306.01337 (2023).\\n\\n```bibtex\\n@inproceedings{wu2023empirical,\\n title={An Empirical Study on Challenging Math Problem Solving with GPT-4},\\n author={Yiran Wu and Feiran Jia and Shaokun Zhang and Hangyu Li and Erkang Zhu and Yue Wang and Yin Tat Lee and Richard Peng and Qingyun Wu and Chi Wang},\\n year={2023},\\n booktitle={ArXiv preprint arXiv:2306.01337},\\n}\\n```\\n# Integrate - Spark\\n\\nFLAML has integrated Spark for distributed training. There are two main aspects of integration with Spark:\\n\\n- Use Spark ML estimators for AutoML.\\n- Use Spark to run training in parallel spark jobs.\\n\\n## Spark ML Estimators\\n\\nFLAML integrates estimators based on Spark ML models. These models are trained in parallel using Spark, so we called them Spark estimators. To use these models, you first need to organize your data in the required format.\\n\\n### Data\\n\\nFor Spark estimators, AutoML only consumes Spark data. FLAML provides a convenient function `to_pandas_on_spark` in the `flaml.automl.spark.utils` module to convert your data into a pandas-on-spark (`pyspark.pandas`) dataframe/series, which Spark estimators require.\\n\\nThis utility function takes data in the form of a `pandas.Dataframe` or `pyspark.sql.Dataframe` and converts it into a pandas-on-spark dataframe. It also takes `pandas.Series` or `pyspark.sql.Dataframe` and converts it into a [pandas-on-spark](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/index.html) series. If you pass in a `pyspark.pandas.Dataframe`, it will not make any changes.\\n\\nThis function also accepts optional arguments `index_col` and `default_index_type`.\\n\\n- `index_col` is the column name to use as the index, default is None.\\n- `default_index_type` is the default index type, default is \"distributed-sequence\". More info about default index type could be found on Spark official [documentation](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/options.html#default-index-type)\\n\\nHere is an example code snippet for Spark Data:\\n\\n```python\\nimport pandas as pd\\nfrom flaml.automl.spark.utils import to_pandas_on_spark\\n\\n# Creating a dictionary\\ndata = {\\n \"Square_Feet\": [800, 1200, 1800, 1500, 850],\\n \"Age_Years\": [20, 15, 10, 7, 25],\\n \"Price\": [100000, 200000, 300000, 240000, 120000],\\n}\\n\\n# Creating a pandas DataFrame\\ndataframe = pd.DataFrame(data)\\nlabel = \"Price\"\\n\\n# Convert to pandas-on-spark dataframe\\npsdf = to_pandas_on_spark(dataframe)\\n```\\n\\nTo use Spark ML models you need to format your data appropriately. Specifically, use [`VectorAssembler`](https://spark.apache.org/docs/latest/api/python/reference/api/pyspark.ml.feature.VectorAssembler.html) to merge all feature columns into a single vector column.\\n\\nHere is an example of how to use it:\\n\\n```python\\nfrom pyspark.ml.feature import VectorAssembler\\n\\ncolumns = psdf.columns\\nfeature_cols = [col for col in columns if col != label]\\nfeaturizer = VectorAssembler(inputCols=feature_cols, outputCol=\"features\")\\npsdf = featurizer.transform(psdf.to_spark(index_col=\"index\"))[\"index\", \"features\"]\\n```\\n\\nLater in conducting the experiment, use your pandas-on-spark data like non-spark data and pass them using `X_train, y_train` or `dataframe, label`.\\n\\n### Estimators\\n\\n#### Model List\\n\\n- `lgbm_spark`: The class for fine-tuning Spark version LightGBM models, using [SynapseML](https://microsoft.github.io/SynapseML/docs/features/lightgbm/about/) API.\\n\\n#### Usage\\n\\nFirst, prepare your data in the required format as described in the previous section.\\n\\nBy including the models you intend to try in the `estimators_list` argument to `flaml.automl`, FLAML will start trying configurations for these models. If your input is Spark data, FLAML will also use estimators with the `_spark` postfix by default, even if you haven\\'t specified them.\\n\\nHere is an example code snippet using SparkML models in AutoML:\\n\\n```python\\nimport flaml\\n\\n# prepare your data in pandas-on-spark format as we previously mentioned\\n\\nautoml = flaml.AutoML()\\nsettings = {\\n \"time_budget\": 30,\\n \"metric\": \"r2\",\\n \"estimator_list\": [\"lgbm_spark\"], # this setting is optional\\n \"task\": \"regression\",\\n}\\n\\nautoml.fit(\\n dataframe=psdf,\\n label=label,\\n **settings,\\n)\\n```\\n\\n[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/automl_bankrupt_synapseml.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/automl_bankrupt_synapseml.ipynb)\\n\\n## Parallel Spark Jobs\\n\\nYou can activate Spark as the parallel backend during parallel tuning in both [AutoML](/docs/Use-Cases/Task-Oriented-AutoML#parallel-tuning) and [Hyperparameter Tuning](/docs/Use-Cases/Tune-User-Defined-Function#parallel-tuning), by setting the `use_spark` to `true`. FLAML will dispatch your job to the distributed Spark backend using [`joblib-spark`](https://github.com/joblib/joblib-spark).\\n\\nPlease note that you should not set `use_spark` to `true` when applying AutoML and Tuning for Spark Data. This is because only SparkML models will be used for Spark Data in AutoML and Tuning. As SparkML models run in parallel, there is no need to distribute them with `use_spark` again.\\n\\nAll the Spark-related arguments are stated below. These arguments are available in both Hyperparameter Tuning and AutoML:\\n\\n- `use_spark`: boolean, default=False | Whether to use spark to run the training in parallel spark jobs. This can be used to accelerate training on large models and large datasets, but will incur more overhead in time and thus slow down training in some cases. GPU training is not supported yet when use_spark is True. For Spark clusters, by default, we will launch one trial per executor. However, sometimes we want to launch more trials than the number of executors (e.g., local mode). In this case, we can set the environment variable `FLAML_MAX_CONCURRENT` to override the detected `num_executors`. The final number of concurrent trials will be the minimum of `n_concurrent_trials` and `num_executors`.\\n- `n_concurrent_trials`: int, default=1 | The number of concurrent trials. When n_concurrent_trials > 1, FLAML performes parallel tuning.\\n- `force_cancel`: boolean, default=False | Whether to forcely cancel Spark jobs if the search time exceeded the time budget. Spark jobs include parallel tuning jobs and Spark-based model training jobs.\\n\\nAn example code snippet for using parallel Spark jobs:\\n\\n```python\\nimport flaml\\n\\nautoml_experiment = flaml.AutoML()\\nautoml_settings = {\\n \"time_budget\": 30,\\n \"metric\": \"r2\",\\n \"task\": \"regression\",\\n \"n_concurrent_trials\": 2,\\n \"use_spark\": True,\\n \"force_cancel\": True, # Activating the force_cancel option can immediately halt Spark jobs once they exceed the allocated time_budget.\\n}\\n\\nautoml.fit(\\n dataframe=dataframe,\\n label=label,\\n **automl_settings,\\n)\\n```\\n\\n[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb)\\n\\n', 'role': 'assistant'}, {'content': 'The author of FLAML is Chi Wang, along with several co-authors for various publications related to FLAML.', 'role': 'user'}], summary='The author of FLAML is Chi Wang, along with several co-authors for various publications related to FLAML.', cost=({'total_cost': 0.004711, 'gpt-35-turbo': {'cost': 0.004711, 'prompt_tokens': 3110, 'completion_tokens': 23, 'total_tokens': 3133}}, {'total_cost': 0}), human_input=[])" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# reset the assistant. Always reset the assistant before starting a new conversation.\n", - "assistant.reset()\n", - "\n", - "qa_problem = \"Who is the author of FLAML?\"\n", - "ragproxyagent.initiate_chat(assistant, message=ragproxyagent.message_generator, problem=qa_problem)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Example 3\n", - "\n", - "[Back to top](#table-of-contents)\n", - "\n", - "Use RetrieveChat to help generate sample code and ask for human-in-loop feedbacks.\n", - "\n", - "Problem: how to build a time series forecasting model for stock price using FLAML?" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING:chromadb.segment.impl.vector.local_persistent_hnsw:Number of requested results 20 is greater than number of elements in index 2, updating n_results = 2\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "doc_ids: [['doc_0', 'doc_1']]\n", - "\u001b[32mAdding doc_id doc_0 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", + "- [Frugal Optimization for Cost-related Hyperparameters](https://arxiv.org/abs/2005.01571). Qingyun Wu, Chi Wang, Silu Huang. AAAI 2021.\n", "\n", - "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", - "context provided by the user.\n", - "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", - "For code generation, you must obey the following rules:\n", - "Rule 1. You MUST NOT install any packages because all the packages needed are already installed.\n", - "Rule 2. You must follow the formats below to write your code:\n", - "```language\n", - "# your code\n", + "```bibtex\n", + "@inproceedings{wu2021cfo,\n", + " title={Frugal Optimization for Cost-related Hyperparameters},\n", + " author={Qingyun Wu and Chi Wang and Silu Huang},\n", + " year={2021},\n", + " booktitle={AAAI},\n", + "}\n", "```\n", "\n", - "User's question is: how to build a time series forecasting model for stock price using FLAML?\n", + "- [Economical Hyperparameter Optimization With Blended Search Strategy](https://www.microsoft.com/en-us/research/publication/economical-hyperparameter-optimization-with-blended-search-strategy/). Chi Wang, Qingyun Wu, Silu Huang, Amin Saied. ICLR 2021.\n", "\n", - "Context is: # Integrate - Spark\n", + "```bibtex\n", + "@inproceedings{wang2021blendsearch,\n", + " title={Economical Hyperparameter Optimization With Blended Search Strategy},\n", + " author={Chi Wang and Qingyun Wu and Silu Huang and Amin Saied},\n", + " year={2021},\n", + " booktitle={ICLR},\n", + "}\n", + "```\n", "\n", - "FLAML has integrated Spark for distributed training. There are two main aspects of integration with Spark:\n", - "- Use Spark ML estimators for AutoML.\n", - "- Use Spark to run training in parallel spark jobs.\n", - "\n", - "## Spark ML Estimators\n", + "- [An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models](https://aclanthology.org/2021.acl-long.178.pdf). Susan Xueqing Liu, Chi Wang. ACL 2021.\n", "\n", - "FLAML integrates estimators based on Spark ML models. These models are trained in parallel using Spark, so we called them Spark estimators. To use these models, you first need to organize your data in the required format.\n", + "```bibtex\n", + "@inproceedings{liuwang2021hpolm,\n", + " title={An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models},\n", + " author={Susan Xueqing Liu and Chi Wang},\n", + " year={2021},\n", + " booktitle={ACL},\n", + "}\n", + "```\n", "\n", - "### Data\n", + "- [ChaCha for Online AutoML](https://www.microsoft.com/en-us/research/publication/chacha-for-online-automl/). Qingyun Wu, Chi Wang, John Langford, Paul Mineiro and Marco Rossi. ICML 2021.\n", "\n", - "For Spark estimators, AutoML only consumes Spark data. FLAML provides a convenient function `to_pandas_on_spark` in the `flaml.automl.spark.utils` module to convert your data into a pandas-on-spark (`pyspark.pandas`) dataframe/series, which Spark estimators require.\n", + "```bibtex\n", + "@inproceedings{wu2021chacha,\n", + " title={ChaCha for Online AutoML},\n", + " author={Qingyun Wu and Chi Wang and John Langford and Paul Mineiro and Marco Rossi},\n", + " year={2021},\n", + " booktitle={ICML},\n", + "}\n", + "```\n", "\n", - "This utility function takes data in the form of a `pandas.Dataframe` or `pyspark.sql.Dataframe` and converts it into a pandas-on-spark dataframe. It also takes `pandas.Series` or `pyspark.sql.Dataframe` and converts it into a [pandas-on-spark](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/index.html) series. If you pass in a `pyspark.pandas.Dataframe`, it will not make any changes.\n", + "- [Fair AutoML](https://arxiv.org/abs/2111.06495). Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2111.06495 (2021).\n", "\n", - "This function also accepts optional arguments `index_col` and `default_index_type`.\n", - "- `index_col` is the column name to use as the index, default is None.\n", - "- `default_index_type` is the default index type, default is \"distributed-sequence\". More info about default index type could be found on Spark official [documentation](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/options.html#default-index-type)\n", + "```bibtex\n", + "@inproceedings{wuwang2021fairautoml,\n", + " title={Fair AutoML},\n", + " author={Qingyun Wu and Chi Wang},\n", + " year={2021},\n", + " booktitle={ArXiv preprint arXiv:2111.06495},\n", + "}\n", + "```\n", "\n", - "Here is an example code snippet for Spark Data:\n", + "- [Mining Robust Default Configurations for Resource-constrained AutoML](https://arxiv.org/abs/2202.09927). Moe Kayali, Chi Wang. ArXiv preprint arXiv:2202.09927 (2022).\n", "\n", - "```python\n", - "import pandas as pd\n", - "from flaml.automl.spark.utils import to_pandas_on_spark\n", - "# Creating a dictionary\n", - "data = {\"Square_Feet\": [800, 1200, 1800, 1500, 850],\n", - " \"Age_Years\": [20, 15, 10, 7, 25],\n", - " \"Price\": [100000, 200000, 300000, 240000, 120000]}\n", + "```bibtex\n", + "@inproceedings{kayaliwang2022default,\n", + " title={Mining Robust Default Configurations for Resource-constrained AutoML},\n", + " author={Moe Kayali and Chi Wang},\n", + " year={2022},\n", + " booktitle={ArXiv preprint arXiv:2202.09927},\n", + "}\n", + "```\n", "\n", - "# Creating a pandas DataFrame\n", - "dataframe = pd.DataFrame(data)\n", - "label = \"Price\"\n", + "- [Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives](https://openreview.net/forum?id=0Ij9_q567Ma). Shaokun Zhang, Feiran Jia, Chi Wang, Qingyun Wu. ICLR 2023 (notable-top-5%).\n", "\n", - "# Convert to pandas-on-spark dataframe\n", - "psdf = to_pandas_on_spark(dataframe)\n", + "```bibtex\n", + "@inproceedings{zhang2023targeted,\n", + " title={Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives},\n", + " author={Shaokun Zhang and Feiran Jia and Chi Wang and Qingyun Wu},\n", + " booktitle={International Conference on Learning Representations},\n", + " year={2023},\n", + " url={https://openreview.net/forum?id=0Ij9_q567Ma},\n", + "}\n", "```\n", "\n", - "To use Spark ML models you need to format your data appropriately. Specifically, use [`VectorAssembler`](https://spark.apache.org/docs/latest/api/python/reference/api/pyspark.ml.feature.VectorAssembler.html) to merge all feature columns into a single vector column.\n", + "- [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673). Chi Wang, Susan Xueqing Liu, Ahmed H. Awadallah. ArXiv preprint arXiv:2303.04673 (2023).\n", "\n", - "Here is an example of how to use it:\n", - "```python\n", - "from pyspark.ml.feature import VectorAssembler\n", - "columns = psdf.columns\n", - "feature_cols = [col for col in columns if col != label]\n", - "featurizer = VectorAssembler(inputCols=feature_cols, outputCol=\"features\")\n", - "psdf = featurizer.transform(psdf.to_spark(index_col=\"index\"))[\"index\", \"features\"]\n", + "```bibtex\n", + "@inproceedings{wang2023EcoOptiGen,\n", + " title={Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference},\n", + " author={Chi Wang and Susan Xueqing Liu and Ahmed H. Awadallah},\n", + " year={2023},\n", + " booktitle={ArXiv preprint arXiv:2303.04673},\n", + "}\n", "```\n", "\n", - "Later in conducting the experiment, use your pandas-on-spark data like non-spark data and pass them using `X_train, y_train` or `dataframe, label`.\n", + "- [An Empirical Study on Challenging Math Problem Solving with GPT-4](https://arxiv.org/abs/2306.01337). Yiran Wu, Feiran Jia, Shaokun Zhang, Hangyu Li, Erkang Zhu, Yue Wang, Yin Tat Lee, Richard Peng, Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2306.01337 (2023).\n", "\n", - "### Estimators\n", - "#### Model List\n", - "- `lgbm_spark`: The class for fine-tuning Spark version LightGBM models, using [SynapseML](https://microsoft.github.io/SynapseML/docs/features/lightgbm/about/) API.\n", + "```bibtex\n", + "@inproceedings{wu2023empirical,\n", + " title={An Empirical Study on Challenging Math Problem Solving with GPT-4},\n", + " author={Yiran Wu and Feiran Jia and Shaokun Zhang and Hangyu Li and Erkang Zhu and Yue Wang and Yin Tat Lee and Richard Peng and Qingyun Wu and Chi Wang},\n", + " year={2023},\n", + " booktitle={ArXiv preprint arXiv:2306.01337},\n", + "}\n", + "```\n", "\n", - "#### Usage\n", - "First, prepare your data in the required format as described in the previous section.\n", "\n", - "By including the models you intend to try in the `estimators_list` argument to `flaml.automl`, FLAML will start trying configurations for these models. If your input is Spark data, FLAML will also use estimators with the `_spark` postfix by default, even if you haven't specified them.\n", "\n", - "Here is an example code snippet using SparkML models in AutoML:\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", + "\n", + "To use FLAML for a classification task and perform parallel training using Spark and train for 30 seconds while forcing cancel jobs if the time limit is reached, you can use the following code:\n", "\n", "```python\n", "import flaml\n", - "# prepare your data in pandas-on-spark format as we previously mentioned\n", + "from flaml.automl.spark.utils import to_pandas_on_spark\n", + "from pyspark.ml.feature import VectorAssembler\n", "\n", - "automl = flaml.AutoML()\n", + "# load your classification dataset as a pandas DataFrame\n", + "dataframe = ...\n", + "\n", + "# convert the pandas DataFrame to a pandas-on-spark DataFrame\n", + "psdf = to_pandas_on_spark(dataframe)\n", + "\n", + "# define the label column\n", + "label = ...\n", + "\n", + "# use VectorAssembler to merge all feature columns into a single vector column\n", + "columns = psdf.columns\n", + "feature_cols = [col for col in columns if col != label]\n", + "featurizer = VectorAssembler(inputCols=feature_cols, outputCol=\"features\")\n", + "psdf = featurizer.transform(psdf.to_spark(index_col=\"index\"))[\"index\", \"features\"]\n", + "\n", + "# configure the AutoML settings\n", "settings = {\n", " \"time_budget\": 30,\n", - " \"metric\": \"r2\",\n", - " \"estimator_list\": [\"lgbm_spark\"], # this setting is optional\n", - " \"task\": \"regression\",\n", + " \"metric\": 'accuracy',\n", + " \"task\": 'classification',\n", + " \"log_file_name\": 'classification.log',\n", + " \"estimator_list\": ['lgbm_spark'],\n", + " \"n_concurrent_trials\": 2,\n", + " \"use_spark\": True,\n", + " \"force_cancel\": True\n", "}\n", "\n", + "# create and run the AutoML experiment\n", + "automl = flaml.AutoML()\n", "automl.fit(\n", " dataframe=psdf,\n", " label=label,\n", - " **settings,\n", + " **settings\n", ")\n", "```\n", "\n", + "Note that you will need to replace the placeholders with your own dataset and label column names. This code will use FLAML's `lgbm_spark` estimator for training the classification model in parallel using Spark. The training will be restricted to 30 seconds, and if the time limit is reached, FLAML will force cancel the Spark jobs.\n", "\n", - "[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/automl_bankrupt_synapseml.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/automl_bankrupt_synapseml.ipynb)\n", - "\n", - "## Parallel Spark Jobs\n", - "You can activate Spark as the parallel backend during parallel tuning in both [AutoML](/docs/Use-Cases/Task-Oriented-AutoML#parallel-tuning) and [Hyperparameter Tuning](/docs/Use-Cases/Tune-User-Defined-Function#parallel-tuning), by setting the `use_spark` to `true`. FLAML will dispatch your job to the distributed Spark backend using [`joblib-spark`](https://github.com/joblib/joblib-spark).\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", "\n", - "Please note that you should not set `use_spark` to `true` when applying AutoML and Tuning for Spark Data. This is because only SparkML models will be used for Spark Data in AutoML and Tuning. As SparkML models run in parallel, there is no need to distribute them with `use_spark` again.\n", "\n", - "All the Spark-related arguments are stated below. These arguments are available in both Hyperparameter Tuning and AutoML:\n", "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", "\n", - "- `use_spark`: boolean, default=False | Whether to use spark to run the training in parallel spark jobs. This can be used to accelerate training on large models and large datasets, but will incur more overhead in time and thus slow down training in some cases. GPU training is not supported yet when use_spark is True. For Spark clusters, by default, we will launch one trial per executor. However, sometimes we want to launch more trials than the number of executors (e.g., local mode). In this case, we can set the environment variable `FLAML_MAX_CONCURRENT` to override the detected `num_executors`. The final number of concurrent trials will be the minimum of `n_concurrent_trials` and `num_executors`.\n", - "- `n_concurrent_trials`: int, default=1 | The number of concurrent trials. When n_concurrent_trials > 1, FLAML performs parallel tuning.\n", - "- `force_cancel`: boolean, default=False | Whether to forcely cancel Spark jobs if the search time exceeded the time budget. Spark jobs include parallel tuning jobs and Spark-based model training jobs.\n", + "UPDATE CONTEXT\n", "\n", - "An example code snippet for using parallel Spark jobs:\n", - "```python\n", - "import flaml\n", - "automl_experiment = flaml.AutoML()\n", - "automl_settings = {\n", - " \"time_budget\": 30,\n", - " \"metric\": \"r2\",\n", - " \"task\": \"regression\",\n", - " \"n_concurrent_trials\": 2,\n", - " \"use_spark\": True,\n", - " \"force_cancel\": True, # Activating the force_cancel option can immediately halt Spark jobs once they exceed the allocated time_budget.\n", - "}\n", + "--------------------------------------------------------------------------------\n" + ] + } + ], + "source": [ + "# reset the assistant. Always reset the assistant before starting a new conversation.\n", + "assistant.reset()\n", + "\n", + "# given a problem, we use the ragproxyagent to generate a prompt to be sent to the assistant as the initial message.\n", + "# the assistant receives the message and generates a response. The response will be sent back to the ragproxyagent for processing.\n", + "# The conversation continues until the termination condition is met, in RetrieveChat, the termination condition when no human-in-loop is no code block detected.\n", + "# With human-in-loop, the conversation will continue until the user says \"exit\".\n", + "code_problem = \"How can I use FLAML to perform a classification task and use spark to do parallel training. Train for 30 seconds and force cancel jobs if time limit is reached.\"\n", + "chat_result = ragproxyagent.initiate_chat(\n", + " assistant, message=ragproxyagent.message_generator, problem=code_problem, search_string=\"spark\"\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 2\n", + "\n", + "[Back to top](#table-of-contents)\n", + "\n", + "Use RetrieveChat to answer a question that is not related to code generation.\n", + "\n", + "Problem: Who is the author of FLAML?" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "VectorDB returns doc_ids: [['7968cf3c', 'bdfbc921']]\n", + "\u001b[32mAdding content of doc 7968cf3c to context.\u001b[0m\n", + "\u001b[32mAdding content of doc bdfbc921 to context.\u001b[0m\n", + "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", "\n", - "automl.fit(\n", - " dataframe=dataframe,\n", - " label=label,\n", - " **automl_settings,\n", - ")\n", + "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", + "context provided by the user.\n", + "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", + "For code generation, you must obey the following rules:\n", + "Rule 1. You MUST NOT install any packages because all the packages needed are already installed.\n", + "Rule 2. You must follow the formats below to write your code:\n", + "```language\n", + "# your code\n", "```\n", "\n", + "User's question is: Who is the author of FLAML?\n", "\n", - "[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb)\n", - "\n", - "# Research\n", + "Context is: # Research\n", "\n", "For technical details, please check our research publications.\n", "\n", - "* [FLAML: A Fast and Lightweight AutoML Library](https://www.microsoft.com/en-us/research/publication/flaml-a-fast-and-lightweight-automl-library/). Chi Wang, Qingyun Wu, Markus Weimer, Erkang Zhu. MLSys 2021.\n", + "- [FLAML: A Fast and Lightweight AutoML Library](https://www.microsoft.com/en-us/research/publication/flaml-a-fast-and-lightweight-automl-library/). Chi Wang, Qingyun Wu, Markus Weimer, Erkang Zhu. MLSys 2021.\n", "\n", "```bibtex\n", "@inproceedings{wang2021flaml,\n", @@ -1006,7 +922,7 @@ "}\n", "```\n", "\n", - "* [Frugal Optimization for Cost-related Hyperparameters](https://arxiv.org/abs/2005.01571). Qingyun Wu, Chi Wang, Silu Huang. AAAI 2021.\n", + "- [Frugal Optimization for Cost-related Hyperparameters](https://arxiv.org/abs/2005.01571). Qingyun Wu, Chi Wang, Silu Huang. AAAI 2021.\n", "\n", "```bibtex\n", "@inproceedings{wu2021cfo,\n", @@ -1017,7 +933,7 @@ "}\n", "```\n", "\n", - "* [Economical Hyperparameter Optimization With Blended Search Strategy](https://www.microsoft.com/en-us/research/publication/economical-hyperparameter-optimization-with-blended-search-strategy/). Chi Wang, Qingyun Wu, Silu Huang, Amin Saied. ICLR 2021.\n", + "- [Economical Hyperparameter Optimization With Blended Search Strategy](https://www.microsoft.com/en-us/research/publication/economical-hyperparameter-optimization-with-blended-search-strategy/). Chi Wang, Qingyun Wu, Silu Huang, Amin Saied. ICLR 2021.\n", "\n", "```bibtex\n", "@inproceedings{wang2021blendsearch,\n", @@ -1028,7 +944,7 @@ "}\n", "```\n", "\n", - "* [An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models](https://aclanthology.org/2021.acl-long.178.pdf). Susan Xueqing Liu, Chi Wang. ACL 2021.\n", + "- [An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models](https://aclanthology.org/2021.acl-long.178.pdf). Susan Xueqing Liu, Chi Wang. ACL 2021.\n", "\n", "```bibtex\n", "@inproceedings{liuwang2021hpolm,\n", @@ -1039,7 +955,7 @@ "}\n", "```\n", "\n", - "* [ChaCha for Online AutoML](https://www.microsoft.com/en-us/research/publication/chacha-for-online-automl/). Qingyun Wu, Chi Wang, John Langford, Paul Mineiro and Marco Rossi. ICML 2021.\n", + "- [ChaCha for Online AutoML](https://www.microsoft.com/en-us/research/publication/chacha-for-online-automl/). Qingyun Wu, Chi Wang, John Langford, Paul Mineiro and Marco Rossi. ICML 2021.\n", "\n", "```bibtex\n", "@inproceedings{wu2021chacha,\n", @@ -1050,7 +966,7 @@ "}\n", "```\n", "\n", - "* [Fair AutoML](https://arxiv.org/abs/2111.06495). Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2111.06495 (2021).\n", + "- [Fair AutoML](https://arxiv.org/abs/2111.06495). Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2111.06495 (2021).\n", "\n", "```bibtex\n", "@inproceedings{wuwang2021fairautoml,\n", @@ -1061,7 +977,7 @@ "}\n", "```\n", "\n", - "* [Mining Robust Default Configurations for Resource-constrained AutoML](https://arxiv.org/abs/2202.09927). Moe Kayali, Chi Wang. ArXiv preprint arXiv:2202.09927 (2022).\n", + "- [Mining Robust Default Configurations for Resource-constrained AutoML](https://arxiv.org/abs/2202.09927). Moe Kayali, Chi Wang. ArXiv preprint arXiv:2202.09927 (2022).\n", "\n", "```bibtex\n", "@inproceedings{kayaliwang2022default,\n", @@ -1072,7 +988,7 @@ "}\n", "```\n", "\n", - "* [Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives](https://openreview.net/forum?id=0Ij9_q567Ma). Shaokun Zhang, Feiran Jia, Chi Wang, Qingyun Wu. ICLR 2023 (notable-top-5%).\n", + "- [Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives](https://openreview.net/forum?id=0Ij9_q567Ma). Shaokun Zhang, Feiran Jia, Chi Wang, Qingyun Wu. ICLR 2023 (notable-top-5%).\n", "\n", "```bibtex\n", "@inproceedings{zhang2023targeted,\n", @@ -1084,7 +1000,7 @@ "}\n", "```\n", "\n", - "* [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673). Chi Wang, Susan Xueqing Liu, Ahmed H. Awadallah. ArXiv preprint arXiv:2303.04673 (2023).\n", + "- [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673). Chi Wang, Susan Xueqing Liu, Ahmed H. Awadallah. ArXiv preprint arXiv:2303.04673 (2023).\n", "\n", "```bibtex\n", "@inproceedings{wang2023EcoOptiGen,\n", @@ -1095,7 +1011,7 @@ "}\n", "```\n", "\n", - "* [An Empirical Study on Challenging Math Problem Solving with GPT-4](https://arxiv.org/abs/2306.01337). Yiran Wu, Feiran Jia, Shaokun Zhang, Hangyu Li, Erkang Zhu, Yue Wang, Yin Tat Lee, Richard Peng, Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2306.01337 (2023).\n", + "- [An Empirical Study on Challenging Math Problem Solving with GPT-4](https://arxiv.org/abs/2306.01337). Yiran Wu, Feiran Jia, Shaokun Zhang, Hangyu Li, Erkang Zhu, Yue Wang, Yin Tat Lee, Richard Peng, Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2306.01337 (2023).\n", "\n", "```bibtex\n", "@inproceedings{wu2023empirical,\n", @@ -1105,229 +1021,56 @@ " booktitle={ArXiv preprint arXiv:2306.01337},\n", "}\n", "```\n", + "# Integrate - Spark\n", "\n", + "FLAML has integrated Spark for distributed training. There are two main aspects of integration with Spark:\n", "\n", + "- Use Spark ML estimators for AutoML.\n", + "- Use Spark to run training in parallel spark jobs.\n", "\n", + "## Spark ML Estimators\n", "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", + "FLAML integrates estimators based on Spark ML models. These models are trained in parallel using Spark, so we called them Spark estimators. To use these models, you first need to organize your data in the required format.\n", + "\n", + "### Data\n", + "\n", + "For Spark estimators, AutoML only consumes Spark data. FLAML provides a convenient function `to_pandas_on_spark` in the `flaml.automl.spark.utils` module to convert your data into a pandas-on-spark (`pyspark.pandas`) dataframe/series, which Spark estimators require.\n", + "\n", + "This utility function takes data in the form of a `pandas.Dataframe` or `pyspark.sql.Dataframe` and converts it into a pandas-on-spark dataframe. It also takes `pandas.Series` or `pyspark.sql.Dataframe` and converts it into a [pandas-on-spark](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/index.html) series. If you pass in a `pyspark.pandas.Dataframe`, it will not make any changes.\n", + "\n", + "This function also accepts optional arguments `index_col` and `default_index_type`.\n", + "\n", + "- `index_col` is the column name to use as the index, default is None.\n", + "- `default_index_type` is the default index type, default is \"distributed-sequence\". More info about default index type could be found on Spark official [documentation](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/options.html#default-index-type)\n", "\n", - "To build a time series forecasting model for stock price using FLAML, you can use the `lgbm_spark` estimator and organize your data in the required format. First, use `to_pandas_on_spark` function to convert your data into a pandas-on-spark dataframe/series, which Spark estimators require. Next, you should use `VectorAssembler` to merge all feature columns into a single vector column. Finally, use `flaml.AutoML` to try different configurations for the `lgbm_spark` model. Here is an example code snippet: \n", + "Here is an example code snippet for Spark Data:\n", "\n", "```python\n", - "import flaml\n", "import pandas as pd\n", "from flaml.automl.spark.utils import to_pandas_on_spark\n", - "from pyspark.ml.feature import VectorAssembler\n", "\n", - "# load your stock price data into a pandas dataframe\n", - "data = pd.read_csv('stock_price.csv')\n", + "# Creating a dictionary\n", + "data = {\n", + " \"Square_Feet\": [800, 1200, 1800, 1500, 850],\n", + " \"Age_Years\": [20, 15, 10, 7, 25],\n", + " \"Price\": [100000, 200000, 300000, 240000, 120000],\n", + "}\n", "\n", - "# specify label column name\n", - "label = 'price'\n", + "# Creating a pandas DataFrame\n", + "dataframe = pd.DataFrame(data)\n", + "label = \"Price\"\n", "\n", - "# convert pandas dataframe to pandas-on-spark dataframe\n", - "psdf = to_pandas_on_spark(data)\n", + "# Convert to pandas-on-spark dataframe\n", + "psdf = to_pandas_on_spark(dataframe)\n", + "```\n", "\n", - "# merge feature columns as a single vector column\n", - "feature_cols = [col for col in psdf.columns if col != label]\n", - "featurizer = VectorAssembler(inputCols=feature_cols, outputCol=\"features\")\n", - "psdf = featurizer.transform(psdf.to_spark(index_col=\"index\"))[\"index\", \"features\"]\n", + "To use Spark ML models you need to format your data appropriately. Specifically, use [`VectorAssembler`](https://spark.apache.org/docs/latest/api/python/reference/api/pyspark.ml.feature.VectorAssembler.html) to merge all feature columns into a single vector column.\n", "\n", - "# start an AutoML experiment with lgbm_spark estimator\n", - "automl = flaml.AutoML()\n", - "settings = {\n", - " \"time_budget\": 30,\n", - " \"metric\": \"r2\",\n", - " \"estimator_list\": [\"lgbm_spark\"],\n", - " \"task\": \"regression\",\n", - "}\n", - "\n", - "automl.fit(\n", - " dataframe=psdf,\n", - " label=label,\n", - " **settings,\n", - ")\n", - "```\n", - "\n", - "--------------------------------------------------------------------------------\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "I want the time_budget to be 10 mins\n", - "\n", - "--------------------------------------------------------------------------------\n", - "I want the time_budget to be 10 mins\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "You can change the `time_budget` parameter in the `settings` dictionary to 10 minutes (600 seconds) like this:\n", + "Here is an example of how to use it:\n", "\n", "```python\n", - "import flaml\n", - "import pandas as pd\n", - "from flaml.automl.spark.utils import to_pandas_on_spark\n", "from pyspark.ml.feature import VectorAssembler\n", "\n", - "# load your stock price data into a pandas dataframe\n", - "data = pd.read_csv('stock_price.csv')\n", - "\n", - "# specify label column name\n", - "label = 'price'\n", - "\n", - "# convert pandas dataframe to pandas-on-spark dataframe\n", - "psdf = to_pandas_on_spark(data)\n", - "\n", - "# merge feature columns as a single vector column\n", - "feature_cols = [col for col in psdf.columns if col != label]\n", - "featurizer = VectorAssembler(inputCols=feature_cols, outputCol=\"features\")\n", - "psdf = featurizer.transform(psdf.to_spark(index_col=\"index\"))[\"index\", \"features\"]\n", - "\n", - "# start an AutoML experiment with lgbm_spark estimator and time_budget of 10 mins\n", - "automl = flaml.AutoML()\n", - "settings = {\n", - " \"time_budget\": 600, # time_budget in seconds\n", - " \"metric\": \"r2\",\n", - " \"estimator_list\": [\"lgbm_spark\"],\n", - " \"task\": \"regression\",\n", - "}\n", - "\n", - "automl.fit(\n", - " dataframe=psdf,\n", - " label=label,\n", - " **settings,\n", - ")\n", - "```\n", - "\n", - "\n", - "In this example, the `time_budget` parameter is set to 600, which represents the number of seconds the FLAML AutoML experiment will run. You can adjust this value to control the total time spent on the experiment.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> NO HUMAN INPUT RECEIVED.\u001b[0m\n", - "\u001b[31m\n", - ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "Is there anything else I can help you with?\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> NO HUMAN INPUT RECEIVED.\u001b[0m\n" - ] - } - ], - "source": [ - "# reset the assistant. Always reset the assistant before starting a new conversation.\n", - "assistant.reset()\n", - "\n", - "# set `human_input_mode` to be `ALWAYS`, so the agent will ask for human input at every step.\n", - "ragproxyagent.human_input_mode = \"ALWAYS\"\n", - "code_problem = \"how to build a time series forecasting model for stock price using FLAML?\"\n", - "ragproxyagent.initiate_chat(assistant, message=ragproxyagent.message_generator, problem=code_problem)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Example 4\n", - "\n", - "[Back to top](#table-of-contents)\n", - "\n", - "Use RetrieveChat to answer a question and ask for human-in-loop feedbacks.\n", - "\n", - "Problem: Is there a function named `tune_automl` in FLAML?" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING:chromadb.segment.impl.vector.local_persistent_hnsw:Number of requested results 20 is greater than number of elements in index 2, updating n_results = 2\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "doc_ids: [['doc_0', 'doc_1']]\n", - "\u001b[32mAdding doc_id doc_0 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", - "context provided by the user.\n", - "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", - "For code generation, you must obey the following rules:\n", - "Rule 1. You MUST NOT install any packages because all the packages needed are already installed.\n", - "Rule 2. You must follow the formats below to write your code:\n", - "```language\n", - "# your code\n", - "```\n", - "\n", - "User's question is: Is there a function named `tune_automl` in FLAML?\n", - "\n", - "Context is: # Integrate - Spark\n", - "\n", - "FLAML has integrated Spark for distributed training. There are two main aspects of integration with Spark:\n", - "- Use Spark ML estimators for AutoML.\n", - "- Use Spark to run training in parallel spark jobs.\n", - "\n", - "## Spark ML Estimators\n", - "\n", - "FLAML integrates estimators based on Spark ML models. These models are trained in parallel using Spark, so we called them Spark estimators. To use these models, you first need to organize your data in the required format.\n", - "\n", - "### Data\n", - "\n", - "For Spark estimators, AutoML only consumes Spark data. FLAML provides a convenient function `to_pandas_on_spark` in the `flaml.automl.spark.utils` module to convert your data into a pandas-on-spark (`pyspark.pandas`) dataframe/series, which Spark estimators require.\n", - "\n", - "This utility function takes data in the form of a `pandas.Dataframe` or `pyspark.sql.Dataframe` and converts it into a pandas-on-spark dataframe. It also takes `pandas.Series` or `pyspark.sql.Dataframe` and converts it into a [pandas-on-spark](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/index.html) series. If you pass in a `pyspark.pandas.Dataframe`, it will not make any changes.\n", - "\n", - "This function also accepts optional arguments `index_col` and `default_index_type`.\n", - "- `index_col` is the column name to use as the index, default is None.\n", - "- `default_index_type` is the default index type, default is \"distributed-sequence\". More info about default index type could be found on Spark official [documentation](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/options.html#default-index-type)\n", - "\n", - "Here is an example code snippet for Spark Data:\n", - "\n", - "```python\n", - "import pandas as pd\n", - "from flaml.automl.spark.utils import to_pandas_on_spark\n", - "# Creating a dictionary\n", - "data = {\"Square_Feet\": [800, 1200, 1800, 1500, 850],\n", - " \"Age_Years\": [20, 15, 10, 7, 25],\n", - " \"Price\": [100000, 200000, 300000, 240000, 120000]}\n", - "\n", - "# Creating a pandas DataFrame\n", - "dataframe = pd.DataFrame(data)\n", - "label = \"Price\"\n", - "\n", - "# Convert to pandas-on-spark dataframe\n", - "psdf = to_pandas_on_spark(dataframe)\n", - "```\n", - "\n", - "To use Spark ML models you need to format your data appropriately. Specifically, use [`VectorAssembler`](https://spark.apache.org/docs/latest/api/python/reference/api/pyspark.ml.feature.VectorAssembler.html) to merge all feature columns into a single vector column.\n", - "\n", - "Here is an example of how to use it:\n", - "```python\n", - "from pyspark.ml.feature import VectorAssembler\n", "columns = psdf.columns\n", "feature_cols = [col for col in columns if col != label]\n", "featurizer = VectorAssembler(inputCols=feature_cols, outputCol=\"features\")\n", @@ -1337,10 +1080,13 @@ "Later in conducting the experiment, use your pandas-on-spark data like non-spark data and pass them using `X_train, y_train` or `dataframe, label`.\n", "\n", "### Estimators\n", + "\n", "#### Model List\n", + "\n", "- `lgbm_spark`: The class for fine-tuning Spark version LightGBM models, using [SynapseML](https://microsoft.github.io/SynapseML/docs/features/lightgbm/about/) API.\n", "\n", "#### Usage\n", + "\n", "First, prepare your data in the required format as described in the previous section.\n", "\n", "By including the models you intend to try in the `estimators_list` argument to `flaml.automl`, FLAML will start trying configurations for these models. If your input is Spark data, FLAML will also use estimators with the `_spark` postfix by default, even if you haven't specified them.\n", @@ -1349,6 +1095,7 @@ "\n", "```python\n", "import flaml\n", + "\n", "# prepare your data in pandas-on-spark format as we previously mentioned\n", "\n", "automl = flaml.AutoML()\n", @@ -1366,24 +1113,25 @@ ")\n", "```\n", "\n", - "\n", "[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/automl_bankrupt_synapseml.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/automl_bankrupt_synapseml.ipynb)\n", "\n", "## Parallel Spark Jobs\n", + "\n", "You can activate Spark as the parallel backend during parallel tuning in both [AutoML](/docs/Use-Cases/Task-Oriented-AutoML#parallel-tuning) and [Hyperparameter Tuning](/docs/Use-Cases/Tune-User-Defined-Function#parallel-tuning), by setting the `use_spark` to `true`. FLAML will dispatch your job to the distributed Spark backend using [`joblib-spark`](https://github.com/joblib/joblib-spark).\n", "\n", "Please note that you should not set `use_spark` to `true` when applying AutoML and Tuning for Spark Data. This is because only SparkML models will be used for Spark Data in AutoML and Tuning. As SparkML models run in parallel, there is no need to distribute them with `use_spark` again.\n", "\n", "All the Spark-related arguments are stated below. These arguments are available in both Hyperparameter Tuning and AutoML:\n", "\n", - "\n", "- `use_spark`: boolean, default=False | Whether to use spark to run the training in parallel spark jobs. This can be used to accelerate training on large models and large datasets, but will incur more overhead in time and thus slow down training in some cases. GPU training is not supported yet when use_spark is True. For Spark clusters, by default, we will launch one trial per executor. However, sometimes we want to launch more trials than the number of executors (e.g., local mode). In this case, we can set the environment variable `FLAML_MAX_CONCURRENT` to override the detected `num_executors`. The final number of concurrent trials will be the minimum of `n_concurrent_trials` and `num_executors`.\n", - "- `n_concurrent_trials`: int, default=1 | The number of concurrent trials. When n_concurrent_trials > 1, FLAML performs parallel tuning.\n", + "- `n_concurrent_trials`: int, default=1 | The number of concurrent trials. When n_concurrent_trials > 1, FLAML performes parallel tuning.\n", "- `force_cancel`: boolean, default=False | Whether to forcely cancel Spark jobs if the search time exceeded the time budget. Spark jobs include parallel tuning jobs and Spark-based model training jobs.\n", "\n", "An example code snippet for using parallel Spark jobs:\n", + "\n", "```python\n", "import flaml\n", + "\n", "automl_experiment = flaml.AutoML()\n", "automl_settings = {\n", " \"time_budget\": 30,\n", @@ -1391,7 +1139,7 @@ " \"task\": \"regression\",\n", " \"n_concurrent_trials\": 2,\n", " \"use_spark\": True,\n", - " \"force_cancel\": True, # Activating the force_cancel option can immediately halt Spark jobs once they exceed the allocated time_budget.\n", + " \"force_cancel\": True, # Activating the force_cancel option can immediately halt Spark jobs once they exceed the allocated time_budget.\n", "}\n", "\n", "automl.fit(\n", @@ -1401,14 +1149,36 @@ ")\n", "```\n", "\n", - "\n", "[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb)\n", "\n", - "# Research\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", + "\n", + "The authors of FLAML are Chi Wang, Qingyun Wu, Markus Weimer, and Erkang Zhu.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32mAdding content of doc bdfbc921 to context.\u001b[0m\n", + "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", + "\n", + "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", + "context provided by the user.\n", + "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", + "For code generation, you must obey the following rules:\n", + "Rule 1. You MUST NOT install any packages because all the packages needed are already installed.\n", + "Rule 2. You must follow the formats below to write your code:\n", + "```language\n", + "# your code\n", + "```\n", + "\n", + "User's question is: Who is the author of FLAML?\n", + "\n", + "Context is: # Research\n", "\n", "For technical details, please check our research publications.\n", "\n", - "* [FLAML: A Fast and Lightweight AutoML Library](https://www.microsoft.com/en-us/research/publication/flaml-a-fast-and-lightweight-automl-library/). Chi Wang, Qingyun Wu, Markus Weimer, Erkang Zhu. MLSys 2021.\n", + "- [FLAML: A Fast and Lightweight AutoML Library](https://www.microsoft.com/en-us/research/publication/flaml-a-fast-and-lightweight-automl-library/). Chi Wang, Qingyun Wu, Markus Weimer, Erkang Zhu. MLSys 2021.\n", "\n", "```bibtex\n", "@inproceedings{wang2021flaml,\n", @@ -1419,7 +1189,7 @@ "}\n", "```\n", "\n", - "* [Frugal Optimization for Cost-related Hyperparameters](https://arxiv.org/abs/2005.01571). Qingyun Wu, Chi Wang, Silu Huang. AAAI 2021.\n", + "- [Frugal Optimization for Cost-related Hyperparameters](https://arxiv.org/abs/2005.01571). Qingyun Wu, Chi Wang, Silu Huang. AAAI 2021.\n", "\n", "```bibtex\n", "@inproceedings{wu2021cfo,\n", @@ -1430,7 +1200,7 @@ "}\n", "```\n", "\n", - "* [Economical Hyperparameter Optimization With Blended Search Strategy](https://www.microsoft.com/en-us/research/publication/economical-hyperparameter-optimization-with-blended-search-strategy/). Chi Wang, Qingyun Wu, Silu Huang, Amin Saied. ICLR 2021.\n", + "- [Economical Hyperparameter Optimization With Blended Search Strategy](https://www.microsoft.com/en-us/research/publication/economical-hyperparameter-optimization-with-blended-search-strategy/). Chi Wang, Qingyun Wu, Silu Huang, Amin Saied. ICLR 2021.\n", "\n", "```bibtex\n", "@inproceedings{wang2021blendsearch,\n", @@ -1441,7 +1211,7 @@ "}\n", "```\n", "\n", - "* [An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models](https://aclanthology.org/2021.acl-long.178.pdf). Susan Xueqing Liu, Chi Wang. ACL 2021.\n", + "- [An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models](https://aclanthology.org/2021.acl-long.178.pdf). Susan Xueqing Liu, Chi Wang. ACL 2021.\n", "\n", "```bibtex\n", "@inproceedings{liuwang2021hpolm,\n", @@ -1452,7 +1222,7 @@ "}\n", "```\n", "\n", - "* [ChaCha for Online AutoML](https://www.microsoft.com/en-us/research/publication/chacha-for-online-automl/). Qingyun Wu, Chi Wang, John Langford, Paul Mineiro and Marco Rossi. ICML 2021.\n", + "- [ChaCha for Online AutoML](https://www.microsoft.com/en-us/research/publication/chacha-for-online-automl/). Qingyun Wu, Chi Wang, John Langford, Paul Mineiro and Marco Rossi. ICML 2021.\n", "\n", "```bibtex\n", "@inproceedings{wu2021chacha,\n", @@ -1463,7 +1233,7 @@ "}\n", "```\n", "\n", - "* [Fair AutoML](https://arxiv.org/abs/2111.06495). Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2111.06495 (2021).\n", + "- [Fair AutoML](https://arxiv.org/abs/2111.06495). Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2111.06495 (2021).\n", "\n", "```bibtex\n", "@inproceedings{wuwang2021fairautoml,\n", @@ -1474,7 +1244,7 @@ "}\n", "```\n", "\n", - "* [Mining Robust Default Configurations for Resource-constrained AutoML](https://arxiv.org/abs/2202.09927). Moe Kayali, Chi Wang. ArXiv preprint arXiv:2202.09927 (2022).\n", + "- [Mining Robust Default Configurations for Resource-constrained AutoML](https://arxiv.org/abs/2202.09927). Moe Kayali, Chi Wang. ArXiv preprint arXiv:2202.09927 (2022).\n", "\n", "```bibtex\n", "@inproceedings{kayaliwang2022default,\n", @@ -1485,7 +1255,7 @@ "}\n", "```\n", "\n", - "* [Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives](https://openreview.net/forum?id=0Ij9_q567Ma). Shaokun Zhang, Feiran Jia, Chi Wang, Qingyun Wu. ICLR 2023 (notable-top-5%).\n", + "- [Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives](https://openreview.net/forum?id=0Ij9_q567Ma). Shaokun Zhang, Feiran Jia, Chi Wang, Qingyun Wu. ICLR 2023 (notable-top-5%).\n", "\n", "```bibtex\n", "@inproceedings{zhang2023targeted,\n", @@ -1497,7 +1267,7 @@ "}\n", "```\n", "\n", - "* [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673). Chi Wang, Susan Xueqing Liu, Ahmed H. Awadallah. ArXiv preprint arXiv:2303.04673 (2023).\n", + "- [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673). Chi Wang, Susan Xueqing Liu, Ahmed H. Awadallah. ArXiv preprint arXiv:2303.04673 (2023).\n", "\n", "```bibtex\n", "@inproceedings{wang2023EcoOptiGen,\n", @@ -1508,7 +1278,7 @@ "}\n", "```\n", "\n", - "* [An Empirical Study on Challenging Math Problem Solving with GPT-4](https://arxiv.org/abs/2306.01337). Yiran Wu, Feiran Jia, Shaokun Zhang, Hangyu Li, Erkang Zhu, Yue Wang, Yin Tat Lee, Richard Peng, Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2306.01337 (2023).\n", + "- [An Empirical Study on Challenging Math Problem Solving with GPT-4](https://arxiv.org/abs/2306.01337). Yiran Wu, Feiran Jia, Shaokun Zhang, Hangyu Li, Erkang Zhu, Yue Wang, Yin Tat Lee, Richard Peng, Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2306.01337 (2023).\n", "\n", "```bibtex\n", "@inproceedings{wu2023empirical,\n", @@ -1518,29 +1288,10 @@ " booktitle={ArXiv preprint arXiv:2306.01337},\n", "}\n", "```\n", - "\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32mAdding doc_id doc_1 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", - "context provided by the user.\n", - "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", - "For code generation, you must obey the following rules:\n", - "Rule 1. You MUST NOT install any packages because all the packages needed are already installed.\n", - "Rule 2. You must follow the formats below to write your code:\n", - "```language\n", - "# your code\n", - "```\n", - "\n", - "User's question is: Is there a function named `tune_automl` in FLAML?\n", - "\n", - "Context is: # Integrate - Spark\n", + "# Integrate - Spark\n", "\n", "FLAML has integrated Spark for distributed training. There are two main aspects of integration with Spark:\n", + "\n", "- Use Spark ML estimators for AutoML.\n", "- Use Spark to run training in parallel spark jobs.\n", "\n", @@ -1555,6 +1306,7 @@ "This utility function takes data in the form of a `pandas.Dataframe` or `pyspark.sql.Dataframe` and converts it into a pandas-on-spark dataframe. It also takes `pandas.Series` or `pyspark.sql.Dataframe` and converts it into a [pandas-on-spark](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/index.html) series. If you pass in a `pyspark.pandas.Dataframe`, it will not make any changes.\n", "\n", "This function also accepts optional arguments `index_col` and `default_index_type`.\n", + "\n", "- `index_col` is the column name to use as the index, default is None.\n", "- `default_index_type` is the default index type, default is \"distributed-sequence\". More info about default index type could be found on Spark official [documentation](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/options.html#default-index-type)\n", "\n", @@ -1563,10 +1315,13 @@ "```python\n", "import pandas as pd\n", "from flaml.automl.spark.utils import to_pandas_on_spark\n", + "\n", "# Creating a dictionary\n", - "data = {\"Square_Feet\": [800, 1200, 1800, 1500, 850],\n", - " \"Age_Years\": [20, 15, 10, 7, 25],\n", - " \"Price\": [100000, 200000, 300000, 240000, 120000]}\n", + "data = {\n", + " \"Square_Feet\": [800, 1200, 1800, 1500, 850],\n", + " \"Age_Years\": [20, 15, 10, 7, 25],\n", + " \"Price\": [100000, 200000, 300000, 240000, 120000],\n", + "}\n", "\n", "# Creating a pandas DataFrame\n", "dataframe = pd.DataFrame(data)\n", @@ -1579,8 +1334,10 @@ "To use Spark ML models you need to format your data appropriately. Specifically, use [`VectorAssembler`](https://spark.apache.org/docs/latest/api/python/reference/api/pyspark.ml.feature.VectorAssembler.html) to merge all feature columns into a single vector column.\n", "\n", "Here is an example of how to use it:\n", + "\n", "```python\n", "from pyspark.ml.feature import VectorAssembler\n", + "\n", "columns = psdf.columns\n", "feature_cols = [col for col in columns if col != label]\n", "featurizer = VectorAssembler(inputCols=feature_cols, outputCol=\"features\")\n", @@ -1590,10 +1347,13 @@ "Later in conducting the experiment, use your pandas-on-spark data like non-spark data and pass them using `X_train, y_train` or `dataframe, label`.\n", "\n", "### Estimators\n", + "\n", "#### Model List\n", + "\n", "- `lgbm_spark`: The class for fine-tuning Spark version LightGBM models, using [SynapseML](https://microsoft.github.io/SynapseML/docs/features/lightgbm/about/) API.\n", "\n", "#### Usage\n", + "\n", "First, prepare your data in the required format as described in the previous section.\n", "\n", "By including the models you intend to try in the `estimators_list` argument to `flaml.automl`, FLAML will start trying configurations for these models. If your input is Spark data, FLAML will also use estimators with the `_spark` postfix by default, even if you haven't specified them.\n", @@ -1602,6 +1362,7 @@ "\n", "```python\n", "import flaml\n", + "\n", "# prepare your data in pandas-on-spark format as we previously mentioned\n", "\n", "automl = flaml.AutoML()\n", @@ -1619,24 +1380,25 @@ ")\n", "```\n", "\n", - "\n", "[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/automl_bankrupt_synapseml.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/automl_bankrupt_synapseml.ipynb)\n", "\n", "## Parallel Spark Jobs\n", + "\n", "You can activate Spark as the parallel backend during parallel tuning in both [AutoML](/docs/Use-Cases/Task-Oriented-AutoML#parallel-tuning) and [Hyperparameter Tuning](/docs/Use-Cases/Tune-User-Defined-Function#parallel-tuning), by setting the `use_spark` to `true`. FLAML will dispatch your job to the distributed Spark backend using [`joblib-spark`](https://github.com/joblib/joblib-spark).\n", "\n", "Please note that you should not set `use_spark` to `true` when applying AutoML and Tuning for Spark Data. This is because only SparkML models will be used for Spark Data in AutoML and Tuning. As SparkML models run in parallel, there is no need to distribute them with `use_spark` again.\n", "\n", "All the Spark-related arguments are stated below. These arguments are available in both Hyperparameter Tuning and AutoML:\n", "\n", - "\n", "- `use_spark`: boolean, default=False | Whether to use spark to run the training in parallel spark jobs. This can be used to accelerate training on large models and large datasets, but will incur more overhead in time and thus slow down training in some cases. GPU training is not supported yet when use_spark is True. For Spark clusters, by default, we will launch one trial per executor. However, sometimes we want to launch more trials than the number of executors (e.g., local mode). In this case, we can set the environment variable `FLAML_MAX_CONCURRENT` to override the detected `num_executors`. The final number of concurrent trials will be the minimum of `n_concurrent_trials` and `num_executors`.\n", - "- `n_concurrent_trials`: int, default=1 | The number of concurrent trials. When n_concurrent_trials > 1, FLAML performs parallel tuning.\n", + "- `n_concurrent_trials`: int, default=1 | The number of concurrent trials. When n_concurrent_trials > 1, FLAML performes parallel tuning.\n", "- `force_cancel`: boolean, default=False | Whether to forcely cancel Spark jobs if the search time exceeded the time budget. Spark jobs include parallel tuning jobs and Spark-based model training jobs.\n", "\n", "An example code snippet for using parallel Spark jobs:\n", + "\n", "```python\n", "import flaml\n", + "\n", "automl_experiment = flaml.AutoML()\n", "automl_settings = {\n", " \"time_budget\": 30,\n", @@ -1644,7 +1406,7 @@ " \"task\": \"regression\",\n", " \"n_concurrent_trials\": 2,\n", " \"use_spark\": True,\n", - " \"force_cancel\": True, # Activating the force_cancel option can immediately halt Spark jobs once they exceed the allocated time_budget.\n", + " \"force_cancel\": True, # Activating the force_cancel option can immediately halt Spark jobs once they exceed the allocated time_budget.\n", "}\n", "\n", "automl.fit(\n", @@ -1654,133 +1416,14 @@ ")\n", "```\n", "\n", - "\n", "[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb)\n", "\n", - "# Research\n", - "\n", - "For technical details, please check our research publications.\n", "\n", - "* [FLAML: A Fast and Lightweight AutoML Library](https://www.microsoft.com/en-us/research/publication/flaml-a-fast-and-lightweight-automl-library/). Chi Wang, Qingyun Wu, Markus Weimer, Erkang Zhu. MLSys 2021.\n", "\n", - "```bibtex\n", - "@inproceedings{wang2021flaml,\n", - " title={FLAML: A Fast and Lightweight AutoML Library},\n", - " author={Chi Wang and Qingyun Wu and Markus Weimer and Erkang Zhu},\n", - " year={2021},\n", - " booktitle={MLSys},\n", - "}\n", - "```\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", "\n", - "* [Frugal Optimization for Cost-related Hyperparameters](https://arxiv.org/abs/2005.01571). Qingyun Wu, Chi Wang, Silu Huang. AAAI 2021.\n", - "\n", - "```bibtex\n", - "@inproceedings{wu2021cfo,\n", - " title={Frugal Optimization for Cost-related Hyperparameters},\n", - " author={Qingyun Wu and Chi Wang and Silu Huang},\n", - " year={2021},\n", - " booktitle={AAAI},\n", - "}\n", - "```\n", - "\n", - "* [Economical Hyperparameter Optimization With Blended Search Strategy](https://www.microsoft.com/en-us/research/publication/economical-hyperparameter-optimization-with-blended-search-strategy/). Chi Wang, Qingyun Wu, Silu Huang, Amin Saied. ICLR 2021.\n", - "\n", - "```bibtex\n", - "@inproceedings{wang2021blendsearch,\n", - " title={Economical Hyperparameter Optimization With Blended Search Strategy},\n", - " author={Chi Wang and Qingyun Wu and Silu Huang and Amin Saied},\n", - " year={2021},\n", - " booktitle={ICLR},\n", - "}\n", - "```\n", - "\n", - "* [An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models](https://aclanthology.org/2021.acl-long.178.pdf). Susan Xueqing Liu, Chi Wang. ACL 2021.\n", - "\n", - "```bibtex\n", - "@inproceedings{liuwang2021hpolm,\n", - " title={An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models},\n", - " author={Susan Xueqing Liu and Chi Wang},\n", - " year={2021},\n", - " booktitle={ACL},\n", - "}\n", - "```\n", - "\n", - "* [ChaCha for Online AutoML](https://www.microsoft.com/en-us/research/publication/chacha-for-online-automl/). Qingyun Wu, Chi Wang, John Langford, Paul Mineiro and Marco Rossi. ICML 2021.\n", - "\n", - "```bibtex\n", - "@inproceedings{wu2021chacha,\n", - " title={ChaCha for Online AutoML},\n", - " author={Qingyun Wu and Chi Wang and John Langford and Paul Mineiro and Marco Rossi},\n", - " year={2021},\n", - " booktitle={ICML},\n", - "}\n", - "```\n", - "\n", - "* [Fair AutoML](https://arxiv.org/abs/2111.06495). Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2111.06495 (2021).\n", - "\n", - "```bibtex\n", - "@inproceedings{wuwang2021fairautoml,\n", - " title={Fair AutoML},\n", - " author={Qingyun Wu and Chi Wang},\n", - " year={2021},\n", - " booktitle={ArXiv preprint arXiv:2111.06495},\n", - "}\n", - "```\n", - "\n", - "* [Mining Robust Default Configurations for Resource-constrained AutoML](https://arxiv.org/abs/2202.09927). Moe Kayali, Chi Wang. ArXiv preprint arXiv:2202.09927 (2022).\n", - "\n", - "```bibtex\n", - "@inproceedings{kayaliwang2022default,\n", - " title={Mining Robust Default Configurations for Resource-constrained AutoML},\n", - " author={Moe Kayali and Chi Wang},\n", - " year={2022},\n", - " booktitle={ArXiv preprint arXiv:2202.09927},\n", - "}\n", - "```\n", - "\n", - "* [Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives](https://openreview.net/forum?id=0Ij9_q567Ma). Shaokun Zhang, Feiran Jia, Chi Wang, Qingyun Wu. ICLR 2023 (notable-top-5%).\n", - "\n", - "```bibtex\n", - "@inproceedings{zhang2023targeted,\n", - " title={Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives},\n", - " author={Shaokun Zhang and Feiran Jia and Chi Wang and Qingyun Wu},\n", - " booktitle={International Conference on Learning Representations},\n", - " year={2023},\n", - " url={https://openreview.net/forum?id=0Ij9_q567Ma},\n", - "}\n", - "```\n", - "\n", - "* [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673). Chi Wang, Susan Xueqing Liu, Ahmed H. Awadallah. ArXiv preprint arXiv:2303.04673 (2023).\n", - "\n", - "```bibtex\n", - "@inproceedings{wang2023EcoOptiGen,\n", - " title={Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference},\n", - " author={Chi Wang and Susan Xueqing Liu and Ahmed H. Awadallah},\n", - " year={2023},\n", - " booktitle={ArXiv preprint arXiv:2303.04673},\n", - "}\n", - "```\n", - "\n", - "* [An Empirical Study on Challenging Math Problem Solving with GPT-4](https://arxiv.org/abs/2306.01337). Yiran Wu, Feiran Jia, Shaokun Zhang, Hangyu Li, Erkang Zhu, Yue Wang, Yin Tat Lee, Richard Peng, Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2306.01337 (2023).\n", - "\n", - "```bibtex\n", - "@inproceedings{wu2023empirical,\n", - " title={An Empirical Study on Challenging Math Problem Solving with GPT-4},\n", - " author={Yiran Wu and Feiran Jia and Shaokun Zhang and Hangyu Li and Erkang Zhu and Yue Wang and Yin Tat Lee and Richard Peng and Qingyun Wu and Chi Wang},\n", - " year={2023},\n", - " booktitle={ArXiv preprint arXiv:2306.01337},\n", - "}\n", - "```\n", - "\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "There is no function named `tune_automl` in FLAML. However, FLAML has integrated Spark for distributed training. There are two main aspects of integration with Spark: \n", - "- Use Spark ML Estimators for AutoML.\n", - "- Use Spark to run training in parallel Spark jobs.\n", + "The authors of FLAML are Chi Wang, Qingyun Wu, Markus Weimer, and Erkang Zhu.\n", "\n", "--------------------------------------------------------------------------------\n" ] @@ -1790,1030 +1433,8 @@ "# reset the assistant. Always reset the assistant before starting a new conversation.\n", "assistant.reset()\n", "\n", - "# set `human_input_mode` to be `ALWAYS`, so the agent will ask for human input at every step.\n", - "ragproxyagent.human_input_mode = \"ALWAYS\"\n", - "qa_problem = \"Is there a function named `tune_automl` in FLAML?\"\n", - "ragproxyagent.initiate_chat(\n", - " assistant, message=ragproxyagent.message_generator, problem=qa_problem\n", - ") # type \"exit\" to exit the conversation" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Example 5\n", - "\n", - "[Back to top](#table-of-contents)\n", - "\n", - "Use RetrieveChat to answer questions for [NaturalQuestion](https://ai.google.com/research/NaturalQuestions) dataset.\n", - "\n", - "First, we will create a new document collection which includes all the contextual corpus. Then, we will choose some questions and utilize RetrieveChat to answer them. For this particular example, we will be using the `gpt-3.5-turbo` model, and we will demonstrate RetrieveChat's feature of automatically updating context in case the documents retrieved do not contain sufficient information." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "config_list[0][\"model\"] = \"gpt-35-turbo\" # change model to gpt-35-turbo" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "corpus_file = \"https://huggingface.co/datasets/thinkall/NaturalQuestionsQA/resolve/main/corpus.txt\"\n", - "\n", - "# Create a new collection for NaturalQuestions dataset\n", - "# `task` indicates the kind of task we're working on. In this example, it's a `qa` task.\n", - "ragproxyagent = RetrieveUserProxyAgent(\n", - " name=\"ragproxyagent\",\n", - " human_input_mode=\"NEVER\",\n", - " max_consecutive_auto_reply=10,\n", - " retrieve_config={\n", - " \"task\": \"qa\",\n", - " \"docs_path\": corpus_file,\n", - " \"chunk_token_size\": 2000,\n", - " \"model\": config_list[0][\"model\"],\n", - " \"client\": chromadb.PersistentClient(path=\"/tmp/chromadb\"),\n", - " \"collection_name\": \"natural-questions\",\n", - " \"chunk_mode\": \"one_line\",\n", - " \"embedding_model\": \"all-MiniLM-L6-v2\",\n", - " },\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['what is non controlling interest on balance sheet', 'how many episodes are in chicago fire season 4', 'what are bulls used for on a farm', 'has been honoured with the wisden leading cricketer in the world award for 2016', 'who carried the usa flag in opening ceremony']\n", - "[[\"the portion of a subsidiary corporation 's stock that is not owned by the parent corporation\"], ['23'], ['breeding', 'as work oxen', 'slaughtered for meat'], ['Virat Kohli'], ['Erin Hamlin']]\n" - ] - } - ], - "source": [ - "# queries_file = \"https://huggingface.co/datasets/thinkall/NaturalQuestionsQA/resolve/main/queries.jsonl\"\n", - "queries = \"\"\"{\"_id\": \"ce2342e1feb4e119cb273c05356b33309d38fa132a1cbeac2368a337e38419b8\", \"text\": \"what is non controlling interest on balance sheet\", \"metadata\": {\"answer\": [\"the portion of a subsidiary corporation 's stock that is not owned by the parent corporation\"]}}\n", - "{\"_id\": \"3a10ff0e520530c0aa33b2c7e8d989d78a8cd5d699201fc4b13d3845010994ee\", \"text\": \"how many episodes are in chicago fire season 4\", \"metadata\": {\"answer\": [\"23\"]}}\n", - "{\"_id\": \"fcdb6b11969d5d3b900806f52e3d435e615c333405a1ff8247183e8db6246040\", \"text\": \"what are bulls used for on a farm\", \"metadata\": {\"answer\": [\"breeding\", \"as work oxen\", \"slaughtered for meat\"]}}\n", - "{\"_id\": \"26c3b53ec44533bbdeeccffa32e094cfea0cc2a78c9f6a6c7a008ada1ad0792e\", \"text\": \"has been honoured with the wisden leading cricketer in the world award for 2016\", \"metadata\": {\"answer\": [\"Virat Kohli\"]}}\n", - "{\"_id\": \"0868d0964c719a52cbcfb116971b0152123dad908ac4e0a01bc138f16a907ab3\", \"text\": \"who carried the usa flag in opening ceremony\", \"metadata\": {\"answer\": [\"Erin Hamlin\"]}}\n", - "\"\"\"\n", - "queries = [json.loads(line) for line in queries.split(\"\\n\") if line]\n", - "questions = [q[\"text\"] for q in queries]\n", - "answers = [q[\"metadata\"][\"answer\"] for q in queries]\n", - "print(questions)\n", - "print(answers)" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - ">>>>>>>>>>>> Below are outputs of Case 1 <<<<<<<<<<<<\n", - "\n", - "\n", - "Trying to create collection.\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
Film Year Fuck count Minutes Uses / mi ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
Character Ultimate Avengers Ultimate Avengers 2 I ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
Position Country Town / City PM2. 5 PM ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
Rank Country ( or dependent territory ) Population
Rank State Gross collections ( in thousands ) Rev ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t < ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
Date Province Mag . MMI Deaths
City River State
Gangakhed ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
Player Pos . Team Career start Career ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t ABO and Rh blood type distribution by country ( population averages )
Country
Total area Land area Performance in the European Cup and UEFA Champions League by club
  • ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
    Rank City State Land area ( sq mi ) La ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
    # Country Name International goals Cap ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
    Rank City Image Population Definition ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
    Rank Team Won Lost Tied Pct ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
    Territory Rights holder Ref
    Asia
    ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
    ( hide ) Rank Nat Name Years Goals
    Total area Land area
    Bids by school Most recent
    Rank Name Nation TP SP
    2014 Rank City 2014 Estimate 2010 Census
    S.No . Year Name
    1961
    Densities of various materials covering a range of values
    Material ρ ( ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
    Club Season League Nation ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
    Rank ( 2016 ) Airports ( large hubs ) IATA Code M ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
    City Region / State Country Park name ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
    Year Winner ( nationally ) Votes Percent
    Compound SERT NET DAT 5 - HT
    Rank Name Industry Revenue ( USD millions )
    ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
    Rank Name Name in Georgian Population 1989
    Country The World Factbook World Res ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
    Rank Country Area ( km2 ) Notes
    ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
    Rank Country Area ( km2 ) Notes
    Date State ( s ) Magnitude Fatalities ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t < ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t
    Artist # Gold # Platinum # Multi-Platinum
    Name Number of locations Revenue
    Name Country Region Depth ( meters ) < ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "UPDATE CONTEXT. The current context does not provide information related to the question.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32mUpdating context and resetting conversation.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1122 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2398 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_309 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3891 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2087 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_330 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_4844 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented chatbot. You answer user's questions based on your own knowledge and the\n", - "context provided by the user.\n", - "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", - "You must give as short an answer as possible.\n", - "\n", - "User's question is: has been honoured with the wisden leading cricketer in the world award for 2016\n", - "\n", - "Context is:
    Rank Player ( 2017 HRs ) HR
    ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\t ...\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "doc_ids: [['doc_0', 'doc_3334', 'doc_720', 'doc_2732', 'doc_2510', 'doc_5084', 'doc_5068', 'doc_3727', 'doc_1938', 'doc_4689', 'doc_5249', 'doc_1751', 'doc_480', 'doc_3989', 'doc_2115', 'doc_1233', 'doc_2264', 'doc_633', 'doc_2376', 'doc_2293', 'doc_5274', 'doc_5213', 'doc_3991', 'doc_2880', 'doc_2737', 'doc_1257', 'doc_1748', 'doc_2038', 'doc_4073', 'doc_2876']]\n", - "\u001b[32mAdding doc_id doc_0 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3334 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_720 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2732 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2510 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_5084 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_5068 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3727 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1938 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_4689 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_5249 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1751 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_480 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3989 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3334 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_720 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2732 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2510 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_5084 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_5068 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3727 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1938 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_4689 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_5249 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1751 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_480 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3989 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2115 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1233 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2264 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_633 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2376 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented chatbot. You answer user's questions based on your own knowledge and the\n", - "context provided by the user.\n", - "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", - "You must give as short an answer as possible.\n", - "\n", - "User's question is: what is non controlling interest on balance sheet\n", - "\n", - "Context is:

    In accounting , minority interest ( or non-controlling interest ) is the portion of a subsidiary corporation 's stock that is not owned by the parent corporation . The magnitude of the minority interest in the subsidiary company is generally less than 50 % of outstanding shares , or the corporation would generally cease to be a subsidiary of the parent .

    \n", - "

    The balance sheet is the financial statement showing a firm 's assets , liabilities and equity ( capital ) at a set point in time , usually the end of the fiscal year reported on the accompanying income statement . The total assets always equal the total combined liabilities and equity in dollar amount . This statement best demonstrates the basic accounting equation - Assets = Liabilities + Equity . The statement can be used to help show the status of a company .

    \n", - "

    The comptroller ( who is also auditor general and head of the National Audit Office ) controls both the Consolidated Fund and the National Loans Fund . The full official title of the role is Comptroller General of the Receipt and Issue of Her Majesty 's Exchequer .

    \n", - "

    Financing activities include the inflow of cash from investors such as banks and shareholders , as well as the outflow of cash to shareholders as dividends as the company generates income . Other activities which impact the long - term liabilities and equity of the company are also listed in the financing activities section of the cash flow statement .

    \n", - "

    It is frequently claimed that annual accounts have not been certified by the external auditor since 1994 . In its annual report on the implementation of the 2009 EU Budget , the Court of Auditors found that the two biggest areas of the EU budget , agriculture and regional spending , have not been signed off on and remain `` materially affected by error '' .

    \n", - "

    The Ministry of Finance , Government of India announces the rate of interest for PPF account every quarter . The current interest rate effective from 1 January 2018 is 7.6 % Per Annum ' ( compounded annually ) . Interest will be paid on 31 March every year . Interest is calculated on the lowest balance between the close of the fifth day and the last day of every month .

    \n", - "
    No . Athlete Nation Sport Years
    Quarter Interest Rate
    April 2018 - June 2018 7.6 %
    \n", - "

    For a percentage of the settlement amount , Public adjusters work exclusively for the policyholder . This means there should be no inherent conflict of interest when it comes to advocating on the policyholder 's behalf to the insurance company .

    \n", - "

    Accounts receivable is a legally enforceable claim for payment held by a business for goods supplied and / or services rendered that customers / clients have ordered but not paid for . These are generally in the form of invoices raised by a business and delivered to the customer for payment within an agreed time frame . Accounts receivable is shown in a balance sheet as an asset . It is one of a series of accounting transactions dealing with the billing of a customer for goods and services that the customer has ordered . These may be distinguished from notes receivable , which are debts created through formal legal instruments called promissory notes .

    \n", - "

    A common synonym for net profit when discussing financial statements ( which include a balance sheet and an income statement ) is the bottom line . This term results from the traditional appearance of an income statement which shows all allocated revenues and expenses over a specified time period with the resulting summation on the bottom line of the report .

    \n", - " Electronic Fund Transfer Act
    Other short titles
    • Financial Institutions Regulatory and Interest Rate Control Act of 1978
    • Change in Bank Control Act
    • Change in Savings and Loan Control Act
    • Depository Institution Management Interlocks Act
    • Export - Import Bank Act Amendments
    • Federal Financial Institutions Examination Council Act
    • National Credit Union Central Liquidity Facility Act
    • Right to Financial Privacy Act
    Long title An Act to extend the authority for the flexible regulation of interest rates on deposits and accounts in depository institutions .
    Nicknames American Arts Gold Medallion Act
    Enacted by the 95th United States Congress
    Effective November 10 , 1978
    Citations
    Public law 95 - 630
    Statutes at Large 92 Stat. 3641 aka 92 Stat. 3728
    Codification
    Titles amended
    • 12 U.S.C. : Banks and Banking
    • 15 U.S.C. : Commerce and Trade
    U.S.C. sections amended
    • 12 U.S.C. ch. 3 § 226 et seq .
    • 15 U.S.C. ch. 41 § 1601 et seq .
    • 15 U.S.C. ch. 41 § 1693 et seq .
    Legislative history
    • Introduced in the House as H.R. 14279 by Fernand St. Germain ( D - RI ) on October 10 , 1978
    • Committee consideration by House Banking , Finance , and Urban Affairs , Senate Banking , Housing , and Urban Affairs
    • Passed the House on October 11 , 1978 ( passed )
    • Passed the Senate on October 12 , 1978 ( passed ) with amendment
    • House agreed to Senate amendment on October 14 , 1978 ( 341 - 32 , in lieu of H. Res. 1439 ) with further amendment
    • Senate agreed to House amendment on October 14 , 1978 ( agreed )
    • Signed into law by President Jimmy Carter on November 10 , 1978
    Major amendments
    Credit CARD Act of 2009
    \n", - "

    Financial management refers to the efficient and effective management of money ( funds ) in such a manner as to accomplish the objectives of the organization . It is the specialized function directly associated with the top management . The significance of this function is not seen in the ' Line ' but also in the capacity of the ' Staff ' in overall of a company . It has been defined differently by different experts in the field .

    \n", - "

    Form 990 ( officially , the `` Return of Organization Exempt From Income Tax '' ) is a United States Internal Revenue Service form that provides the public with financial information about a nonprofit organization . It is often the only source of such information . It is also used by government agencies to prevent organizations from abusing their tax - exempt status . Certain nonprofits have more comprehensive reporting requirements , such as hospitals and other health care organizations ( Schedule H ) .

    \n", - "

    The Board of Governors of the Federal Reserve System , commonly known as the Federal Reserve Board , is the main governing body of the Federal Reserve System . It is charged with overseeing the Federal Reserve Banks and with helping implement monetary policy of the United States . Governors are appointed by the President of the United States and confirmed by the Senate for staggered 14 - year terms .

    \n", - "

    The International Monetary Fund ( IMF ) is an international organization headquartered in Washington , D.C. , of `` 189 countries working to foster global monetary cooperation , secure financial stability , facilitate international trade , promote high employment and sustainable economic growth , and reduce poverty around the world . '' Formed in 1945 at the Bretton Woods Conference primarily by the ideas of Harry Dexter White and John Maynard Keynes , it came into formal existence in 1945 with 29 member countries and the goal of reconstructing the international payment system . It now plays a central role in the management of balance of payments difficulties and international financial crises . Countries contribute funds to a pool through a quota system from which countries experiencing balance of payments problems can borrow money . As of 2016 , the fund had SDR 477 billion ( about $668 billion ) .

    \n", - "
  • Callability -- Some bonds give the issuer the right to repay the bond before the maturity date on the call dates ; see call option . These bonds are referred to as callable bonds . Most callable bonds allow the issuer to repay the bond at par . With some bonds , the issuer has to pay a premium , the so - called call premium . This is mainly the case for high - yield bonds . These have very strict covenants , restricting the issuer in its operations . To be free from these covenants , the issuer can repay the bonds early , but only at a high cost .
  • \n", - "

    On November 7 , 2016 , debt held by the public was $14.3 trillion or about 76 % of the previous 12 months of GDP . Intragovernmental holdings stood at $5.4 trillion , giving a combined total gross national debt of $19.8 trillion or about 106 % of the previous 12 months of GDP ; $6.2 trillion or approximately 45 % of the debt held by the public was owned by foreign investors , the largest of which were Japan and China at about $1.09 trillion for Japan and $1.06 trillion for China as of December 2016 .

    \n", - "

    A currency transaction report ( CTR ) is a report that U.S. financial institutions are required to file with FinCEN for each deposit , withdrawal , exchange of currency , or other payment or transfer , by , through , or to the financial institution which involves a transaction in currency of more than $10,000 . Used in this context , currency means the coin and / or paper money of any country that is designated as legal tender by the country of issuance . Currency also includes U.S. silver certificates , U.S. notes , Federal Reserve notes , and official foreign bank notes .

    \n", - "

    Checks and balances is the principle that each of the Branches has the power to limit or check the other two and this creates a balance between the three separate powers of the state , this principle induces that the ambitions of one branch prevent that one of the other branches become supreme , and thus be eternally confronting each other and in that process leaving the people free from government abuses . Checks and Balances are designed to maintain the system of separation of powers keeping each branch in its place . This is based on the idea that it is not enough to separate the powers and guarantee their independence but to give the various branches the constitutional means to defend their own legitimate powers from the encroachments of the other branches . They guarantee that the powers of the State have the same weight ( co-equal ) , that is , to be balanced , so that they can limit each other , avoiding the abuse of state power . the origin of checks and balances , like separation of powers itself , is specifically credited to Montesquieu in the Enlightenment ( in The Spirit of the Laws , 1748 ) , under this influence was implemented in 1787 in the Constitution of the United States .

    \n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "Non controlling interest on balance sheet refers to the portion of a subsidiary corporation's stock that is not owned by the parent corporation. It represents ownership of less than 50% of the outstanding shares. It is shown as a separate line item in the equity section of the balance sheet.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\n", - "\n", - ">>>>>>>>>>>> Below are outputs of Case 2 <<<<<<<<<<<<\n", - "\n", - "\n", - "doc_ids: [['doc_1', 'doc_1097', 'doc_4221', 'doc_4972', 'doc_1352', 'doc_96', 'doc_988', 'doc_2370', 'doc_2414', 'doc_5038', 'doc_302', 'doc_1608', 'doc_980', 'doc_2112', 'doc_562', 'doc_4204', 'doc_3298', 'doc_2995', 'doc_3978', 'doc_1258', 'doc_2971', 'doc_2171', 'doc_1065', 'doc_17', 'doc_2683', 'doc_87', 'doc_1767', 'doc_158', 'doc_482', 'doc_3850']]\n", - "\u001b[32mAdding doc_id doc_1 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1097 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_4221 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_4972 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1352 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_96 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_988 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2370 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2414 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_5038 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_302 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1608 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_980 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2112 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_562 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_4204 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3298 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2995 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3978 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1258 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2971 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2171 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1065 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_17 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2683 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented chatbot. You answer user's questions based on your own knowledge and the\n", - "context provided by the user.\n", - "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", - "You must give as short an answer as possible.\n", - "\n", - "User's question is: how many episodes are in chicago fire season 4\n", - "\n", - "Context is:

    The fourth season of Chicago Fire , an American drama television series with executive producer Dick Wolf , and producers Derek Haas , Michael Brandt , and Matt Olmstead , was ordered on February 5 , 2015 , by NBC , and premiered on October 13 , 2015 and concluded on May 17 , 2016 . The season contained 23 episodes .

    \n", - "

    The fourth season began airing on October 10 , 2017 , and is set to run for 23 episodes on The CW until May 22 , 2018 .

    \n", - "

    The fourth season began airing on October 10 , 2017 , on The CW .

    \n", - "

    The fifth season of Chicago P.D. , an American police drama television series with executive producer Dick Wolf , and producers Derek Haas , Michael Brandt , and Rick Eid , premiered on September 27 , 2017 . This season featured its 100th episode .

    \n", - "

    This was the city of Chicago 's first professional sports championship since the Chicago Fire won MLS Cup ' 98 ( which came four months after the Chicago Bulls ' sixth NBA championship that year ) . The next major Chicago sports championship came in 2010 , when the NHL 's Chicago Blackhawks ended a 49 - year Stanley Cup title drought . With the Chicago Bears ' win in Super Bowl XX and the Chicago Cubs ' own World Series championship in 2016 , all Chicago sports teams have won at least one major championship since 1985 . Meanwhile , the Astros themselves made it back to the World Series in 2017 , but this time as an AL team , where they defeated the Los Angeles Dodgers in seven games , resulting in Houston 's first professional sports championship since the 2006 -- 07 Houston Dynamo won their back - to - back MLS Championships .

    \n", - "

    The season was ordered in May 2017 , and production began the following month . Ben McKenzie stars as Gordon , alongside Donal Logue , David Mazouz , Morena Baccarin , Sean Pertwee , Robin Lord Taylor , Erin Richards , Camren Bicondova , Cory Michael Smith , Jessica Lucas , Chris Chalk , Drew Powell , Crystal Reed and Alexander Siddig . The fourth season premiered on September 21 , 2017 , on Fox , while the second half premiered on March 1 , 2018 .

    \n", - "

    As of May 24 , 2017 , 58 episodes of The 100 have aired , concluding the fourth season . In March 2017 , The CW renewed the series for a fifth season , set to premiere on April 24 , 2018 .

    \n", - "

    The fifth book , River of Fire , is scheduled to be released on April 10 , 2018 .

    \n", - "

    On September 10 , 2013 , AMC officially cancelled the series after 38 episodes and three seasons . However , on November 15 , 2013 , Netflix ordered a fourth and final season of six episodes , that was released on Netflix on August 1 , 2014 .

    \n", - "

    The second season of Fargo , an American anthology black comedy -- crime drama television series created by Noah Hawley , premiered on October 12 , 2015 , on the basic cable network FX . Its principal cast consists of Kirsten Dunst , Patrick Wilson , Jesse Plemons , Jean Smart , and Ted Danson . The season had ten episodes , and its initial airing concluded on December 14 , 2015 . As an anthology , each Fargo season possesses its own self - contained narrative , following a disparate set of characters in various settings .

    \n", - "

    The Great Fire of London was a major conflagration that swept through the central parts of the English city of London from Sunday , 2 September to Wednesday , 5 September 1666 . The fire gutted the medieval City of London inside the old Roman city wall . It threatened but did not reach the aristocratic district of Westminster , Charles II 's Palace of Whitehall , and most of the suburban slums . It consumed 13,200 houses , 87 parish churches , St Paul 's Cathedral , and most of the buildings of the City authorities . It is estimated to have destroyed the homes of 70,000 of the City 's 80,000 inhabitants .

    \n", - "

    The first season consisted of eight one - hour - long episodes which were released worldwide on Netflix on July 15 , 2016 , in Ultra HD 4K . The second season , consisting of nine episodes , was released on October 27 , 2017 in HDR . A teaser for the second season , which also announced the release date , aired during Super Bowl LI .

    \n", - "

    `` Two Days Before the Day After Tomorrow '' is the eighth episode in the ninth season of the American animated television series South Park . The 133rd overall episode overall , it originally aired on Comedy Central in the United States on October 19 , 2005 . In the episode , Stan and Cartman accidentally destroy a dam , causing the town of Beaverton to be destroyed .

    \n", - "

    The fourth season consists of a double order of twenty episodes , split into two parts of ten episodes ; the second half premiered on November 30 , 2016 . The season follows the battles between Ragnar and Rollo in Francia , Bjorn 's raid into the Mediterranean , and the Viking invasion of England . It concluded in its entirety on February 1 , 2017 .

    \n", - "

    This is an episode list for Sabrina the Teenage Witch , an American sitcom that debuted on ABC in 1996 . From Season 5 , the program was aired on The WB . The series ran for seven seasons totaling 163 episodes . It originally premiered on September 27 , 1996 on ABC and ended on April 24 , 2003 on The WB .

    \n", - "

    Hart of Dixie was renewed by The CW for 10 episode season on May 8 , 2014 . The show 's fourth and final season premiered on November 15 , 2014 . The series was later cancelled on May 7 , 2015 .

    \n", - "

    The Burning Maze is the third book in the series . It is scheduled to be released on May 1 , 2018 .

    \n", - "
    My Name Is Earl ( season 4 )
    DVD cover
    Country of origin United States
    No. of episodes 27
    Release
    Original network NBC
    Original release September 25 , 2008 -- May 14 , 2009
    Season chronology
    ← Previous Season 3
    List of My Name Is Earl episodes
    \n", - "

    The eighteenth season of Law & Order : Special Victims Unit debuted on Wednesday , September 21 , 2016 , on NBC and finished on Wednesday , May 24 , 2017 , with a two - hour season finale .

    \n", - "

    The eighth and final season of the fantasy drama television series Game of Thrones was announced by HBO in July 2016 . Unlike the first six seasons that each had ten episodes and the seventh that had seven episodes , the eighth season will have only six episodes . Like the previous season , it will largely consist of original content not found currently in George R.R. Martin 's A Song of Ice and Fire series , and will instead adapt material Martin has revealed to showrunners about the upcoming novels in the series , The Winds of Winter and A Dream of Spring .

    \n", - "

    A total of 49 episodes of The Glades were produced and aired over four seasons .

    \n", - "

    Sneaky Pete is an American crime drama series created by David Shore and Bryan Cranston . The series follows Marius Josipović ( Giovanni Ribisi ) , a released convict who adopts the identity of his cell mate , Pete Murphy , in order to avoid his past life . The series also stars Marin Ireland , Shane McRae , Libe Barer , Michael Drayer , Peter Gerety , and Margo Martindale . The pilot debuted on August 7 , 2015 , and was followed by a full series order that September . Shore left the project in early 2016 and was replaced by Graham Yost , who served as executive producer and showrunner for the remaining nine episodes . The first season premiered in its entirety on January 13 , 2017 , exclusively on Amazon Video . On January 19 , 2017 , Amazon announced that Sneaky Pete had been renewed for a second season , which was released on March 9 , 2018 .

    \n", - "

    The eighth season of Blue Bloods , a police procedural drama series created by Robin Green and Mitchell Burgess , premiered on CBS on September 29 , 2017 . The season is set to contain 22 episodes .

    \n", - "

    The first five seasons of Prison Break have been released on DVD and Blu - ray in Regions 1 , 2 , and 4 . Each DVD boxed set includes all of the broadcast episodes from that season , the associated special episode , commentary from cast and crew , and profiles of various parts of Prison Break , such as Fox River State Penitentiary or the tattoo . Prison Break is also available online , including iTunes , Amazon Video , and Netflix . After the premiere of the second season of Prison Break , Fox began online streaming of the prior week 's episode , though it originally restricted viewing to the United States .

    \n", - "

    In June 2017 , Remini was upped to a series regular starting with Season 2 ; shortly after , it was announced that Erinn Hayes would not be returning for the show 's second season . Sources cited in a Variety article confirmed that Remini would be returning as Detective Vanessa Cellucci , the character she portrayed in the first - season finale , and that Hayes ' dismissal was for creative reasons and `` not a reflection '' of the actress ' performance . In August 2017 , it was reported Hayes ' character will be killed off before season two begins and the season will take place 7 -- 10 months after season one ended , in order to make room for Remini .

    \n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "There are 23 episodes in Chicago Fire season 4.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\n", - "\n", - ">>>>>>>>>>>> Below are outputs of Case 3 <<<<<<<<<<<<\n", - "\n", - "\n", - "doc_ids: [['doc_47', 'doc_45', 'doc_2570', 'doc_2851', 'doc_4033', 'doc_5320', 'doc_3849', 'doc_4172', 'doc_3202', 'doc_2282', 'doc_1896', 'doc_949', 'doc_103', 'doc_1552', 'doc_2791', 'doc_392', 'doc_1175', 'doc_5315', 'doc_832', 'doc_3185', 'doc_2532', 'doc_3409', 'doc_824', 'doc_4075', 'doc_1201', 'doc_4116', 'doc_1448', 'doc_2545', 'doc_2251', 'doc_2485']]\n", - "\u001b[32mAdding doc_id doc_47 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_45 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2570 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2851 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_4033 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_5320 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3849 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_4172 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3202 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2282 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1896 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_949 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_103 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1552 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2791 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_392 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1175 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_5315 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_832 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3185 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2532 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented chatbot. You answer user's questions based on your own knowledge and the\n", - "context provided by the user.\n", - "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", - "You must give as short an answer as possible.\n", - "\n", - "User's question is: what are bulls used for on a farm\n", - "\n", - "Context is:

    Many cattle ranches and stations run bulls with cows , and most dairy or beef farms traditionally had at least one , if not several , bulls for purposes of herd maintenance . However , the problems associated with handling a bull ( particularly where cows must be removed from its presence to be worked ) has prompted many dairy farmers to restrict themselves to artificial insemination ( AI ) of the cows . Semen is removed from the bulls and stored in canisters of liquid nitrogen , where it is kept until it can be sold , at which time it can be very profitable , in fact , many ranchers keep bulls specifically for this purpose . AI is also used to increase the quality of a herd , or to introduce an outcross of bloodlines . Some ranchers prefer to use AI to allow them to breed to several different bulls in a season or to breed their best stock to a higher quality bull than they could afford to purchase outright . AI may also be used in conjunction with embryo transfer to allow cattle producers to add new breeding to their herds .

    \n", - "

    Other than the few bulls needed for breeding , the vast majority of male cattle are slaughtered for meat before the age of three years , except where they are needed ( castrated ) as work oxen for haulage . Most of these beef animals are castrated as calves to reduce aggressive behavior and prevent unwanted mating , although some are reared as uncastrated bull beef . A bull is typically ready for slaughter one or two months sooner than a castrated male or a female , and produces proportionately more , leaner muscle .

    \n", - "

    Pastoral farming is the major land use but there are increases in land area devoted to horticulture .

    \n", - "

    Animal fibers are natural fibers that consist largely of particular proteins . Instances are silk , hair / fur ( including wool ) and feathers . The animal fibers used most commonly both in the manufacturing world as well as by the hand spinners are wool from domestic sheep and silk . Also very popular are alpaca fiber and mohair from Angora goats . Unusual fibers such as Angora wool from rabbits and Chiengora from dogs also exist , but are rarely used for mass production .

    \n", - "

    In 2012 , there were 3.2 million farmers , ranchers and other agricultural managers and an estimated 757,900 agricultural workers were legally employed in the US . Animal breeders accounted for 11,500 of those workers with the rest categorized as miscellaneous agricultural workers . The median pay was $9.12 per hour or $18,970 per year . In 2009 , about 519,000 people under age 20 worked on farms owned by their family . In addition to the youth who lived on family farms , an additional 230,000 youth were employed in agriculture . In 2004 , women made up approximately 24 % of farmers ; that year , there were 580,000 women employed in agriculture , forestry , and fishing .

    \n", - "

    The recipe can vary widely . The defining ingredients are minced meat ( commonly beef when named cottage pie or lamb when named shepherd 's pie ) , typically cooked in a gravy with onions and sometimes other vegetables , such as peas , celery or carrots , and topped with mashed potato . The pie is sometimes also topped with grated cheese .

    \n", - "

    The history of the domesticated sheep goes back to between 11000 and 9000 BC , and the domestication of the wild mouflon in ancient Mesopotamia . Sheep are among the first animals to have been domesticated by humans , and there is evidence of sheep farming in Iranian statuary dating to that time period . These sheep were primarily raised for meat , milk , and skins . Woolly sheep began to be developed around 6000 BC in Iran , and cultures such as the Persians relied on sheep 's wool for trading . They were then imported to Africa and Europe via trading .

    \n", - "

    Although large - scale use of wheels did not occur in the Americas prior to European contact , numerous small wheeled artifacts , identified as children 's toys , have been found in Mexican archeological sites , some dating to about 1500 BC . It is thought that the primary obstacle to large - scale development of the wheel in the Americas was the absence of domesticated large animals which could be used to pull wheeled carriages . The closest relative of cattle present in Americas in pre-Columbian times , the American Bison , is difficult to domesticate and was never domesticated by Native Americans ; several horse species existed until about 12,000 years ago , but ultimately became extinct . The only large animal that was domesticated in the Western hemisphere , the llama , did not spread far beyond the Andes by the time of the arrival of Columbus .

    \n", - "

    The Call of the Wild is a short adventure novel by Jack London published in 1903 and set in Yukon , Canada during the 1890s Klondike Gold Rush , when strong sled dogs were in high demand . The central character of the novel is a dog named Buck . The story opens at a ranch in Santa Clara Valley , California , when Buck is stolen from his home and sold into service as a sled dog in Alaska . He becomes progressively feral in the harsh environment , where he is forced to fight to survive and dominate other dogs . By the end , he sheds the veneer of civilization , and relies on primordial instinct and learned experience to emerge as a leader in the wild .

    \n", - "

    The Three Little Pigs was included in The Nursery Rhymes of England ( London and New York , c. 1886 ) , by James Halliwell - Phillipps . The story in its arguably best - known form appeared in English Fairy Tales by Joseph Jacobs , first published in 1890 and crediting Halliwell as his source . The story begins with the title characters being sent out into the world by their mother , to `` seek out their fortune '' . The first little pig builds a house of straw , but a wolf blows it down and devours him . The second little pig builds a house of sticks , which the wolf also blows down , and the second little pig is also devoured . Each exchange between wolf and pig features ringing proverbial phrases , namely :

    \n", - "

    `` How now brown cow '' ( / ˈhaʊ ˈnaʊ ˈbraʊn ˈkaʊ / ) is a phrase used in elocution teaching to demonstrate rounded vowel sounds . Each `` ow '' sound in the phrase represents the diphthong / aʊ / . Although orthographies for each of the four words in this utterance is represented by the English spelling `` ow '' , the articulation required to create this same diphthong represented by the International Phonetic Association 's phonetic alphabet as / aʊ / is also represented by the spelling `` ou '' . Some examples of these homophonic / aʊ / 's are the English words `` house '' , `` blouse '' , `` noun '' , and `` cloud '' . The use of the phrase `` how now brown cow '' in teaching elocution can be dated back to at least 1926 . Although not in use today , the phrase `` how now '' is a greeting , short for `` how say you now '' , and can be found in archaic literature , such as the plays of William Shakespeare .

    \n", - "

    Brisket is a cut of meat from the breast or lower chest of beef or veal . The beef brisket is one of the nine beef primal cuts , though the precise definition of the cut differs internationally . The brisket muscles include the superficial and deep pectorals . As cattle do not have collar bones , these muscles support about 60 % of the body weight of standing / moving cattle . This requires a significant amount of connective tissue , so the resulting meat must be cooked correctly to tenderize the connective tissue .

    \n", - "

    The music to `` Man Gave Names to All the Animals '' is reggae - inspired . The lyrics were inspired by the biblical Book of Genesis , verses 2 : 19 -- 20 in which Adam named the animals and birds . The lyrics have an appeal to children , rhyming the name of the animal with one of its characteristics . So after describing an animal 's `` muddy trail '' and `` curly tail , '' Dylan sings that `` he was n't too small and he was n't too big '' and so that animal was named a pig . Similarly , the cow got its name because Adam `` saw milk comin ' out but he did n't know how '' and the bear got its name because it has a `` great big furry back and furry hair . ''

    \n", - "

    As early as 1671 railed roads were in use in Durham to ease the conveyance of coal ; the first of these was the Tanfield Wagonway . Many of these tramroads or wagon ways were built in the 17th and 18th centuries . They used simply straight and parallel rails of timber on which carts with simple flanged iron wheels were drawn by horses , enabling several wagons to be moved simultaneously .

    \n", - "

    Unicorns are not found in Greek mythology , but rather in the accounts of natural history , for Greek writers of natural history were convinced of the reality of unicorns , which they believed lived in India , a distant and fabulous realm for them . The earliest description is from Ctesias , who in his book Indika ( `` On India '' ) described them as wild asses , fleet of foot , having a horn a cubit and a half ( 700 mm , 28 inches ) in length , and colored white , red and black . Aristotle must be following Ctesias when he mentions two one - horned animals , the oryx ( a kind of antelope ) and the so - called `` Indian ass '' . Strabo says that in the Caucasus there were one - horned horses with stag - like heads . Pliny the Elder mentions the oryx and an Indian ox ( perhaps a rhinoceros ) as one - horned beasts , as well as `` a very fierce animal called the monoceros which has the head of the stag , the feet of the elephant , and the tail of the boar , while the rest of the body is like that of the horse ; it makes a deep lowing noise , and has a single black horn , which projects from the middle of its forehead , two cubits ( 900 mm , 35 inches ) in length . '' In On the Nature of Animals ( Περὶ Ζῴων Ἰδιότητος , De natura animalium ) , Aelian , quoting Ctesias , adds that India produces also a one - horned horse ( iii. 41 ; iv. 52 ) , and says ( xvi. 20 ) that the monoceros ( Greek : μονόκερως ) was sometimes called cartazonos ( Greek : καρτάζωνος ) , which may be a form of the Arabic karkadann , meaning `` rhinoceros '' .

    \n", - "

    The First Battle of Bull Run ( the name used by Union forces ) , also known as the First Battle of Manassas ( the name used by Confederate forces ) , was fought on July 21 , 1861 in Prince William County , Virginia , just north of the city of Manassas and about 25 miles west - southwest of Washington , D.C. It was the first major battle of the American Civil War . The Union 's forces were slow in positioning themselves , allowing Confederate reinforcements time to arrive by rail . Each side had about 18,000 poorly trained and poorly led troops in their first battle . It was a Confederate victory , followed by a disorganized retreat of the Union forces .

    \n", - "

    Hops production is concentrated in moist temperate climates , with much of the world 's production occurring near the 48th parallel north . Hop plants prefer the same soils as potatoes and the leading potato - growing states in the United States are also major hops - producing areas ; however , not all potato - growing areas can produce good hops naturally : soils in the Maritime Provinces of Canada , for example , lack the boron that hops prefer . Historically , hops were not grown in Ireland , but were imported from England . In 1752 more than 500 tons of English hops were imported through Dublin alone .

    \n", - "

    Shepherd 's pie or cottage pie is a meat pie with a crust of mashed potato .

    \n", - "

    Castles served a range of purposes , the most important of which were military , administrative , and domestic . As well as defensive structures , castles were also offensive tools which could be used as a base of operations in enemy territory . Castles were established by Norman invaders of England for both defensive purposes and to pacify the country 's inhabitants . As William the Conqueror advanced through England , he fortified key positions to secure the land he had taken . Between 1066 and 1087 , he established 36 castles such as Warwick Castle , which he used to guard against rebellion in the English Midlands .

    \n", - "

    The Rocky and Bullwinkle Show remained in syndicated reruns and was still available for local television stations through The Program Exchange as late as 2016 ; WBBZ - TV , for instance , aired the show in a strip to counterprogram 10 PM newscasts in the Buffalo , New York market during the summer 2013 season . The underlying rights are now owned by Universal Pictures , which holds the library of predecessor companies DreamWorks Animation and Classic Media , and who in turn with copyright holder Ward Productions forms the joint venture Bullwinkle Studios , which manages the Rocky and Bullwinkle properties ; Universal 's purchase of Classic Media coincided with The Program Exchange 's shutdown .

    \n", - "

    When Yellowstone National Park was created in 1872 , gray wolf ( Canis lupus ) populations were already in decline in Montana , Wyoming and Idaho . The creation of the national park did not provide protection for wolves or other predators , and government predator control programs in the first decades of the 1900s essentially helped eliminate the gray wolf from Yellowstone . The last wolves were killed in Yellowstone in 1926 . After that time , sporadic reports of wolves still occurred , but scientists confirmed that sustainable wolf populations had been extirpated and were absent from Yellowstone during the mid-1900s .

    \n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "Bulls are used for breeding and often kept for their semen to sell for AI purposes. Some male cattle are also kept as work oxen for haulage. The vast majority, however, are slaughtered for meat before the age of three years.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\n", - "\n", - ">>>>>>>>>>>> Below are outputs of Case 4 <<<<<<<<<<<<\n", - "\n", - "\n", - "doc_ids: [['doc_3031', 'doc_819', 'doc_4521', 'doc_3980', 'doc_3423', 'doc_5275', 'doc_745', 'doc_753', 'doc_3562', 'doc_4139', 'doc_3678', 'doc_4931', 'doc_2347', 'doc_1115', 'doc_2806', 'doc_5204', 'doc_2707', 'doc_3653', 'doc_1122', 'doc_2398', 'doc_309', 'doc_3891', 'doc_2087', 'doc_330', 'doc_4844', 'doc_2155', 'doc_2674', 'doc_5357', 'doc_1581', 'doc_9']]\n", - "\u001b[32mAdding doc_id doc_3031 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_819 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_4521 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3980 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3423 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_5275 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_745 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_753 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3562 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented chatbot. You answer user's questions based on your own knowledge and the\n", - "context provided by the user.\n", - "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", - "You must give as short an answer as possible.\n", - "\n", - "User's question is: has been honoured with the wisden leading cricketer in the world award for 2016\n", - "\n", - "Context is:

    The first recipient was Uttam Kumar from Bengali cinema , who was honoured at the 15th National Film Awards in 1968 for his performances in Anthony Firingee and Chiriyakhana . As of 2017 , Amitabh Bachchan is the most honoured actor , with four awards . Two actors -- Kamal Haasan and Mammootty -- have been honoured three times , while six actors -- Sanjeev Kumar , Mithun Chakraborty , Om Puri , Naseeruddin Shah , Mohanlal , and Ajay Devgn -- have won the award two times . Two actors have achieved the honour for performing in two languages -- Mithun Chakraborty ( Hindi and Bengali ) and Mammootty ( Malayalam and English ) . The most recent recipient is Riddhi Sen , who was honoured at the 65th National Film Awards for his performance in the Bengali film Nagarkirtan .

    \n", - "

    There was controversy over the National Film Award for Best Actor , which the committee awarded to Akshay Kumar for his performance in Rustom , snubbing Aamir Khan 's performance for Dangal . Committee member Priyadarshan , who has worked with Kumar on several films , gave the following explanation for awarding Kumar instead of Khan :

    \n", - "

    The 2017 ICC Champions Trophy was the eighth ICC Champions Trophy , a cricket tournament for the eight top - ranked One Day International ( ODI ) teams in the world . It was held in England and Wales from 1 June to 18 June 2017 . Pakistan won the competition for the first time with a 180 - run victory over India in the final at The Oval . The margin of victory was the largest by any team in the final of an ICC ODI tournament in terms of runs .

    \n", - " List of One Day International cricket double centuries
    No . Runs Batsman S / R For Against ODI Venue Date
    200 * Tendulkar , Sachin Sachin Tendulkar 136.05 India South Africa 2962 Captain Roop Singh Stadium , Gwalior , India 24 February 2010
    219 Sehwag , Virender Virender Sehwag 146.98 India West Indies 3223 Holkar Stadium , Indore , India 8 December 2011
    209 Sharma , Rohit Rohit Sharma 132.28 India Australia 3428 M. Chinnaswamy Stadium , Bangalore , India 2 November 2013
    264 Sharma , Rohit Rohit Sharma 152.60 India Sri Lanka 3544 Eden Gardens , India 13 November 2014
    5 215 Gayle , Chris Chris Gayle 146.30 West Indies Zimbabwe 3612 Manuka Oval , Canberra , Australia 24 February 2015
    6 237 * Guptill , Martin Martin Guptill 145.40 New Zealand West Indies 3643 Wellington Regional Stadium , Wellington , New Zealand 22 March 2015
    7 208 * Sharma , Rohit Rohit Sharma 135.95 India Sri Lanka 3941 Punjab Cricket Association IS Bindra Stadium , Mohali , India 13 December 2017
    \n", - "

    G. Sankara Kurup , ( 3 June 1901 , Nayathode , Kingdom of Cochin ( now in Ernakulam district , Kerala , India ) -- 2 February 1978 , Vappalassery , Angamaly , Ernakulam district , Kerala ) , better known as Mahakavi G ( The Great Poet G ) , was the first winner of the Jnanpith Award , India 's highest literary award . He won the prize in 1965 for his collection of poems in Malayalam Odakkuzhal ( The Bamboo Flute , 1950 ) . With part of the prize money he established the literary award Odakkuzhal in 1968 . He was also the recipient of the Soviet Land Nehru Award , in 1967 , and the Padma Bhushan in 1968 . His poetry collection Viswadarshanam won the Kerala Sahitya Akademi Award in 1961 and Kendra Sahitya Akademi Award in 1963 .

    \n", - "

    The 2019 Cricket World Cup ( officially ICC Cricket World Cup 2019 ) is the 12th edition of the Cricket World Cup , scheduled to be hosted by England and Wales , from 30 May to 14 July 2019 .

    \n", - " 2018 Under - 19 Cricket World Cup
    Dates 13 January -- 3 February 2018
    Administrator ( s ) International Cricket Council
    Cricket format 50 overs
    Tournament format ( s ) Round - robin and knockout
    Host ( s ) New Zealand
    Champions India ( 4th title )
    Runners - up Australia
    Participants 16
    Matches played 48
    Player of the series Shubman Gill
    Most runs Alick Athanaze ( 418 )
    Most wickets Anukul Roy ( 14 ) Qais Ahmad ( 14 ) Faisal Jamkhandi ( 14 )
    Official website Official website
    ← 2016 2020 →
    \n", - "

    The 2018 ICC Under - 19 Cricket World Cup was an international limited - overs cricket tournament that was held in New Zealand from 13 January to 3 February 2018 . It was the twelfth edition of the Under - 19 Cricket World Cup , and the third to be held in New Zealand ( after the 2002 and 2010 events ) . New Zealand was the first country to host the event three times . The opening ceremony took place on 7 January 2018 . The West Indies were the defending champions . However , they failed to defend their title , after losing their first two group fixtures .

    \n", - "

    Scoring over 10,000 runs across a playing career in any format of cricket is considered a significant achievement . In the year 2001 , Sachin Tendulkar became the first player to score 10,000 runs in ODIs , while playing a match during the bi-lateral series against Australia at home . In the chase for achieving top scores , West Indies ' Desmond Haynes retired as the most prolific run - scorer in One Day Internationals ( ODIs ) , with a total of 8,648 runs in 1994 . The record stood for four years until it was broken by India 's Mohammed Azharuddin . Azharuddin remained the top - scorer in the format until his compatriot Sachin Tendulkar passed him in October 2000 . As of August 2016 , eleven players -- from six teams that are Full members of the International Cricket Council -- have scored more than 10,000 runs in ODIs . Four of these are from Sri Lanka and three from India . The rest are one player each from Pakistan , Australia , West Indies , and South Africa . Bangladesh , England , New Zealand , and Zimbabwe are yet to have a player reach the 10,000 - run mark in this format .

    \n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "I'm sorry, I couldn't find any information about who has been honoured with the Wisden Leading Cricketer in the World award for 2016. UPDATE CONTEXT.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32mUpdating context and resetting conversation.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_4139 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3678 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_4931 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2347 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1115 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2806 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_5204 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2707 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3653 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented chatbot. You answer user's questions based on your own knowledge and the\n", - "context provided by the user.\n", - "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", - "You must give as short an answer as possible.\n", - "\n", - "User's question is: has been honoured with the wisden leading cricketer in the world award for 2016\n", - "\n", - "Context is: List of the Indian Oscar nominee ( s ) / recipient ( s ) , also showing the year , film , category , and result
    Year Nominee ( s ) / recipient ( s ) Film Category / Honorary Award Result / received Ref .
    1958 ( 30th ) Mehboob Khan Mother India Best Foreign Language Film Nominated
    1961 ( 33rd ) Ismail Merchant The Creation of Woman Best Short Subject ( Live Action ) Nominated
    1979 ( 51st ) Vidhu Vinod Chopra and K.K. Kapil An Encounter with Faces Best Documentary ( Short Subject ) Nominated
    ( 55th ) Bhanu Athaiya Gandhi Best Costume Design Won
    Ravi Shankar Best Original Score Nominated
    ( 59th ) Ismail Merchant A Room with a View Best Picture Nominated
    ( 61st ) Mira Nair Salaam Bombay ! Best Foreign Language Film Nominated
    1992 ( 64th ) Satyajit Ray Pather Pachali Honorary Award Received
    ( 65th ) Ismail Merchant Howards End Best Picture Nominated
    ( 66th ) Ismail Merchant The Remains of the Day Best Picture Nominated
    2002 ( 74th ) Ashutosh Gowarikar Lagaan Best Foreign Language Film Nominated
    2005 ( 77th ) Ashvin Kumar Little Terrorist Best Short Subject ( Live Action ) Nominated
    2007 ( 79th ) Deepa Mehta Water Best Foreign Language Film Nominated
    2009 ( 81st ) Resul Pookutty Slumdog Millionaire Best Sound Mixing Won
    A.R. Rahman Best Original Score Won
    A.R. Rahman and Gulzar Best Original Song Won
    2011 ( 83rd ) A.R. Rahman 127 Hours Best Original Score Nominated
    A.R. Rahman Best Original Song Nominated
    2013 ( 85th ) Bombay Jayashri Life of Pi Best Original Song Nominated
    2016 Rahul Thakkar n / a Sci - Tech Award Received
    2016 Cottalango Leon n / a Sci - Tech Award Received
    2018 Vikas Sathaye n / a Sci - Tech Award Received
    \n", - "

    The 2017 Nobel Peace Prize was awarded to the International Campaign to Abolish Nuclear Weapons ( ICAN ) `` for its work to draw attention to the catastrophic humanitarian consequences of any use of nuclear weapons and for its ground - breaking efforts to achieve a treaty - based prohibition on such weapons , '' according to the Norwegian Nobel Committee announcement on October 6 , 2017 . The award announcement acknowledged the fact that `` the world 's nine nuclear - armed powers and their allies '' neither signed nor supported the treaty - based prohibition known as the Treaty on the Prohibition of Nuclear Weapons or nuclear ban treaty , yet in an interview Committee Chair Berit Reiss - Andersen told reporters that the award was intended to give `` encouragement to all players in the field '' to disarm . The award was hailed by civil society as well as governmental and intergovernmental representatives who support the nuclear ban treaty , but drew criticism from those opposed . At the Nobel Peace Prize award ceremony held in Oslo City Hall on December 10 , 2017 , Setsuko Thurlow , an 85 - year - old survivor of the 1945 atomic bombing of Hiroshima , and ICAN Executive Director Beatrice Fihn jointly received a medal and diploma of the award on behalf of ICAN and delivered the Nobel lecture .

    \n", - "

    Career records for batting average are usually subject to a minimum qualification of 20 innings played or completed , in order to exclude batsmen who have not played enough games for their skill to be reliably assessed . Under this qualification , the highest Test batting average belongs to Australia 's Sir Donald Bradman , with 99.94 . Given that a career batting average over 50 is exceptional , and that only five other players have averages over 60 , this is an outstanding statistic . The fact that Bradman 's average is so far above that of any other cricketer has led several statisticians to argue that , statistically at least , he was the greatest athlete in any sport .

    \n", - "
    Indian cricket team in South Africa in 2017 -- 18
    South Africa India
    Dates 5 January 2018 -- 24 February 2018
    Captains Faf du Plessis ( Tests and ODIs ) JP Duminy ( T20Is ) Virat Kohli
    Test series
    Result South Africa won the 3 - match series 2 -- 1
    Most runs AB de Villiers ( 211 ) Virat Kohli ( 286 )
    Most wickets Vernon Philander ( 15 ) Kagiso Rabada ( 15 ) Mohammed Shami ( 15 )
    Player of the series Vernon Philander ( SA )
    One Day International series
    Results India won the 6 - match series 5 -- 1
    Most runs Hashim Amla ( 154 ) Virat Kohli ( 558 )
    Most wickets Lungi Ngidi ( 8 ) Kuldeep Yadav ( 17 )
    Player of the series Virat Kohli ( Ind )
    Twenty20 International series
    Results India won the 3 - match series 2 -- 1
    Most runs JP Duminy ( 122 ) Shikhar Dhawan ( 143 )
    Most wickets Junior Dala ( 7 ) Bhuvneshwar Kumar ( 7 )
    Player of the series Bhuvneshwar Kumar ( Ind )
    \n", - "

    Brian Lara took the least number of innings ( 195 ) to reach the 10,000 run mark , later equalled by Sachin Tendulkar and Kumar Sangakkara , while Australia 's Steve Waugh took 244 innings to achieve the feat . Alastair Cook is the fastest in terms of time span , taking 10 years and 87 days . The time taken by Shivnarine Chanderpaul ( 18 years and 37 days ) is the slowest among all . As of May 2017 , Tendulkar leads the list with 15,921 runs followed by Ricky Ponting of Australia with 13,378 .

    \n", - "
    50 + Player Matches Innings
    119 Sachin Tendulkar 200 329
    103 Jacques Kallis 166 280
    103 Ricky Ponting 168 287
    99 Rahul Dravid 164 286
    96 Shivnarine Chanderpaul 164 280

    Last updated : 15 June 2016

    \n", - "

    Chandan Shetty emerged as the winner of this season on 28. January. 2018 with Karthik being the runner up . Other finalists Niveditha , Diwakar , Shruti were eliminated

    \n", - "

    Arthur Chung ( January 10 , 1918 -- June 23 , 2008 ) was the first President of Guyana from 1970 to 1980 . During his time as President of Guyana , the office was that of a ceremonial head of state , with real power in the hands of Prime Minister Forbes Burnham . He was honoured with Guyana 's highest national honour , the Order of Excellence ( O.E. ) .

    \n", - "
    Incumbent Achal Kumar Jyoti since 6 July 2017
    No Name ( birth -- death ) Portrait Elected ( % votes ) Took office Left office Term ( in years ) Notes President ( s ) Candidate of
    Sarvepalli Radhakrishnan ( 1888 -- 1975 ) 1952 ( Unopposed )

    1957 ( Unopposed )

    13 May 1952 12 May 1962 10 Radhakrishnan was a prominent scholar . Besides being awarded the Bharat Ratna he also held the position of vice-chancellor in the Banaras Hindu University and the Andhra college . He served as the Vice-President for two terms . Rajendra Prasad Independent
    Zakir Husain ( 1897 -- 1969 ) -- 1962 ( 97.59 ) 13 May 1962 12 May 1967 5 Sarvepalli Radhakrishnan Independent
    Varahagiri Venkata Giri ( 1894 -- 1980 ) -- 1967 ( 71.45 ) 13 May 1967 3 May 1969 Zakir Husain Independent
    Gopal Swarup Pathak ( 1896 -- 1982 ) -- 1969 -- 31 August 1969 30 August 1974 5 Varahagiri Venkata Giri ( 1969 -- 1974 )

    Fakhruddin Ali Ahmed ( 1974 )

    Independent
    5 Basappa Danappa Jatti ( 1912 -- 2002 ) -- ( 78.70 ) 31 August 1974 30 August 1979 5 Fakhruddin Ali Ahmed ( 1974 -- 1977 ) Neelam Sanjiva Reddy ( 1977 -- 1979 ) Indian National Congress
    6 Mohammad Hidayatullah ( 1905 -- 1992 ) -- 1979 ( Unopposed ) 31 August 1979 30 August 1984 5 Neelam Sanjiva Reddy ( 1979 -- 1982 ) Giani Zail Singh ( 1982 -- 1984 ) Independent
    7 Ramaswamy Venkataraman ( 1910 -- 2009 ) 1984 ( 71.05 ) 31 August 1984 24 July 1987 Giani Zail Singh Indian National Congress
    8 Shankar Dayal Sharma ( 1918 -- 1999 ) ( Unopposed ) 3 September 1987 24 July 1992 5 Ramaswamy Venkataraman Indian National Congress
    9 Kocheril Raman Narayanan ( 1920 -- 2005 ) 1992 ( 99.86 ) 21 August 1992 24 July 1997 5 Shankar Dayal Sharma Indian National Congress
    10 Krishan Kant ( 1927 -- 2002 ) -- 1997 ( 61.76 ) 21 August 1997 27 July 2002 Kocheril Raman Narayanan ( 1997 -- 2002 ) A.P.J. Abdul Kalam ( 2002 ) Janata Dal
    11 Bhairon Singh Shekhawat ( 1923 -- 2010 ) 2002 ( 59.82 ) 19 August 2002 21 July 2007 5 A.P.J. Abdul Kalam Bharatiya Janata Party
    12 Mohammad Hamid Ansari ( 1937 -- ) 2007 ( 60.51 ) 2012 ( 67.31 ) 11 August 2007 11 August 2017 10 Pratibha Patil ( 2007 -- 2012 ) Pranab Mukherjee ( 2012 -- 2017 ) Ram Nath Kovind ( 2017 ) Indian National Congress
    13 Muppavarapu Venkaiah Naidu ( 1949 -- ) 2017 ( 67.89 ) 11 August 2017 Incumbent -- Ram Nath Kovind Bharatiya Janata Party
    \n", - "
    Governor of Maharashtra
    Incumbent Chennamaneni Vidyasagar Rao since 30 August 2014
    Style His Excellency
    Residence Main : Raj Bhavan ( Mumbai ) Additional : Raj Bhavan ( Nagpur ) ; Raj Bhavan ( Pune ) & Raj Bhavan ( Mahabaleshwar )
    Appointer President of India
    Term length Five Years
    Inaugural holder John Colville , PC , GCIE
    Formation 15 August 1947 ; 70 years ago ( 1947 - 08 - 15 )
    \n", - "

    Every player who has won this award and has been eligible for the Naismith Memorial Basketball Hall of Fame has been inducted . Kareem Abdul - Jabbar won the award a record six times . Both Bill Russell and Michael Jordan won the award five times , while Wilt Chamberlain and LeBron James won the award four times . Russell and James are the only players to have won the award four times in five seasons . Moses Malone , Larry Bird and Magic Johnson each won the award three times , while Bob Pettit , Karl Malone , Tim Duncan , Steve Nash and Stephen Curry have each won it twice . Only two rookies have won the award : Wilt Chamberlain in the 1959 -- 60 season and Wes Unseld in the 1968 -- 69 season . Hakeem Olajuwon of Nigeria , Tim Duncan of the U.S. Virgin Islands , Steve Nash of Canada and Dirk Nowitzki of Germany are the only MVP winners considered `` international players '' by the NBA .

    \n", - "

    The Jawaharlal Nehru Centre for Advanced Scientific Research ( JNCASR ) is a multidisciplinary research institute located at Jakkur , Bangalore , India . It was established by the Department of Science and Technology of the Government of India , to mark the birth centenary of Pandit Jawaharlal Nehru .

    \n", - "

    Ajay Tyagi was appointed chairman on 10 January 2017 replacing UK Sinha . And took charge of chairman office on 1 March 2017 . The Board comprises

    \n", - "
    Year Player Country
    2003 Ponting , Ricky Ricky Ponting Australia
    Warne , Shane Shane Warne Australia
    2005 Flintoff , Andrew Andrew Flintoff England
    2006 Muralitharan , Muttiah Muttiah Muralitharan Sri Lanka
    2007 Kallis , Jacques Jacques Kallis South Africa
    2008 Sehwag , Virender Virender Sehwag India
    2009 Sehwag , Virender Virender Sehwag India
    Tendulkar , Sachin Sachin Tendulkar India
    2011 Sangakkara , Kumar Kumar Sangakkara Sri Lanka
    2012 Clarke , Michael Michael Clarke Australia
    2013 Steyn , Dale Dale Steyn South Africa
    2014 Sangakkara , Kumar Kumar Sangakkara Sri Lanka
    2015 Williamson , Kane Kane Williamson New Zealand
    2016 Kohli , Virat Virat Kohli India
    2017 Kohli , Virat Virat Kohli India
    \n", - "

    Mankombu Sambasivan Swaminathan ( born 7 August 1925 ) is an Indian geneticist and international administrator , renowned for his leading role in India 's Green Revolution a program under which high - yield varieties of wheat and rice seedlings were planted in the fields of poor farmers . Swaminathan is known as `` Indian Father of Green Revolution '' for his leadership and success in introducing and further developing high - yielding varieties of wheat in India . He is the founder and chairman of the MS Swaminathan Research Foundation . His stated vision is to rid the world of hunger and poverty . Swaminathan is an advocate of moving India to sustainable development , especially using environmentally sustainable agriculture , sustainable food security and the preservation of biodiversity , which he calls an `` evergreen revolution . ''

    \n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "I'm sorry, the provided context doesn't contain information about any cricketer being honored with the Wisden Leading Cricketer in the World award for 2016. UPDATE CONTEXT if you have any other query.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\n", - "\n", - ">>>>>>>>>>>> Below are outputs of Case 5 <<<<<<<<<<<<\n", - "\n", - "\n", - "doc_ids: [['doc_20', 'doc_2943', 'doc_2059', 'doc_3293', 'doc_4056', 'doc_1914', 'doc_2749', 'doc_1796', 'doc_3468', 'doc_1793', 'doc_876', 'doc_2577', 'doc_27', 'doc_366', 'doc_321', 'doc_3103', 'doc_715', 'doc_3534', 'doc_142', 'doc_5337', 'doc_2426', 'doc_5346', 'doc_3021', 'doc_1596', 'doc_316', 'doc_1103', 'doc_1602', 'doc_1677', 'doc_1670', 'doc_2853']]\n", - "\u001b[32mAdding doc_id doc_20 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2943 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2059 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3293 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_4056 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1914 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2749 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1796 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_3468 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_1793 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_876 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_2577 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_27 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented chatbot. You answer user's questions based on your own knowledge and the\n", - "context provided by the user.\n", - "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", - "You must give as short an answer as possible.\n", - "\n", - "User's question is: who carried the usa flag in opening ceremony\n", - "\n", - "Context is:

    On January 17 , 1899 , under orders from President William McKinley , Commander Edward D. Taussig of USS Bennington landed on Wake and formally took possession of the island for the United States . After a 21 - gun salute , the flag was raised and a brass plate was affixed to the flagstaff with the following inscription :

    \n", - "
  • 1960 Flag with 50 stars ( Hawaii )
  • \n", - "

    The flag of the United States of America , often referred to as the American flag , is the national flag of the United States . It consists of thirteen equal horizontal stripes of red ( top and bottom ) alternating with white , with a blue rectangle in the canton ( referred to specifically as the `` union '' ) bearing fifty small , white , five - pointed stars arranged in nine offset horizontal rows , where rows of six stars ( top and bottom ) alternate with rows of five stars . The 50 stars on the flag represent the 50 states of the United States of America , and the 13 stripes represent the thirteen British colonies that declared independence from the Kingdom of Great Britain , and became the first states in the U.S. Nicknames for the flag include The Stars and Stripes , Old Glory , and The Star - Spangled Banner .

    \n", - "

    The Pledge of Allegiance of the United States is an expression of allegiance to the Flag of the United States and the republic of the United States of America . It was originally composed by Captain George Thatcher Balch , a Union Army Officer during the Civil War and later a teacher of patriotism in New York City schools . The form of the pledge used today was largely devised by Francis Bellamy in 1892 , and formally adopted by Congress as the pledge in 1942 . The official name of The Pledge of Allegiance was adopted in 1945 . The most recent alteration of its wording came on Flag Day in 1954 , when the words `` under God '' were added .

    \n", - "

    In modern times , the U.S. military plays ( or sounds ) `` Reveille '' in the morning , generally near sunrise , though its exact time varies from base to base . On U.S. Army posts and Air Force bases , `` Reveille '' is played by itself or followed by the bugle call `` To the Colors '' at which time the national flag is raised and all U.S. military personnel outdoors are required to come to attention and present a salute in uniform , either to the flag or in the direction of the music if the flag is not visible . While in formation , soldiers are brought to the position of parade rest while `` Reveille '' plays then called to attention and present arms as the national flag is raised . On board U.S. Navy , Marine Corps , and Coast Guard facilities , the flag is generally raised at 0800 ( 8 am ) while `` The Star Spangled Banner '' or the bugle call `` To the Colors '' is played . On some U.S. military bases , `` Reveille '' is accompanied by a cannon shot .

    \n", - "

    When the National Anthem was first recognized by law in 1932 , there was no prescription as to behavior during its playing . On June 22 , 1942 , the law was revised indicating that those in uniform should salute during its playing , while others should simply stand at attention , men removing their hats . ( The same code also required that women should place their hands over their hearts when the flag is displayed during the playing of the Anthem , but not if the flag was not present . ) On December 23 , 1942 the law was again revised instructing men and women to stand at attention and face in the direction of the music when it was played . That revision also directed men and women to place their hands over their hearts only if the flag was displayed . Those in uniform were required to salute . On July 7 , 1976 , the law was simplified . Men and women were instructed to stand with their hands over their hearts , men removing their hats , irrespective of whether or not the flag was displayed and those in uniform saluting . On August 12 , 1998 , the law was rewritten keeping the same instructions , but differentiating between `` those in uniform '' and `` members of the Armed Forces and veterans '' who were both instructed to salute during the playing whether or not the flag was displayed . Because of the changes in law over the years and confusion between instructions for the Pledge of Allegence versus the National Anthem , throughout most of the 20th century many people simply stood at attention or with their hands folded in front of them during the playing of the Anthem , and when reciting the Pledge they would hold their hand ( or hat ) over their heart . After 9 / 11 , the custom of placing the hand over the heart during the playing of the Anthem became nearly universal .

    \n", - "

    A flag designed by John McConnell in 1969 for the first Earth Day is a dark blue field charged with The Blue Marble , a famous NASA photo of the Earth as seen from outer space . The first edition of McConnell 's flag used screen - printing and used different colors : ocean and land were blue and the clouds were white . McConnell presented his flag to the United Nations as a symbol for consideration .

    \n", - "

    The torch - bearing arm was displayed at the Centennial Exposition in Philadelphia in 1876 , and in Madison Square Park in Manhattan from 1876 to 1882 . Fundraising proved difficult , especially for the Americans , and by 1885 work on the pedestal was threatened by lack of funds . Publisher Joseph Pulitzer , of the New York World , started a drive for donations to finish the project and attracted more than 120,000 contributors , most of whom gave less than a dollar . The statue was built in France , shipped overseas in crates , and assembled on the completed pedestal on what was then called Bedloe 's Island . The statue 's completion was marked by New York 's first ticker - tape parade and a dedication ceremony presided over by President Grover Cleveland .

    \n", - "

    The horizontal stripes on the flag represent the nine original departments of Uruguay , based on the U.S flag , where the stripes represent the original 13 colonies . The first flag designed in 1828 had 9 light blue stripes ; this number was reduced to 4 in 1830 due to visibility problems from distance . The Sun of May represents the May Revolution of 1810 ; according to the historian Diego Abad de Santillán , the Sun of May is a figurative sun that represents Inti , the sun god of the Inca religion . It also appears in the Flag of Argentina and the Coat of Arms of Bolivia .

    \n", - "

    The anthem has been recorded and performed in many different languages , usually as a result of the hosting of either form of the Games in various countries . The IOC does n't require that the anthem be performed in either English or Greek . But in the 2008 Olympic opening and closing ceremonies in Beijing , China , Greek was sung instead of the host country 's official language , Mandarin . Also in the 2016 Olympic opening ceremonies in Rio de Janeiro , Brazil , English was also sung instead of host country 's official language , Portuguese .

    \n", - "

    The United States Oath of Allegiance , officially referred to as the `` Oath of Allegiance , '' 8 C.F.R. Part 337 ( 2008 ) , is an allegiance oath that must be taken by all immigrants who wish to become United States citizens .

    \n", - "

    During the first half of the 19th century , seven stars were added to the flag to represent the seven signatories to the Venezuelan declaration of independence , being the provinces of Caracas , Cumaná , Barcelona , Barinas , Margarita , Mérida , and Trujillo .

    \n", - "

    With the annexation of Hawaii in 1898 and the seizure of Guam and the Philippines during the Spanish -- American War that same year , the United States began to consider unclaimed and uninhabited Wake Island , located approximately halfway between Honolulu and Manila , as a good location for a telegraph cable station and coaling station for refueling warships of the rapidly expanding United States Navy and passing merchant and passenger steamships . On July 4 , 1898 , United States Army Brigadier General Francis V. Greene of the 2nd Brigade , Philippine Expeditionary Force , of the Eighth Army Corps , stopped at Wake Island and raised the American flag while en route to the Philippines on the steamship liner SS China .

    \n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "I don't have the answer with the provided context. UPDATE CONTEXT.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32mUpdating context and resetting conversation.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_366 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented chatbot. You answer user's questions based on your own knowledge and the\n", - "context provided by the user.\n", - "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", - "You must give as short an answer as possible.\n", - "\n", - "User's question is: who carried the usa flag in opening ceremony\n", - "\n", - "Context is: \n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "Erin Hamlin carried the USA flag in the opening ceremony.\n", - "\n", - "--------------------------------------------------------------------------------\n" - ] - } - ], - "source": [ - "for i in range(len(questions)):\n", - " print(f\"\\n\\n>>>>>>>>>>>> Below are outputs of Case {i+1} <<<<<<<<<<<<\\n\\n\")\n", - "\n", - " # reset the assistant. Always reset the assistant before starting a new conversation.\n", - " assistant.reset()\n", - "\n", - " qa_problem = questions[i]\n", - " ragproxyagent.initiate_chat(assistant, message=ragproxyagent.message_generator, problem=qa_problem, n_results=30)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this example, questions were directly selected from the dataset. RetrieveChat was able to answer the questions correctly in the first attempt as the retrieved context contained the necessary information in the first two cases. However, in the last three cases, the context with the highest similarity to the question embedding did not contain the required information to answer the question. As a result, the LLM model responded with `UPDATE CONTEXT`. With the unique and innovative ability to update context in RetrieveChat, the agent automatically updated the context and sent it to the LLM model again. After several rounds of this process, the agent was able to generate the correct answer to the questions." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Example 6\n", - "\n", - "[Back to top](#table-of-contents)\n", - "\n", - "Use RetrieveChat to answer multi-hop questions for [2WikiMultihopQA](https://github.com/Alab-NII/2wikimultihop) dataset with customized prompt and few-shot learning.\n", - "\n", - "First, we will create a new document collection which includes all the contextual corpus. Then, we will choose some questions and utilize RetrieveChat to answer them. For this particular example, we will be using the `gpt-3.5-turbo` model, and we will demonstrate RetrieveChat's feature of automatically updating context in case the documents retrieved do not contain sufficient information. Moreover, we'll demonstrate how to use customized prompt and few-shot learning to address tasks that are not pre-defined in RetrieveChat." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "PROMPT_MULTIHOP = \"\"\"You're a retrieve augmented chatbot. You answer user's questions based on your own knowledge and the context provided by the user. You must think step-by-step.\n", - "First, please learn the following examples of context and question pairs and their corresponding answers.\n", - "\n", - "Context:\n", - "Kurram Garhi: Kurram Garhi is a small village located near the city of Bannu, which is the part of Khyber Pakhtunkhwa province of Pakistan. Its population is approximately 35000.\n", - "Trojkrsti: Trojkrsti is a village in Municipality of Prilep, Republic of Macedonia.\n", - "Q: Are both Kurram Garhi and Trojkrsti located in the same country?\n", - "A: Kurram Garhi is located in the country of Pakistan. Trojkrsti is located in the country of Republic of Macedonia. Thus, they are not in the same country. So the answer is: no.\n", - "\n", - "\n", - "Context:\n", - "Early Side of Later: Early Side of Later is the third studio album by English singer- songwriter Matt Goss. It was released on 21 June 2004 by Concept Music and reached No. 78 on the UK Albums Chart.\n", - "What's Inside: What's Inside is the fourteenth studio album by British singer- songwriter Joan Armatrading.\n", - "Q: Which album was released earlier, What'S Inside or Cassandra'S Dream (Album)?\n", - "A: What's Inside was released in the year 1995. Cassandra's Dream (album) was released in the year 2008. Thus, of the two, the album to release earlier is What's Inside. So the answer is: What's Inside.\n", - "\n", - "\n", - "Context:\n", - "Maria Alexandrovna (Marie of Hesse): Maria Alexandrovna , born Princess Marie of Hesse and by Rhine (8 August 1824 – 3 June 1880) was Empress of Russia as the first wife of Emperor Alexander II.\n", - "Grand Duke Alexei Alexandrovich of Russia: Grand Duke Alexei Alexandrovich of Russia,(Russian: Алексей Александрович; 14 January 1850 (2 January O.S.) in St. Petersburg – 14 November 1908 in Paris) was the fifth child and the fourth son of Alexander II of Russia and his first wife Maria Alexandrovna (Marie of Hesse).\n", - "Q: What is the cause of death of Grand Duke Alexei Alexandrovich Of Russia's mother?\n", - "A: The mother of Grand Duke Alexei Alexandrovich of Russia is Maria Alexandrovna. Maria Alexandrovna died from tuberculosis. So the answer is: tuberculosis.\n", - "\n", - "\n", - "Context:\n", - "Laughter in Hell: Laughter in Hell is a 1933 American Pre-Code drama film directed by Edward L. Cahn and starring Pat O'Brien. The film's title was typical of the sensationalistic titles of many Pre-Code films.\n", - "Edward L. Cahn: Edward L. Cahn (February 12, 1899 – August 25, 1963) was an American film director.\n", - "Q: When did the director of film Laughter In Hell die?\n", - "A: The film Laughter In Hell was directed by Edward L. Cahn. Edward L. Cahn died on August 25, 1963. So the answer is: August 25, 1963.\n", - "\n", - "Second, please complete the answer by thinking step-by-step.\n", - "\n", - "Context:\n", - "{input_context}\n", - "Q: {input_question}\n", - "A:\n", - "\"\"\"" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [], - "source": [ - "# create the RetrieveUserProxyAgent instance named \"ragproxyagent\"\n", - "corpus_file = \"https://huggingface.co/datasets/thinkall/2WikiMultihopQA/resolve/main/corpus.txt\"\n", - "\n", - "# Create a new collection for NaturalQuestions dataset\n", - "ragproxyagent = RetrieveUserProxyAgent(\n", - " name=\"ragproxyagent\",\n", - " human_input_mode=\"NEVER\",\n", - " max_consecutive_auto_reply=3,\n", - " retrieve_config={\n", - " \"task\": \"qa\",\n", - " \"docs_path\": corpus_file,\n", - " \"chunk_token_size\": 2000,\n", - " \"model\": config_list[0][\"model\"],\n", - " \"client\": chromadb.PersistentClient(path=\"/tmp/chromadb\"),\n", - " \"collection_name\": \"2wikimultihopqa\",\n", - " \"chunk_mode\": \"one_line\",\n", - " \"embedding_model\": \"all-MiniLM-L6-v2\",\n", - " \"customized_prompt\": PROMPT_MULTIHOP,\n", - " \"customized_answer_prefix\": \"the answer is\",\n", - " },\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['Which film came out first, Blind Shaft or The Mask Of Fu Manchu?', 'Are North Marion High School (Oregon) and Seoul High School both located in the same country?']\n", - "[['The Mask Of Fu Manchu'], ['no']]\n" - ] - } - ], - "source": [ - "# queries_file = \"https://huggingface.co/datasets/thinkall/2WikiMultihopQA/resolve/main/queries.jsonl\"\n", - "queries = \"\"\"{\"_id\": \"61a46987092f11ebbdaeac1f6bf848b6\", \"text\": \"Which film came out first, Blind Shaft or The Mask Of Fu Manchu?\", \"metadata\": {\"answer\": [\"The Mask Of Fu Manchu\"]}}\n", - "{\"_id\": \"a7b9672009c311ebbdb0ac1f6bf848b6\", \"text\": \"Are North Marion High School (Oregon) and Seoul High School both located in the same country?\", \"metadata\": {\"answer\": [\"no\"]}}\n", - "\"\"\"\n", - "queries = [json.loads(line) for line in queries.split(\"\\n\") if line]\n", - "questions = [q[\"text\"] for q in queries]\n", - "answers = [q[\"metadata\"][\"answer\"] for q in queries]\n", - "print(questions)\n", - "print(answers)" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - ">>>>>>>>>>>> Below are outputs of Case 1 <<<<<<<<<<<<\n", - "\n", - "\n", - "Trying to create collection.\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\tClyde Thompson: Clyde Thompson( 1910 – July 1, 1979) was an American prisoner turned chaplain. He is ...\n", - "max_tokens is too small to fit a single line of text. Breaking this line:\n", - "\tAustralian Historical Monographs: The Australian Historical Monographs are a series of Historical st ...\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "doc_ids: [['doc_12', 'doc_11', 'doc_16', 'doc_19', 'doc_13116', 'doc_14', 'doc_13', 'doc_18', 'doc_977', 'doc_10']]\n", - "\u001b[32mAdding doc_id doc_12 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_11 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_16 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_19 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_13116 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_14 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_13 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_18 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_977 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_10 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented chatbot. You answer user's questions based on your own knowledge and the context provided by the user. You must think step-by-step.\n", - "First, please learn the following examples of context and question pairs and their corresponding answers.\n", - "\n", - "Context:\n", - "Kurram Garhi: Kurram Garhi is a small village located near the city of Bannu, which is the part of Khyber Pakhtunkhwa province of Pakistan. Its population is approximately 35000.\n", - "Trojkrsti: Trojkrsti is a village in Municipality of Prilep, Republic of Macedonia.\n", - "Q: Are both Kurram Garhi and Trojkrsti located in the same country?\n", - "A: Kurram Garhi is located in the country of Pakistan. Trojkrsti is located in the country of Republic of Macedonia. Thus, they are not in the same country. So the answer is: no.\n", - "\n", - "\n", - "Context:\n", - "Early Side of Later: Early Side of Later is the third studio album by English singer- songwriter Matt Goss. It was released on 21 June 2004 by Concept Music and reached No. 78 on the UK Albums Chart.\n", - "What's Inside: What's Inside is the fourteenth studio album by British singer- songwriter Joan Armatrading.\n", - "Q: Which album was released earlier, What'S Inside or Cassandra'S Dream (Album)?\n", - "A: What's Inside was released in the year 1995. Cassandra's Dream (album) was released in the year 2008. Thus, of the two, the album to release earlier is What's Inside. So the answer is: What's Inside.\n", - "\n", - "\n", - "Context:\n", - "Maria Alexandrovna (Marie of Hesse): Maria Alexandrovna , born Princess Marie of Hesse and by Rhine (8 August 1824 – 3 June 1880) was Empress of Russia as the first wife of Emperor Alexander II.\n", - "Grand Duke Alexei Alexandrovich of Russia: Grand Duke Alexei Alexandrovich of Russia,(Russian: Алексей Александрович; 14 January 1850 (2 January O.S.) in St. Petersburg – 14 November 1908 in Paris) was the fifth child and the fourth son of Alexander II of Russia and his first wife Maria Alexandrovna (Marie of Hesse).\n", - "Q: What is the cause of death of Grand Duke Alexei Alexandrovich Of Russia's mother?\n", - "A: The mother of Grand Duke Alexei Alexandrovich of Russia is Maria Alexandrovna. Maria Alexandrovna died from tuberculosis. So the answer is: tuberculosis.\n", - "\n", - "\n", - "Context:\n", - "Laughter in Hell: Laughter in Hell is a 1933 American Pre-Code drama film directed by Edward L. Cahn and starring Pat O'Brien. The film's title was typical of the sensationalistic titles of many Pre-Code films.\n", - "Edward L. Cahn: Edward L. Cahn (February 12, 1899 – August 25, 1963) was an American film director.\n", - "Q: When did the director of film Laughter In Hell die?\n", - "A: The film Laughter In Hell was directed by Edward L. Cahn. Edward L. Cahn died on August 25, 1963. So the answer is: August 25, 1963.\n", - "\n", - "Second, please complete the answer by thinking step-by-step.\n", - "\n", - "Context:\n", - "The Mask of Fu Manchu: The Mask of Fu Manchu is a 1932 pre-Code adventure film directed by Charles Brabin. It was written by Irene Kuhn, Edgar Allan Woolf and John Willard based on the 1932 novel of the same name by Sax Rohmer. Starring Boris Karloff as Fu Manchu, and featuring Myrna Loy as his depraved daughter, the movie revolves around Fu Manchu's quest for the golden sword and mask of Genghis Khan. Lewis Stone plays his nemesis. Dr. Petrie is absent from this film.\n", - "The Mysterious Dr. Fu Manchu: The Mysterious Dr. Fu Manchu is a 1929 American pre-Code drama film directed by Rowland V. Lee and starring Warner Oland as Dr. Fu Manchu. It was the first Fu Manchu film of the talkie era. Since this was during the transition period to sound, a silent version was also released in the United States.\n", - "The Face of Fu Manchu: The Face of Fu Manchu is a 1965 thriller film directed by Don Sharp and based on the characters created by Sax Rohmer. It stars Christopher Lee as the eponymous villain, a Chinese criminal mastermind, and Nigel Green as his pursuing rival Nayland Smith, a Scotland Yard detective. The film was a British- West German co-production, and was the first in a five- part series starring Lee and produced by Harry Alan Towers for Constantin Film, the second of which was\" The Brides of Fu Manchu\" released the next year, with the final entry being\" The Castle of Fu Manchu\" in 1969. It was shot in Technicolor and Techniscope, on- location in County Dublin, Ireland.\n", - "The Return of Dr. Fu Manchu: The Return of Dr. Fu Manchu is a 1930 American pre-Code film directed by Rowland V. Lee. It is the second of three films starring Warner Oland as the fiendish Fu Manchu, who returns from apparent death in the previous film,\" The Mysterious Dr. Fu Manchu\"( 1929), to seek revenge on those he holds responsible for the death of his wife and child.\n", - "The Vengeance of Fu Manchu: The Vengeance of Fu Manchu is a 1967 British film directed by Jeremy Summers and starring Christopher Lee, Horst Frank, Douglas Wilmer and Tsai Chin. It was the third British/ West German Constantin Film co-production of the Dr. Fu Manchu series and the first to be filmed in Hong Kong. It was generally released in the U.K. through Warner- Pathé( as a support feature to the Lindsay Shonteff film\" The Million Eyes of Sumuru\") on 3 December 1967.\n", - "The Brides of Fu Manchu: The Brides of Fu Manchu is a 1966 British/ West German Constantin Film co-production adventure crime film based on the fictional Chinese villain Dr. Fu Manchu, created by Sax Rohmer. It was the second film in a series, and was preceded by\" The Face of Fu ManchuThe Vengeance of Fu Manchu\" followed in 1967,\" The Blood of Fu Manchu\" in 1968, and\" The Castle of Fu Manchu\" in 1969. It was produced by Harry Alan Towers for Hallam Productions. Like the first film, it was directed by Don Sharp, and starred Christopher Lee as Fu Manchu. Nigel Green was replaced by Douglas Wilmer as Scotland Yard detective Nayland Smith. The action takes place mainly in London, where much of the location filming took place.\n", - "The Castle of Fu Manchu: The Castle of Fu Manchu( also known as The Torture Chamber of Dr. Fu Manchu and also known by its German title Die Folterkammer des Dr. Fu Man Chu) is a 1969 film and the fifth and final Dr. Fu Manchu film with Christopher Lee portraying the title character.\n", - "The Blood of Fu Manchu: The Blood of Fu Manchu, also known as Fu Manchu and the Kiss of Death, Kiss of Death, Kiss and Kill( U.S. title) and Against All Odds( original U.S. video title), is a 1968 British adventure crime film directed by Jesús Franco, based on the fictional Asian villain Dr. Fu Manchu created by Sax Rohmer. It was the fourth film in a series, and was preceded by\" The Vengeance of Fu Manchu The Castle of Fu Manchu\" followed in 1969. It was produced by Harry Alan Towers for Udastex Films. It starred Christopher Lee as Dr. Fu Manchu, Richard Greene as Scotland Yard detective Nayland Smith, and Howard Marion- Crawford as Dr. Petrie. The movie was filmed in Spain and Brazil. Shirley Eaton appears in a scene that she claimed she was never paid for; apparently, the director Jesús Franco had inserted some stock footage of her from one of her films(\" The Girl from Rio\"( 1968)) into the film without telling her. She only found out years later that she had been in a Fu Manchu film.\n", - "Don Sharp: Donald Herman Sharp( 19 April 192114 December 2011) was an Australian- born British film director. His best known films were made for Hammer in the 1960s, and included\" The Kiss of the Vampire\"( 1962) and\" Rasputin, the Mad Monk\"( 1966). In 1965 he directed\" The Face of Fu Manchu\", based on the character created by Sax Rohmer, and starring Christopher Lee. Sharp also directed the sequel\" The Brides of Fu Manchu\"( 1966). In the 1980s he was also responsible for several hugely popular miniseries adapted from the novels of Barbara Taylor Bradford.\n", - "Blind Shaft: Blind Shaft is a 2003 film about a pair of brutal con artists operating in the illegal coal mines of present- day northern China. The film was written and directed by Li Yang( 李杨), and is based on Chinese writer Liu Qingbang's short novel\" Shen MuSacred Wood\").\n", - "\n", - "Q: Which film came out first, Blind Shaft or The Mask Of Fu Manchu?\n", - "A:\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32mAdding doc_id doc_11 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_16 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_19 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_13116 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_14 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_13 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_18 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_977 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_10 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented chatbot. You answer user's questions based on your own knowledge and the context provided by the user. You must think step-by-step.\n", - "First, please learn the following examples of context and question pairs and their corresponding answers.\n", - "\n", - "Context:\n", - "Kurram Garhi: Kurram Garhi is a small village located near the city of Bannu, which is the part of Khyber Pakhtunkhwa province of Pakistan. Its population is approximately 35000.\n", - "Trojkrsti: Trojkrsti is a village in Municipality of Prilep, Republic of Macedonia.\n", - "Q: Are both Kurram Garhi and Trojkrsti located in the same country?\n", - "A: Kurram Garhi is located in the country of Pakistan. Trojkrsti is located in the country of Republic of Macedonia. Thus, they are not in the same country. So the answer is: no.\n", - "\n", - "\n", - "Context:\n", - "Early Side of Later: Early Side of Later is the third studio album by English singer- songwriter Matt Goss. It was released on 21 June 2004 by Concept Music and reached No. 78 on the UK Albums Chart.\n", - "What's Inside: What's Inside is the fourteenth studio album by British singer- songwriter Joan Armatrading.\n", - "Q: Which album was released earlier, What'S Inside or Cassandra'S Dream (Album)?\n", - "A: What's Inside was released in the year 1995. Cassandra's Dream (album) was released in the year 2008. Thus, of the two, the album to release earlier is What's Inside. So the answer is: What's Inside.\n", - "\n", - "\n", - "Context:\n", - "Maria Alexandrovna (Marie of Hesse): Maria Alexandrovna , born Princess Marie of Hesse and by Rhine (8 August 1824 – 3 June 1880) was Empress of Russia as the first wife of Emperor Alexander II.\n", - "Grand Duke Alexei Alexandrovich of Russia: Grand Duke Alexei Alexandrovich of Russia,(Russian: Алексей Александрович; 14 January 1850 (2 January O.S.) in St. Petersburg – 14 November 1908 in Paris) was the fifth child and the fourth son of Alexander II of Russia and his first wife Maria Alexandrovna (Marie of Hesse).\n", - "Q: What is the cause of death of Grand Duke Alexei Alexandrovich Of Russia's mother?\n", - "A: The mother of Grand Duke Alexei Alexandrovich of Russia is Maria Alexandrovna. Maria Alexandrovna died from tuberculosis. So the answer is: tuberculosis.\n", - "\n", - "\n", - "Context:\n", - "Laughter in Hell: Laughter in Hell is a 1933 American Pre-Code drama film directed by Edward L. Cahn and starring Pat O'Brien. The film's title was typical of the sensationalistic titles of many Pre-Code films.\n", - "Edward L. Cahn: Edward L. Cahn (February 12, 1899 – August 25, 1963) was an American film director.\n", - "Q: When did the director of film Laughter In Hell die?\n", - "A: The film Laughter In Hell was directed by Edward L. Cahn. Edward L. Cahn died on August 25, 1963. So the answer is: August 25, 1963.\n", - "\n", - "Second, please complete the answer by thinking step-by-step.\n", - "\n", - "Context:\n", - "The Mask of Fu Manchu: The Mask of Fu Manchu is a 1932 pre-Code adventure film directed by Charles Brabin. It was written by Irene Kuhn, Edgar Allan Woolf and John Willard based on the 1932 novel of the same name by Sax Rohmer. Starring Boris Karloff as Fu Manchu, and featuring Myrna Loy as his depraved daughter, the movie revolves around Fu Manchu's quest for the golden sword and mask of Genghis Khan. Lewis Stone plays his nemesis. Dr. Petrie is absent from this film.\n", - "The Mysterious Dr. Fu Manchu: The Mysterious Dr. Fu Manchu is a 1929 American pre-Code drama film directed by Rowland V. Lee and starring Warner Oland as Dr. Fu Manchu. It was the first Fu Manchu film of the talkie era. Since this was during the transition period to sound, a silent version was also released in the United States.\n", - "The Face of Fu Manchu: The Face of Fu Manchu is a 1965 thriller film directed by Don Sharp and based on the characters created by Sax Rohmer. It stars Christopher Lee as the eponymous villain, a Chinese criminal mastermind, and Nigel Green as his pursuing rival Nayland Smith, a Scotland Yard detective. The film was a British- West German co-production, and was the first in a five- part series starring Lee and produced by Harry Alan Towers for Constantin Film, the second of which was\" The Brides of Fu Manchu\" released the next year, with the final entry being\" The Castle of Fu Manchu\" in 1969. It was shot in Technicolor and Techniscope, on- location in County Dublin, Ireland.\n", - "The Return of Dr. Fu Manchu: The Return of Dr. Fu Manchu is a 1930 American pre-Code film directed by Rowland V. Lee. It is the second of three films starring Warner Oland as the fiendish Fu Manchu, who returns from apparent death in the previous film,\" The Mysterious Dr. Fu Manchu\"( 1929), to seek revenge on those he holds responsible for the death of his wife and child.\n", - "The Vengeance of Fu Manchu: The Vengeance of Fu Manchu is a 1967 British film directed by Jeremy Summers and starring Christopher Lee, Horst Frank, Douglas Wilmer and Tsai Chin. It was the third British/ West German Constantin Film co-production of the Dr. Fu Manchu series and the first to be filmed in Hong Kong. It was generally released in the U.K. through Warner- Pathé( as a support feature to the Lindsay Shonteff film\" The Million Eyes of Sumuru\") on 3 December 1967.\n", - "The Brides of Fu Manchu: The Brides of Fu Manchu is a 1966 British/ West German Constantin Film co-production adventure crime film based on the fictional Chinese villain Dr. Fu Manchu, created by Sax Rohmer. It was the second film in a series, and was preceded by\" The Face of Fu ManchuThe Vengeance of Fu Manchu\" followed in 1967,\" The Blood of Fu Manchu\" in 1968, and\" The Castle of Fu Manchu\" in 1969. It was produced by Harry Alan Towers for Hallam Productions. Like the first film, it was directed by Don Sharp, and starred Christopher Lee as Fu Manchu. Nigel Green was replaced by Douglas Wilmer as Scotland Yard detective Nayland Smith. The action takes place mainly in London, where much of the location filming took place.\n", - "The Castle of Fu Manchu: The Castle of Fu Manchu( also known as The Torture Chamber of Dr. Fu Manchu and also known by its German title Die Folterkammer des Dr. Fu Man Chu) is a 1969 film and the fifth and final Dr. Fu Manchu film with Christopher Lee portraying the title character.\n", - "The Blood of Fu Manchu: The Blood of Fu Manchu, also known as Fu Manchu and the Kiss of Death, Kiss of Death, Kiss and Kill( U.S. title) and Against All Odds( original U.S. video title), is a 1968 British adventure crime film directed by Jesús Franco, based on the fictional Asian villain Dr. Fu Manchu created by Sax Rohmer. It was the fourth film in a series, and was preceded by\" The Vengeance of Fu Manchu The Castle of Fu Manchu\" followed in 1969. It was produced by Harry Alan Towers for Udastex Films. It starred Christopher Lee as Dr. Fu Manchu, Richard Greene as Scotland Yard detective Nayland Smith, and Howard Marion- Crawford as Dr. Petrie. The movie was filmed in Spain and Brazil. Shirley Eaton appears in a scene that she claimed she was never paid for; apparently, the director Jesús Franco had inserted some stock footage of her from one of her films(\" The Girl from Rio\"( 1968)) into the film without telling her. She only found out years later that she had been in a Fu Manchu film.\n", - "Don Sharp: Donald Herman Sharp( 19 April 192114 December 2011) was an Australian- born British film director. His best known films were made for Hammer in the 1960s, and included\" The Kiss of the Vampire\"( 1962) and\" Rasputin, the Mad Monk\"( 1966). In 1965 he directed\" The Face of Fu Manchu\", based on the character created by Sax Rohmer, and starring Christopher Lee. Sharp also directed the sequel\" The Brides of Fu Manchu\"( 1966). In the 1980s he was also responsible for several hugely popular miniseries adapted from the novels of Barbara Taylor Bradford.\n", - "Blind Shaft: Blind Shaft is a 2003 film about a pair of brutal con artists operating in the illegal coal mines of present- day northern China. The film was written and directed by Li Yang( 李杨), and is based on Chinese writer Liu Qingbang's short novel\" Shen MuSacred Wood\").\n", - "\n", - "Q: Which film came out first, Blind Shaft or The Mask Of Fu Manchu?\n", - "A:\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "Blind Shaft is a 2003 film while The Mask of Fu Manchu is a 1932 pre-Code adventure film. Thus, The Mask of Fu Manchu came out earlier than Blind Shaft. So the answer is: The Mask of Fu Manchu.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\n", - "\n", - ">>>>>>>>>>>> Below are outputs of Case 2 <<<<<<<<<<<<\n", - "\n", - "\n", - "doc_ids: [['doc_74', 'doc_76', 'doc_68', 'doc_42890', 'doc_75', 'doc_19596', 'doc_45135', 'doc_995', 'doc_7274', 'doc_23187']]\n", - "\u001b[32mAdding doc_id doc_74 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_76 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_68 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_42890 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_75 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_19596 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_45135 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_995 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_7274 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_23187 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented chatbot. You answer user's questions based on your own knowledge and the context provided by the user. You must think step-by-step.\n", - "First, please learn the following examples of context and question pairs and their corresponding answers.\n", - "\n", - "Context:\n", - "Kurram Garhi: Kurram Garhi is a small village located near the city of Bannu, which is the part of Khyber Pakhtunkhwa province of Pakistan. Its population is approximately 35000.\n", - "Trojkrsti: Trojkrsti is a village in Municipality of Prilep, Republic of Macedonia.\n", - "Q: Are both Kurram Garhi and Trojkrsti located in the same country?\n", - "A: Kurram Garhi is located in the country of Pakistan. Trojkrsti is located in the country of Republic of Macedonia. Thus, they are not in the same country. So the answer is: no.\n", - "\n", - "\n", - "Context:\n", - "Early Side of Later: Early Side of Later is the third studio album by English singer- songwriter Matt Goss. It was released on 21 June 2004 by Concept Music and reached No. 78 on the UK Albums Chart.\n", - "What's Inside: What's Inside is the fourteenth studio album by British singer- songwriter Joan Armatrading.\n", - "Q: Which album was released earlier, What'S Inside or Cassandra'S Dream (Album)?\n", - "A: What's Inside was released in the year 1995. Cassandra's Dream (album) was released in the year 2008. Thus, of the two, the album to release earlier is What's Inside. So the answer is: What's Inside.\n", - "\n", - "\n", - "Context:\n", - "Maria Alexandrovna (Marie of Hesse): Maria Alexandrovna , born Princess Marie of Hesse and by Rhine (8 August 1824 – 3 June 1880) was Empress of Russia as the first wife of Emperor Alexander II.\n", - "Grand Duke Alexei Alexandrovich of Russia: Grand Duke Alexei Alexandrovich of Russia,(Russian: Алексей Александрович; 14 January 1850 (2 January O.S.) in St. Petersburg – 14 November 1908 in Paris) was the fifth child and the fourth son of Alexander II of Russia and his first wife Maria Alexandrovna (Marie of Hesse).\n", - "Q: What is the cause of death of Grand Duke Alexei Alexandrovich Of Russia's mother?\n", - "A: The mother of Grand Duke Alexei Alexandrovich of Russia is Maria Alexandrovna. Maria Alexandrovna died from tuberculosis. So the answer is: tuberculosis.\n", - "\n", - "\n", - "Context:\n", - "Laughter in Hell: Laughter in Hell is a 1933 American Pre-Code drama film directed by Edward L. Cahn and starring Pat O'Brien. The film's title was typical of the sensationalistic titles of many Pre-Code films.\n", - "Edward L. Cahn: Edward L. Cahn (February 12, 1899 – August 25, 1963) was an American film director.\n", - "Q: When did the director of film Laughter In Hell die?\n", - "A: The film Laughter In Hell was directed by Edward L. Cahn. Edward L. Cahn died on August 25, 1963. So the answer is: August 25, 1963.\n", - "\n", - "Second, please complete the answer by thinking step-by-step.\n", - "\n", - "Context:\n", - "Seoul High School: Seoul High School( Hangul: 서울고등학교) is a public high school located in the heart of Seoul, South Korea.\n", - "North Marion High School (Oregon): North Marion High School is a public high school in Aurora, Oregon, United States. The school is part of the North Marion School District with all four schools being located on the same campus. The school draws students from the cities of Aurora, Hubbard, and Donald as well as the communities of Broadacres and Butteville.\n", - "Marion High School (Kansas): Marion High School is a public high school in Marion, Kansas, USA. It is one of three schools operated by Marion USD 408, and is the sole high school in the district.\n", - "Northwest High School: Northwest High School or North West High School may refer to:\n", - "Marion High School (Indiana): Marion High School is a high school in Marion, Indiana with more than 1,000 students.\n", - "Macon County High School: Macon County High School is located in Montezuma, Georgia, United States, which is a part of Macon County. Enrollment as of the 2017- 2018 school year is 491.\n", - "Canyon High School (Ogden, Utah): Canyon High School was a high school in Ogden, Utah.\n", - "Northside High School: Northside High School or North Side High School or Northside Christian School or similar can refer to:\n", - "Springs Boys' High School: Springs Boys' High School is a high school in Springs, Gauteng, South Africa.\n", - "International School of Koje: International School of Koje( ISK) is a privately funded international school located in Geoje, South Korea.\n", - "\n", - "Q: Are North Marion High School (Oregon) and Seoul High School both located in the same country?\n", - "A:\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "No, North Marion High School (Oregon) is located in the United States, specifically in the state of Oregon, while Seoul High School is located in South Korea. So they are not in the same country.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32mUpdating context and resetting conversation.\u001b[0m\n", - "doc_ids: [['doc_76', 'doc_68', 'doc_74', 'doc_75', 'doc_19596', 'doc_42890', 'doc_24819', 'doc_69', 'doc_995', 'doc_7274']]\n", - "\u001b[32mAdding doc_id doc_24819 to context.\u001b[0m\n", - "\u001b[32mAdding doc_id doc_69 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented chatbot. You answer user's questions based on your own knowledge and the context provided by the user. You must think step-by-step.\n", - "First, please learn the following examples of context and question pairs and their corresponding answers.\n", - "\n", - "Context:\n", - "Kurram Garhi: Kurram Garhi is a small village located near the city of Bannu, which is the part of Khyber Pakhtunkhwa province of Pakistan. Its population is approximately 35000.\n", - "Trojkrsti: Trojkrsti is a village in Municipality of Prilep, Republic of Macedonia.\n", - "Q: Are both Kurram Garhi and Trojkrsti located in the same country?\n", - "A: Kurram Garhi is located in the country of Pakistan. Trojkrsti is located in the country of Republic of Macedonia. Thus, they are not in the same country. So the answer is: no.\n", - "\n", - "\n", - "Context:\n", - "Early Side of Later: Early Side of Later is the third studio album by English singer- songwriter Matt Goss. It was released on 21 June 2004 by Concept Music and reached No. 78 on the UK Albums Chart.\n", - "What's Inside: What's Inside is the fourteenth studio album by British singer- songwriter Joan Armatrading.\n", - "Q: Which album was released earlier, What'S Inside or Cassandra'S Dream (Album)?\n", - "A: What's Inside was released in the year 1995. Cassandra's Dream (album) was released in the year 2008. Thus, of the two, the album to release earlier is What's Inside. So the answer is: What's Inside.\n", - "\n", - "\n", - "Context:\n", - "Maria Alexandrovna (Marie of Hesse): Maria Alexandrovna , born Princess Marie of Hesse and by Rhine (8 August 1824 – 3 June 1880) was Empress of Russia as the first wife of Emperor Alexander II.\n", - "Grand Duke Alexei Alexandrovich of Russia: Grand Duke Alexei Alexandrovich of Russia,(Russian: Алексей Александрович; 14 January 1850 (2 January O.S.) in St. Petersburg – 14 November 1908 in Paris) was the fifth child and the fourth son of Alexander II of Russia and his first wife Maria Alexandrovna (Marie of Hesse).\n", - "Q: What is the cause of death of Grand Duke Alexei Alexandrovich Of Russia's mother?\n", - "A: The mother of Grand Duke Alexei Alexandrovich of Russia is Maria Alexandrovna. Maria Alexandrovna died from tuberculosis. So the answer is: tuberculosis.\n", - "\n", - "\n", - "Context:\n", - "Laughter in Hell: Laughter in Hell is a 1933 American Pre-Code drama film directed by Edward L. Cahn and starring Pat O'Brien. The film's title was typical of the sensationalistic titles of many Pre-Code films.\n", - "Edward L. Cahn: Edward L. Cahn (February 12, 1899 – August 25, 1963) was an American film director.\n", - "Q: When did the director of film Laughter In Hell die?\n", - "A: The film Laughter In Hell was directed by Edward L. Cahn. Edward L. Cahn died on August 25, 1963. So the answer is: August 25, 1963.\n", - "\n", - "Second, please complete the answer by thinking step-by-step.\n", - "\n", - "Context:\n", - "Seoul High School: Seoul High School( Hangul: 서울고등학교) is a public high school located in the heart of Seoul, South Korea.\n", - "North Marion High School (Oregon): North Marion High School is a public high school in Aurora, Oregon, United States. The school is part of the North Marion School District with all four schools being located on the same campus. The school draws students from the cities of Aurora, Hubbard, and Donald as well as the communities of Broadacres and Butteville.\n", - "Marion High School (Kansas): Marion High School is a public high school in Marion, Kansas, USA. It is one of three schools operated by Marion USD 408, and is the sole high school in the district.\n", - "Northwest High School: Northwest High School or North West High School may refer to:\n", - "Marion High School (Indiana): Marion High School is a high school in Marion, Indiana with more than 1,000 students.\n", - "Macon County High School: Macon County High School is located in Montezuma, Georgia, United States, which is a part of Macon County. Enrollment as of the 2017- 2018 school year is 491.\n", - "Canyon High School (Ogden, Utah): Canyon High School was a high school in Ogden, Utah.\n", - "Northside High School: Northside High School or North Side High School or Northside Christian School or similar can refer to:\n", - "Springs Boys' High School: Springs Boys' High School is a high school in Springs, Gauteng, South Africa.\n", - "International School of Koje: International School of Koje( ISK) is a privately funded international school located in Geoje, South Korea.\n", - "Anderson High School (Anderson, Indiana): Anderson High School is a public high school located in Anderson, Indiana.\n", - "North Marion High School (West Virginia): North Marion High School is a public Double A (\"AA\") high school in the U.S. state of West Virginia, with a current enrollment of 851 students. North Marion High School is located approximately 4 miles from Farmington, West Virginia on US Route 250 north. While it is closer to the city of Mannington, West Virginia, and is often considered to be located in Rachel, West Virginia, the school mailing address is Farmington. Rachel is a small coal mining community located adjacent to the school, and is an unincorporated municipality. North Marion High School is represented as \"Grantville High School\" in the popular alternative history novel \"1632\" by writer Eric Flint. The novel is set in the fictional town of Grantville, which is based on the real town and surroundings of Mannington.\n", - "Q: Are North Marion High School (Oregon) and Seoul High School both located in the same country?\n", - "A:\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "North Marion High School (Oregon) is located in the country of United States. Seoul High School is located in the country of South Korea. Thus, they are not in the same country. So the answer is: no.\n", - "\n", - "--------------------------------------------------------------------------------\n" - ] - } - ], - "source": [ - "for i in range(len(questions)):\n", - " print(f\"\\n\\n>>>>>>>>>>>> Below are outputs of Case {i+1} <<<<<<<<<<<<\\n\\n\")\n", - "\n", - " # reset the assistant. Always reset the assistant before starting a new conversation.\n", - " assistant.reset()\n", - "\n", - " qa_problem = questions[i]\n", - " ragproxyagent.initiate_chat(assistant, message=ragproxyagent.message_generator, problem=qa_problem, n_results=10)" + "qa_problem = \"Who is the author of FLAML?\"\n", + "chat_result = ragproxyagent.initiate_chat(assistant, message=ragproxyagent.message_generator, problem=qa_problem)" ] } ], diff --git a/setup.py b/setup.py index 29c7dda0322..870a10899ec 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ __version__ = version["__version__"] install_requires = [ - "openai>=1.3", + "openai>=1.23.3", "diskcache", "termcolor", "flaml", @@ -35,7 +35,43 @@ "ipykernel>=6.29.0", ] -rag = ["sentence_transformers", "pypdf", "ipython", "beautifulsoup4", "markdownify"] +retrieve_chat = ["chromadb", "sentence_transformers", "pypdf", "ipython", "beautifulsoup4", "markdownify"] + +extra_require = { + "test": [ + "coverage>=5.3", + "ipykernel", + "nbconvert", + "nbformat", + "pre-commit", + "pytest-asyncio", + "pytest>=6.1.1,<8", + "pandas", + ], + "blendsearch": ["flaml[blendsearch]"], + "mathchat": ["sympy", "pydantic==1.10.9", "wolframalpha"], + "retrievechat": retrieve_chat, + "retrievechat-pgvector": [ + *retrieve_chat, + "pgvector>=0.2.5", + "psycopg>=3.1.18", + ], + "retrievechat-qdrant": [ + *retrieve_chat, + "qdrant_client[fastembed]", + ], + "autobuild": ["chromadb", "sentence-transformers", "huggingface-hub"], + "teachable": ["chromadb"], + "lmm": ["replicate", "pillow"], + "graph": ["networkx", "matplotlib"], + "gemini": ["google-generativeai>=0.5,<1", "pillow", "pydantic"], + "websurfer": ["beautifulsoup4", "markdownify", "pdfminer.six", "pathvalidate"], + "redis": ["redis"], + "cosmosdb": ["azure-cosmos>=4.2.0"], + "websockets": ["websockets>=12.0,<13"], + "jupyter-executor": jupyter_executor, + "types": ["mypy==1.9.0", "pytest>=6.1.1,<8"] + jupyter_executor, +} setuptools.setup( name="pyautogen", @@ -48,34 +84,7 @@ url="https://github.com/microsoft/autogen", packages=setuptools.find_packages(include=["autogen*"], exclude=["test"]), install_requires=install_requires, - extras_require={ - "test": [ - "coverage>=5.3", - "ipykernel", - "nbconvert", - "nbformat", - "pre-commit", - "pytest-asyncio", - "pytest>=6.1.1,<8", - "pandas", - ], - "blendsearch": ["flaml[blendsearch]"], - "mathchat": ["sympy", "pydantic==1.10.9", "wolframalpha"], - "retrievechat": ["chromadb"] + rag, - "retrievechat-pgvector": ["pgvector>=0.2.5", "psycopg>=3.1.18"] + rag, - "retrievechat-qdrant": ["qdrant_client[fastembed]"] + rag, - "autobuild": ["chromadb", "sentence-transformers", "huggingface-hub"], - "teachable": ["chromadb"], - "lmm": ["replicate", "pillow"], - "graph": ["networkx", "matplotlib"], - "gemini": ["google-generativeai>=0.5,<1", "pillow", "pydantic"], - "websurfer": ["beautifulsoup4", "markdownify", "pdfminer.six", "pathvalidate"], - "redis": ["redis"], - "cosmosdb": ["azure-cosmos>=4.2.0"], - "websockets": ["websockets>=12.0,<13"], - "jupyter-executor": jupyter_executor, - "types": ["mypy==1.9.0", "pytest>=6.1.1,<8"] + jupyter_executor, - }, + extras_require=extra_require, classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", diff --git a/test/agentchat/contrib/test_pgvector_retrievechat.py b/test/agentchat/contrib/retrievechat/test_pgvector_retrievechat.py similarity index 77% rename from test/agentchat/contrib/test_pgvector_retrievechat.py rename to test/agentchat/contrib/retrievechat/test_pgvector_retrievechat.py index f4a1247ac65..b104f25af76 100644 --- a/test/agentchat/contrib/test_pgvector_retrievechat.py +++ b/test/agentchat/contrib/retrievechat/test_pgvector_retrievechat.py @@ -9,10 +9,10 @@ from autogen import config_list_from_json from autogen.agentchat.contrib.retrieve_assistant_agent import RetrieveAssistantAgent -sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) +sys.path.append(os.path.join(os.path.dirname(__file__), "../../..")) from conftest import skip_openai # noqa: E402 -sys.path.append(os.path.join(os.path.dirname(__file__), "..")) +sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) from test_assistant_agent import KEY_LOC, OAI_CONFIG_LIST # noqa: E402 try: @@ -27,14 +27,14 @@ except ImportError: skip = True else: - skip = False or skip_openai + skip = False -test_dir = os.path.join(os.path.dirname(__file__), "../..", "test_files") +test_dir = os.path.join(os.path.dirname(__file__), "../../..", "test_files") @pytest.mark.skipif( - skip, + skip or skip_openai, reason="dependency is not installed OR requested to skip", ) def test_retrievechat(): @@ -97,34 +97,5 @@ def test_retrievechat(): print(conversations) -@pytest.mark.skipif( - skip, - reason="dependency is not installed", -) -def test_retrieve_config(caplog): - # test warning message when no docs_path is provided - ragproxyagent = RetrieveUserProxyAgent( - name="ragproxyagent", - human_input_mode="NEVER", - max_consecutive_auto_reply=2, - retrieve_config={ - "chunk_token_size": 2000, - "get_or_create": True, - }, - ) - - # Capture the printed content - captured_logs = caplog.records[0] - print(captured_logs) - - # Assert on the printed content - assert ( - f"docs_path is not provided in retrieve_config. Will raise ValueError if the collection `{ragproxyagent._collection_name}` doesn't exist." - in captured_logs.message - ) - assert captured_logs.levelname == "WARNING" - - if __name__ == "__main__": test_retrievechat() - test_retrieve_config() diff --git a/test/agentchat/contrib/test_qdrant_retrievechat.py b/test/agentchat/contrib/retrievechat/test_qdrant_retrievechat.py similarity index 95% rename from test/agentchat/contrib/test_qdrant_retrievechat.py rename to test/agentchat/contrib/retrievechat/test_qdrant_retrievechat.py index e4bede17477..85f098c64b1 100755 --- a/test/agentchat/contrib/test_qdrant_retrievechat.py +++ b/test/agentchat/contrib/retrievechat/test_qdrant_retrievechat.py @@ -8,10 +8,10 @@ from autogen import config_list_from_json from autogen.agentchat.contrib.retrieve_assistant_agent import RetrieveAssistantAgent -sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) +sys.path.append(os.path.join(os.path.dirname(__file__), "../../..")) from conftest import skip_openai # noqa: E402 -sys.path.append(os.path.join(os.path.dirname(__file__), "..")) +sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) from test_assistant_agent import KEY_LOC, OAI_CONFIG_LIST # noqa: E402 try: @@ -35,7 +35,7 @@ else: skip = False or skip_openai -test_dir = os.path.join(os.path.dirname(__file__), "../..", "test_files") +test_dir = os.path.join(os.path.dirname(__file__), "../../..", "test_files") @pytest.mark.skipif( diff --git a/test/agentchat/contrib/test_retrievechat.py b/test/agentchat/contrib/retrievechat/test_retrievechat.py similarity index 75% rename from test/agentchat/contrib/test_retrievechat.py rename to test/agentchat/contrib/retrievechat/test_retrievechat.py index e7bbc975041..ceb97357785 100755 --- a/test/agentchat/contrib/test_retrievechat.py +++ b/test/agentchat/contrib/retrievechat/test_retrievechat.py @@ -7,10 +7,10 @@ import autogen -sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) -from conftest import skip_openai # noqa: E402 +sys.path.append(os.path.join(os.path.dirname(__file__), "../../..")) +from conftest import reason, skip_openai # noqa: E402 -sys.path.append(os.path.join(os.path.dirname(__file__), "..")) +sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) from test_assistant_agent import KEY_LOC, OAI_CONFIG_LIST # noqa: E402 try: @@ -27,12 +27,14 @@ except ImportError: skip = True else: - skip = False or skip_openai + skip = False + +reason = "do not run on MacOS or windows OR dependency is not installed OR " + reason @pytest.mark.skipif( - sys.platform in ["darwin", "win32"] or skip, - reason="do not run on MacOS or windows OR dependency is not installed OR requested to skip", + sys.platform in ["darwin", "win32"] or skip or skip_openai, + reason=reason, ) def test_retrievechat(): conversations = {} @@ -80,9 +82,9 @@ def test_retrievechat(): @pytest.mark.skipif( sys.platform in ["darwin", "win32"] or skip, - reason="do not run on MacOS or windows OR dependency is not installed OR requested to skip", + reason=reason, ) -def test_retrieve_config(caplog): +def test_retrieve_config(): # test warning message when no docs_path is provided ragproxyagent = RetrieveUserProxyAgent( name="ragproxyagent", @@ -93,17 +95,7 @@ def test_retrieve_config(caplog): "get_or_create": True, }, ) - - # Capture the printed content - captured_logs = caplog.records[0] - print(captured_logs) - - # Assert on the printed content - assert ( - f"docs_path is not provided in retrieve_config. Will raise ValueError if the collection `{ragproxyagent._collection_name}` doesn't exist." - in captured_logs.message - ) - assert captured_logs.levelname == "WARNING" + assert ragproxyagent._docs_path is None if __name__ == "__main__": diff --git a/test/agentchat/contrib/vectordb/test_pgvectordb.py b/test/agentchat/contrib/vectordb/test_pgvectordb.py index 3eafd2df2d5..bcccef2abfe 100644 --- a/test/agentchat/contrib/vectordb/test_pgvectordb.py +++ b/test/agentchat/contrib/vectordb/test_pgvectordb.py @@ -2,6 +2,7 @@ import sys import pytest +from conftest import reason sys.path.append(os.path.join(os.path.dirname(__file__), "..")) @@ -9,28 +10,33 @@ import pgvector import sentence_transformers - from autogen.agentchat.contrib.vectordb.pgvector import PGVector + from autogen.agentchat.contrib.vectordb.pgvectordb import PGVectorDB except ImportError: skip = True else: skip = False +reason = "do not run on MacOS or windows OR dependency is not installed OR " + reason -@pytest.mark.skipif(skip, reason="dependency is not installed OR requested to skip") + +@pytest.mark.skipif( + sys.platform in ["darwin", "win32"] or skip, + reason=reason, +) def test_pgvector(): # test create collection db_config = { "connection_string": "postgresql://postgres:postgres@localhost:5432/postgres", } - db = PGVector(connection_string=db_config["connection_string"]) + db = PGVectorDB(connection_string=db_config["connection_string"]) collection_name = "test_collection" - collection = db.create_collection(collection_name, overwrite=True, get_or_create=True) + collection = db.create_collection(collection_name=collection_name, overwrite=True, get_or_create=True) assert collection.name == collection_name # test_delete_collection db.delete_collection(collection_name) - pytest.raises(ValueError, db.get_collection, collection_name) + assert collection.table_exists(table_name=collection_name) is False # test more create collection collection = db.create_collection(collection_name, overwrite=False, get_or_create=False) @@ -48,21 +54,24 @@ def test_pgvector(): # test_insert_docs docs = [{"content": "doc1", "id": "1"}, {"content": "doc2", "id": "2"}, {"content": "doc3", "id": "3"}] db.insert_docs(docs, collection_name, upsert=False) - res = db.get_collection(collection_name).get(["1", "2"]) - assert res["documents"] == ["doc1", "doc2"] + res = db.get_collection(collection_name).get(ids=["1", "2"]) + final_results = [result.get("content") for result in res] + assert final_results == ["doc1", "doc2"] # test_update_docs docs = [{"content": "doc11", "id": "1"}, {"content": "doc2", "id": "2"}, {"content": "doc3", "id": "3"}] db.update_docs(docs, collection_name) res = db.get_collection(collection_name).get(["1", "2"]) - assert res["documents"] == ["doc11", "doc2"] + final_results = [result.get("content") for result in res] + assert final_results == ["doc11", "doc2"] # test_delete_docs ids = ["1"] collection_name = "test_collection" db.delete_docs(ids, collection_name) res = db.get_collection(collection_name).get(ids) - assert res["documents"] == [] + final_results = [result.get("content") for result in res] + assert final_results == [] # test_retrieve_docs queries = ["doc2", "doc3"] @@ -70,12 +79,13 @@ def test_pgvector(): res = db.retrieve_docs(queries, collection_name) assert [[r[0]["id"] for r in rr] for rr in res] == [["2", "3"], ["3", "2"]] res = db.retrieve_docs(queries, collection_name, distance_threshold=0.1) - print(res) assert [[r[0]["id"] for r in rr] for rr in res] == [["2"], ["3"]] # test_get_docs_by_ids res = db.get_docs_by_ids(["1", "2"], collection_name) assert [r["id"] for r in res] == ["2"] # "1" has been deleted + res = db.get_docs_by_ids(collection_name=collection_name) + assert set([r["id"] for r in res]) == set(["2", "3"]) # All Docs returned if __name__ == "__main__": From c94b5c6a6160b85e7ef7abd68c6de5f2405b2f44 Mon Sep 17 00:00:00 2001 From: Chi Wang Date: Sun, 28 Apr 2024 12:47:27 -0700 Subject: [PATCH 14/30] Use config list in notebook (#2537) --- ...at_auto_feedback_from_code_execution.ipynb | 11 +- website/docs/topics/llm_configuration.ipynb | 682 +++++++++--------- 2 files changed, 348 insertions(+), 345 deletions(-) diff --git a/notebook/agentchat_auto_feedback_from_code_execution.ipynb b/notebook/agentchat_auto_feedback_from_code_execution.ipynb index b81b58472c7..bf784889d61 100644 --- a/notebook/agentchat_auto_feedback_from_code_execution.ipynb +++ b/notebook/agentchat_auto_feedback_from_code_execution.ipynb @@ -30,14 +30,17 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", - "\n", "from IPython.display import Image, display\n", "\n", "import autogen\n", "from autogen.coding import LocalCommandLineCodeExecutor\n", "\n", - "config_list = [{\"model\": \"gpt-4\", \"api_key\": os.getenv(\"OPENAI_API_KEY\")}]" + "config_list = autogen.config_list_from_json(\n", + " \"OAI_CONFIG_LIST\",\n", + " filter_dict={\"tags\": [\"gpt-4\"]}, # comment out to get all\n", + ")\n", + "# When using a single openai endpoint, you can use the following:\n", + "# config_list = [{\"model\": \"gpt-4\", \"api_key\": os.getenv(\"OPENAI_API_KEY\")}]\n" ] }, { @@ -882,7 +885,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.5" + "version": "3.10.14" }, "vscode": { "interpreter": { diff --git a/website/docs/topics/llm_configuration.ipynb b/website/docs/topics/llm_configuration.ipynb index 518092ecfba..073ec686b2c 100644 --- a/website/docs/topics/llm_configuration.ipynb +++ b/website/docs/topics/llm_configuration.ipynb @@ -1,342 +1,342 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# LLM Configuration\n", - "\n", - "In AutoGen, agents use LLMs as key components to understand and react. To configure an agent's access to LLMs, you can specify an `llm_config` argument in its constructor. For example, the following snippet shows a configuration that uses `gpt-4`:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "\n", - "llm_config = {\n", - " \"config_list\": [{\"model\": \"gpt-4\", \"api_key\": os.environ[\"OPENAI_API_KEY\"]}],\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "````{=mdx}\n", - ":::warning\n", - "It is important to never commit secrets into your code, therefore we read the OpenAI API key from an environment variable.\n", - ":::\n", - "````\n", - "\n", - "This `llm_config` can then be passed to an agent's constructor to enable it to use the LLM." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import autogen\n", - "\n", - "assistant = autogen.AssistantAgent(name=\"assistant\", llm_config=llm_config)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "## Introduction to `config_list`\n", - "\n", - "Different tasks may require different models, and the `config_list` allows specifying the different endpoints and configurations that are to be used. It is a list of dictionaries, each of which contains the following keys depending on the kind of endpoint being used:\n", - "\n", - "````{=mdx}\n", - "import Tabs from '@theme/Tabs';\n", - "import TabItem from '@theme/TabItem';\n", - "\n", - "\n", - " \n", - " - `model` (str, required): The identifier of the model to be used, such as 'gpt-4', 'gpt-3.5-turbo'.\n", - " - `api_key` (str, optional): The API key required for authenticating requests to the model's API endpoint.\n", - " - `base_url` (str, optional): The base URL of the API endpoint. This is the root address where API calls are directed.\n", - " - `tags` (List[str], optional): Tags which can be used for filtering.\n", - "\n", - " Example:\n", - " ```json\n", - " [\n", - " {\n", - " \"model\": \"gpt-4\",\n", - " \"api_key\": os.environ['OPENAI_API_KEY']\n", - " }\n", - " ]\n", - " ```\n", - " \n", - " \n", - " - `model` (str, required): The deployment to be used. The model corresponds to the deployment name on Azure OpenAI.\n", - " - `api_key` (str, optional): The API key required for authenticating requests to the model's API endpoint.\n", - " - `api_type`: `azure`\n", - " - `base_url` (str, optional): The base URL of the API endpoint. This is the root address where API calls are directed.\n", - " - `api_version` (str, optional): The version of the Azure API you wish to use.\n", - " - `tags` (List[str], optional): Tags which can be used for filtering.\n", - "\n", - " Example:\n", - " ```json\n", - " [\n", - " {\n", - " \"model\": \"my-gpt-4-deployment\",\n", - " \"api_type\": \"azure\",\n", - " \"api_key\": os.environ['AZURE_OPENAI_API_KEY'],\n", - " \"base_url\": \"https://ENDPOINT.openai.azure.com/\",\n", - " \"api_version\": \"2024-02-15-preview\"\n", - " }\n", - " ]\n", - " ```\n", - " \n", - " \n", - " - `model` (str, required): The identifier of the model to be used, such as 'llama-7B'.\n", - " - `api_key` (str, optional): The API key required for authenticating requests to the model's API endpoint.\n", - " - `base_url` (str, optional): The base URL of the API endpoint. This is the root address where API calls are directed.\n", - " - `tags` (List[str], optional): Tags which can be used for filtering.\n", - "\n", - " Example:\n", - " ```json\n", - " [\n", - " {\n", - " \"model\": \"llama-7B\",\n", - " \"base_url\": \"http://localhost:1234\"\n", - " }\n", - " ]\n", - " ```\n", - " \n", - "\n", - "````\n", - "\n", - "---\n", - "\n", - "````{=mdx}\n", - ":::tip\n", - "By default this will create a model client which assumes an OpenAI API (or compatible) endpoint. To use custom model clients, see [here](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_custom_model.ipynb).\n", - ":::\n", - "````\n", - "\n", - "### `OAI_CONFIG_LIST` pattern\n", - "\n", - "A common, useful pattern used is to define this `config_list` is via JSON (specified as a file or an environment variable set to a JSON-formatted string) and then use the [`config_list_from_json`](/docs/reference/oai/openai_utils#config_list_from_json) helper function to load it:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "config_list = autogen.config_list_from_json(\n", - " env_or_file=\"OAI_CONFIG_LIST\",\n", - ")\n", - "\n", - "# Then, create the assistant agent with the config list\n", - "assistant = autogen.AssistantAgent(name=\"assistant\", llm_config={\"config_list\": config_list})" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This can be helpful as it keeps all the configuration in one place across different projects or notebooks.\n", - "\n", - "This function interprets the `env_or_file` argument as follows:\n", - "\n", - "- If `env_or_file` is an environment variable then:\n", - " - It will first try to load the file from the path specified in the environment variable.\n", - " - If there is no file, it will try to interpret the environment variable as a JSON string.\n", - "- Otherwise, it will try to open the file at the path specified by `env_or_file`.\n", - "\n", - "### Why is it a list?\n", - "\n", - "Being a list allows you to define multiple models that can be used by the agent. This is useful for a few reasons:\n", - "\n", - "- If one model times out or fails, the agent can try another model.\n", - "- Having a single global list of models and [filtering it](#config-list-filtering) based on certain keys (e.g. name, tag) in order to pass select models into a certain agent (e.g. use cheaper GPT 3.5 for agents solving easier tasks)\n", - "- While the core agents, (e.g. conversable or assistant) do not have special logic around selecting configs, some of the specialized agents *may* have logic to select the best model based on the task at hand.\n", - "\n", - "### How does an agent decide which model to pick out of the list?\n", - "\n", - "An agent uses the very first model available in the \"config_list\" and makes LLM calls against this model. If the model fails (e.g. API throttling) the agent will retry the request against the 2nd model and so on until prompt completion is received (or throws an error if none of the models successfully completes the request). In general there's no implicit/hidden logic inside agents that is used to pick \"the best model for the task\". However, some specialized agents may attempt to choose \"the best model for the task\". It is developers responsibility to pick the right models and use them with agents.\n", - "\n", - "### Config list filtering\n", - "\n", - "As described above the list can be filtered based on certain criteria. This is defined as a dictionary of key to filter on and value to filter by. For example, if you have a list of configs and you want to select the one with the model \"gpt-3.5-turbo\" you can use the following filter:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "filter_dict = {\"model\": \"gpt-3.5-turbo\"}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "This can then be applied to a config list loaded in Python with [`filter_config`](/docs/reference/oai/openai_utils#filter_config):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "config_list = autogen.filter_config(config_list, filter_dict)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Or, directly when loading the config list using [`config_list_from_json`](/docs/reference/oai/openai_utils#config_list_from_json):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "config_list = autogen.config_list_from_json(env_or_file=\"OAI_CONFIG_LIST\", filter_dict=filter_dict)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Tags\n", - "\n", - "Model names can differ between OpenAI and Azure OpenAI, so tags offer an easy way to smooth over this inconsistency. Tags are a list of strings in the `config_list`, for example for the following `config_list`:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "config_list = [\n", - " {\"model\": \"my-gpt-4-deployment\", \"api_key\": \"\", \"tags\": [\"gpt4\", \"openai\"]},\n", - " {\"model\": \"llama-7B\", \"base_url\": \"http://127.0.0.1:8080\", \"tags\": [\"llama\", \"local\"]},\n", - "]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Then when filtering the `config_list` you can can specify the desired tags. A config is selected if it has at least one of the tags specified in the filter. For example, to just get the `llama` model, you can use the following filter:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "filter_dict = {\"tags\": [\"llama\", \"another_tag\"]}\n", - "config_list = autogen.filter_config(config_list, filter_dict)\n", - "assert len(config_list) == 1" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Other configuration parameters\n", - "\n", - "Besides the `config_list`, there are other parameters that can be used to configure the LLM. These are split between parameters specifically used by Autogen and those passed into the model client.\n", - "\n", - "### AutoGen specific parameters\n", - "\n", - "- `cache_seed` - This is a legacy parameter and not recommended to be used unless the reason for using it is to disable the default caching behavior. To disable default caching, set this to `None`. Otherwise, by default or if an int is passed the [DiskCache](/docs/reference/cache/disk_cache) will be used. For the new way of using caching, pass a [Cache](/docs/reference/cache/) object into [`initiate_chat`](/docs/reference/agentchat/conversable_agent#initiate_chat).\n", - "\n", - "### Extra model client parameters\n", - "\n", - "It is also possible to passthrough parameters through to the OpenAI client. Parameters that correspond to the [`OpenAI` client](https://github.com/openai/openai-python/blob/d231d1fa783967c1d3a1db3ba1b52647fff148ac/src/openai/_client.py#L67) or the [`OpenAI` completions create API](https://github.com/openai/openai-python/blob/d231d1fa783967c1d3a1db3ba1b52647fff148ac/src/openai/resources/completions.py#L35) can be supplied.\n", - "\n", - "This is commonly used for things like `temperature`, or `timeout`.\n", - "\n", - "## Example\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "llm_config = {\n", - " \"config_list\": [\n", - " {\n", - " \"model\": \"my-gpt-4-deployment\",\n", - " \"api_key\": os.environ.get(\"AZURE_OPENAI_API_KEY\"),\n", - " \"api_type\": \"azure\",\n", - " \"base_url\": os.environ.get(\"AZURE_OPENAI_API_BASE\"),\n", - " \"api_version\": \"2024-02-15-preview\",\n", - " },\n", - " {\n", - " \"model\": \"llama-7B\",\n", - " \"base_url\": \"http://127.0.0.1:8080\",\n", - " \"api_type\": \"openai\",\n", - " },\n", - " ],\n", - " \"temperature\": 0.9,\n", - " \"timeout\": 300,\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Other helpers for loading a config list\n", - "\n", - "- [`get_config_list`](/docs/reference/oai/openai_utils#get_config_list): Generates configurations for API calls, primarily from provided API keys.\n", - "- [`config_list_openai_aoai`](/docs/reference/oai/openai_utils#config_list_openai_aoai): Constructs a list of configurations using both Azure OpenAI and OpenAI endpoints, sourcing API keys from environment variables or local files.\n", - "- [`config_list_from_models`](/docs/reference/oai/openai_utils#config_list_from_models): Creates configurations based on a provided list of models, useful when targeting specific models without manually specifying each configuration.\n", - "- [`config_list_from_dotenv`](/docs/reference/oai/openai_utils#config_list_from_dotenv): Constructs a configuration list from a `.env` file, offering a consolidated way to manage multiple API configurations and keys from a single file.\n", - "\n", - "See [this notebook](https://github.com/microsoft/autogen/blob/main/notebook/config_loader_utility_functions.ipynb) for examples of using the above functions." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "masterclass", - "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.11.7" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# LLM Configuration\n", + "\n", + "In AutoGen, agents use LLMs as key components to understand and react. To configure an agent's access to LLMs, you can specify an `llm_config` argument in its constructor. For example, the following snippet shows a configuration that uses `gpt-4`:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "llm_config = {\n", + " \"config_list\": [{\"model\": \"gpt-4\", \"api_key\": os.environ[\"OPENAI_API_KEY\"]}],\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "````{=mdx}\n", + ":::warning\n", + "It is important to never commit secrets into your code, therefore we read the OpenAI API key from an environment variable.\n", + ":::\n", + "````\n", + "\n", + "This `llm_config` can then be passed to an agent's constructor to enable it to use the LLM." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import autogen\n", + "\n", + "assistant = autogen.AssistantAgent(name=\"assistant\", llm_config=llm_config)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## Introduction to `config_list`\n", + "\n", + "Different tasks may require different models, and the `config_list` allows specifying the different endpoints and configurations that are to be used. It is a list of dictionaries, each of which contains the following keys depending on the kind of endpoint being used:\n", + "\n", + "````{=mdx}\n", + "import Tabs from '@theme/Tabs';\n", + "import TabItem from '@theme/TabItem';\n", + "\n", + "\n", + " \n", + " - `model` (str, required): The identifier of the model to be used, such as 'gpt-4', 'gpt-3.5-turbo'.\n", + " - `api_key` (str, optional): The API key required for authenticating requests to the model's API endpoint.\n", + " - `base_url` (str, optional): The base URL of the API endpoint. This is the root address where API calls are directed.\n", + " - `tags` (List[str], optional): Tags which can be used for filtering.\n", + "\n", + " Example:\n", + " ```json\n", + " [\n", + " {\n", + " \"model\": \"gpt-4\",\n", + " \"api_key\": os.environ['OPENAI_API_KEY']\n", + " }\n", + " ]\n", + " ```\n", + " \n", + " \n", + " - `model` (str, required): The deployment to be used. The model corresponds to the deployment name on Azure OpenAI.\n", + " - `api_key` (str, optional): The API key required for authenticating requests to the model's API endpoint.\n", + " - `api_type`: `azure`\n", + " - `base_url` (str, optional): The base URL of the API endpoint. This is the root address where API calls are directed.\n", + " - `api_version` (str, optional): The version of the Azure API you wish to use.\n", + " - `tags` (List[str], optional): Tags which can be used for filtering.\n", + "\n", + " Example:\n", + " ```json\n", + " [\n", + " {\n", + " \"model\": \"my-gpt-4-deployment\",\n", + " \"api_type\": \"azure\",\n", + " \"api_key\": os.environ['AZURE_OPENAI_API_KEY'],\n", + " \"base_url\": \"https://ENDPOINT.openai.azure.com/\",\n", + " \"api_version\": \"2024-02-15-preview\"\n", + " }\n", + " ]\n", + " ```\n", + " \n", + " \n", + " - `model` (str, required): The identifier of the model to be used, such as 'llama-7B'.\n", + " - `api_key` (str, optional): The API key required for authenticating requests to the model's API endpoint.\n", + " - `base_url` (str, optional): The base URL of the API endpoint. This is the root address where API calls are directed.\n", + " - `tags` (List[str], optional): Tags which can be used for filtering.\n", + "\n", + " Example:\n", + " ```json\n", + " [\n", + " {\n", + " \"model\": \"llama-7B\",\n", + " \"base_url\": \"http://localhost:1234\"\n", + " }\n", + " ]\n", + " ```\n", + " \n", + "\n", + "````\n", + "\n", + "---\n", + "\n", + "````{=mdx}\n", + ":::tip\n", + "By default this will create a model client which assumes an OpenAI API (or compatible) endpoint. To use custom model clients, see [here](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_custom_model.ipynb).\n", + ":::\n", + "````\n", + "\n", + "### `OAI_CONFIG_LIST` pattern\n", + "\n", + "A common, useful pattern used is to define this `config_list` is via JSON (specified as a file or an environment variable set to a JSON-formatted string) and then use the [`config_list_from_json`](/docs/reference/oai/openai_utils#config_list_from_json) helper function to load it:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "config_list = autogen.config_list_from_json(\n", + " env_or_file=\"OAI_CONFIG_LIST\",\n", + ")\n", + "\n", + "# Then, create the assistant agent with the config list\n", + "assistant = autogen.AssistantAgent(name=\"assistant\", llm_config={\"config_list\": config_list})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This can be helpful as it keeps all the configuration in one place across different projects or notebooks.\n", + "\n", + "This function interprets the `env_or_file` argument as follows:\n", + "\n", + "- If `env_or_file` is an environment variable then:\n", + " - It will first try to load the file from the path specified in the environment variable.\n", + " - If there is no file, it will try to interpret the environment variable as a JSON string.\n", + "- Otherwise, it will try to open the file at the path specified by `env_or_file`.\n", + "\n", + "### Why is it a list?\n", + "\n", + "Being a list allows you to define multiple models that can be used by the agent. This is useful for a few reasons:\n", + "\n", + "- If one model times out or fails, the agent can try another model.\n", + "- Having a single global list of models and [filtering it](#config-list-filtering) based on certain keys (e.g. name, tag) in order to pass select models into a certain agent (e.g. use cheaper GPT 3.5 for agents solving easier tasks)\n", + "- While the core agents, (e.g. conversable or assistant) do not have special logic around selecting configs, some of the specialized agents *may* have logic to select the best model based on the task at hand.\n", + "\n", + "### How does an agent decide which model to pick out of the list?\n", + "\n", + "An agent uses the very first model available in the \"config_list\" and makes LLM calls against this model. If the model fails (e.g. API throttling) the agent will retry the request against the 2nd model and so on until prompt completion is received (or throws an error if none of the models successfully completes the request). In general there's no implicit/hidden logic inside agents that is used to pick \"the best model for the task\". However, some specialized agents may attempt to choose \"the best model for the task\". It is developers responsibility to pick the right models and use them with agents.\n", + "\n", + "### Config list filtering\n", + "\n", + "As described above the list can be filtered based on certain criteria. This is defined as a dictionary of key to filter on and values to filter by. For example, if you have a list of configs and you want to select the one with the model \"gpt-3.5-turbo\" you can use the following filter:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "filter_dict = {\"model\": [\"gpt-3.5-turbo\"]}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "This can then be applied to a config list loaded in Python with [`filter_config`](/docs/reference/oai/openai_utils#filter_config):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "config_list = autogen.filter_config(config_list, filter_dict)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or, directly when loading the config list using [`config_list_from_json`](/docs/reference/oai/openai_utils#config_list_from_json):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "config_list = autogen.config_list_from_json(env_or_file=\"OAI_CONFIG_LIST\", filter_dict=filter_dict)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Tags\n", + "\n", + "Model names can differ between OpenAI and Azure OpenAI, so tags offer an easy way to smooth over this inconsistency. Tags are a list of strings in the `config_list`, for example for the following `config_list`:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "config_list = [\n", + " {\"model\": \"my-gpt-4-deployment\", \"api_key\": \"\", \"tags\": [\"gpt4\", \"openai\"]},\n", + " {\"model\": \"llama-7B\", \"base_url\": \"http://127.0.0.1:8080\", \"tags\": [\"llama\", \"local\"]},\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then when filtering the `config_list` you can can specify the desired tags. A config is selected if it has at least one of the tags specified in the filter. For example, to just get the `llama` model, you can use the following filter:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "filter_dict = {\"tags\": [\"llama\", \"another_tag\"]}\n", + "config_list = autogen.filter_config(config_list, filter_dict)\n", + "assert len(config_list) == 1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Other configuration parameters\n", + "\n", + "Besides the `config_list`, there are other parameters that can be used to configure the LLM. These are split between parameters specifically used by Autogen and those passed into the model client.\n", + "\n", + "### AutoGen specific parameters\n", + "\n", + "- `cache_seed` - This is a legacy parameter and not recommended to be used unless the reason for using it is to disable the default caching behavior. To disable default caching, set this to `None`. Otherwise, by default or if an int is passed the [DiskCache](/docs/reference/cache/disk_cache) will be used. For the new way of using caching, pass a [Cache](/docs/reference/cache/) object into [`initiate_chat`](/docs/reference/agentchat/conversable_agent#initiate_chat).\n", + "\n", + "### Extra model client parameters\n", + "\n", + "It is also possible to passthrough parameters through to the OpenAI client. Parameters that correspond to the [`OpenAI` client](https://github.com/openai/openai-python/blob/d231d1fa783967c1d3a1db3ba1b52647fff148ac/src/openai/_client.py#L67) or the [`OpenAI` completions create API](https://github.com/openai/openai-python/blob/d231d1fa783967c1d3a1db3ba1b52647fff148ac/src/openai/resources/completions.py#L35) can be supplied.\n", + "\n", + "This is commonly used for things like `temperature`, or `timeout`.\n", + "\n", + "## Example\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "llm_config = {\n", + " \"config_list\": [\n", + " {\n", + " \"model\": \"my-gpt-4-deployment\",\n", + " \"api_key\": os.environ.get(\"AZURE_OPENAI_API_KEY\"),\n", + " \"api_type\": \"azure\",\n", + " \"base_url\": os.environ.get(\"AZURE_OPENAI_API_BASE\"),\n", + " \"api_version\": \"2024-02-15-preview\",\n", + " },\n", + " {\n", + " \"model\": \"llama-7B\",\n", + " \"base_url\": \"http://127.0.0.1:8080\",\n", + " \"api_type\": \"openai\",\n", + " },\n", + " ],\n", + " \"temperature\": 0.9,\n", + " \"timeout\": 300,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Other helpers for loading a config list\n", + "\n", + "- [`get_config_list`](/docs/reference/oai/openai_utils#get_config_list): Generates configurations for API calls, primarily from provided API keys.\n", + "- [`config_list_openai_aoai`](/docs/reference/oai/openai_utils#config_list_openai_aoai): Constructs a list of configurations using both Azure OpenAI and OpenAI endpoints, sourcing API keys from environment variables or local files.\n", + "- [`config_list_from_models`](/docs/reference/oai/openai_utils#config_list_from_models): Creates configurations based on a provided list of models, useful when targeting specific models without manually specifying each configuration.\n", + "- [`config_list_from_dotenv`](/docs/reference/oai/openai_utils#config_list_from_dotenv): Constructs a configuration list from a `.env` file, offering a consolidated way to manage multiple API configurations and keys from a single file.\n", + "\n", + "See [this notebook](https://github.com/microsoft/autogen/blob/main/notebook/config_loader_utility_functions.ipynb) for examples of using the above functions." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "masterclass", + "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.11.7" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} From a9171211c7533fc11e078899720a3847f45807cc Mon Sep 17 00:00:00 2001 From: Wael Karkoub Date: Mon, 29 Apr 2024 01:27:34 +0100 Subject: [PATCH 15/30] Streamline Testing with `pytest-cov` and `pytest` Defaults (#2490) * done * update docs * try fix * update workflows * undo minor fix * resolve comments * adds back pytest-asyncio * minor fix * add branch coverage * restore pip install e. * test with coverage * fix mypy * fix coverage + docker + windows combo * fix bash command * formatter * formatter * one last fix * I lied, last fix * fix * fix retrieve chat test * fix windows paths * change cache seed * down grade openai version * fix openai mypy * better error type * fix image gen cache test * fix * experimenting * fix lmm * skip cosmos test * remove cosmos db * unused imports * handle more cosmosdb skips * fix flaky test --- .github/workflows/build.yml | 7 +- .github/workflows/contrib-openai.yml | 200 +++++++++--------- .github/workflows/contrib-tests.yml | 62 +++--- .github/workflows/openai.yml | 11 +- .github/workflows/samples-tools-tests.yml | 2 +- .github/workflows/type-check.yml | 4 +- autogen/cache/cache_factory.py | 4 +- autogen/code_utils.py | 2 +- autogen/oai/openai_utils.py | 6 +- pyproject.toml | 39 ++-- setup.py | 4 +- test/cache/test_cache.py | 11 +- test/cache/test_cosmos_db_cache.py | 24 ++- test/coding/test_commandline_code_executor.py | 12 +- test/oai/test_client_stream.py | 17 +- test/test_code_utils.py | 17 +- website/docs/contributor-guide/tests.md | 10 +- 17 files changed, 204 insertions(+), 228 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b45de32dddd..ce3654e5868 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: python -m pip install --upgrade pip wheel pip install -e .[cosmosdb] python -c "import autogen" - pip install pytest mock + pip install pytest-cov>=5 mock - name: Install optional dependencies for code executors # code executors and udfs auto skip without deps, so only run for python 3.11 if: matrix.python-version == '3.11' @@ -71,12 +71,11 @@ jobs: if: matrix.python-version == '3.10' run: | pip install -e .[test,redis,websockets] - coverage run -a -m pytest test --ignore=test/agentchat/contrib --skip-openai --durations=10 --durations-min=1.0 - coverage xml + pytest test --ignore=test/agentchat/contrib --skip-openai --durations=10 --durations-min=1.0 - name: Test with Cosmos DB run: | pip install -e .[test,cosmosdb] - coverage run -a -m pytest test/cache/test_cosmos_db_cache.py --skip-openai --durations=10 --durations-min=1.0 + pytest test/cache/test_cosmos_db_cache.py --skip-openai --durations=10 --durations-min=1.0 - name: Upload coverage to Codecov if: matrix.python-version == '3.10' uses: codecov/codecov-action@v3 diff --git a/.github/workflows/contrib-openai.yml b/.github/workflows/contrib-openai.yml index 73c2197c27e..1bf71115d6b 100644 --- a/.github/workflows/contrib-openai.yml +++ b/.github/workflows/contrib-openai.yml @@ -5,14 +5,15 @@ name: OpenAI4ContribTests on: pull_request: - branches: ['main'] + branches: ["main"] paths: - - 'autogen/**' - - 'test/agentchat/contrib/**' - - '.github/workflows/contrib-openai.yml' - - 'setup.py' -permissions: {} - # actions: read + - "autogen/**" + - "test/agentchat/contrib/**" + - ".github/workflows/contrib-openai.yml" + - "setup.py" +permissions: + {} + # actions: read # checks: read # contents: read # deployments: read @@ -55,7 +56,7 @@ jobs: python -m pip install --upgrade pip wheel pip install -e . python -c "import autogen" - pip install coverage pytest-asyncio + pip install pytest-cov>=5 pytest-asyncio - name: Install packages for test when needed run: | pip install docker @@ -67,8 +68,7 @@ jobs: AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }} OAI_CONFIG_LIST: ${{ secrets.OAI_CONFIG_LIST }} run: | - coverage run -a -m pytest -k test_retrievechat test/agentchat/contrib/retrievechat - coverage xml + pytest test/agentchat/contrib/retrievechat/ test/agentchat/contrib/retrievechat - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: @@ -98,7 +98,7 @@ jobs: python -m pip install --upgrade pip wheel pip install -e . python -c "import autogen" - pip install coverage pytest-asyncio + pip install pytest-cov>=5 pytest-asyncio - name: Install packages for test when needed run: | pip install docker @@ -109,8 +109,7 @@ jobs: AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }} OAI_CONFIG_LIST: ${{ secrets.OAI_CONFIG_LIST }} run: | - coverage run -a -m pytest test/agentchat/contrib/test_compressible_agent.py - coverage xml + pytest test/agentchat/contrib/test_compressible_agent.py - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: @@ -139,7 +138,7 @@ jobs: python -m pip install --upgrade pip wheel pip install -e . python -c "import autogen" - pip install coverage pytest-asyncio + pip install pytest-cov>=5 pytest-asyncio - name: Install packages for test when needed run: | pip install docker @@ -150,8 +149,7 @@ jobs: AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }} OAI_CONFIG_LIST: ${{ secrets.OAI_CONFIG_LIST }} run: | - coverage run -a -m pytest test/agentchat/contrib/test_gpt_assistant.py - coverage xml + pytest test/agentchat/contrib/test_gpt_assistant.py - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: @@ -180,7 +178,7 @@ jobs: python -m pip install --upgrade pip wheel pip install -e .[teachable] python -c "import autogen" - pip install coverage pytest + pip install pytest-cov>=5 - name: Coverage env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -188,8 +186,7 @@ jobs: AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }} OAI_CONFIG_LIST: ${{ secrets.OAI_CONFIG_LIST }} run: | - coverage run -a -m pytest test/agentchat/contrib/capabilities/test_teachable_agent.py - coverage xml + pytest test/agentchat/contrib/capabilities/test_teachable_agent.py - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: @@ -198,8 +195,8 @@ jobs: AgentBuilder: strategy: matrix: - os: [ ubuntu-latest ] - python-version: [ "3.11" ] + os: [ubuntu-latest] + python-version: ["3.11"] runs-on: ${{ matrix.os }} environment: openai1 steps: @@ -218,7 +215,7 @@ jobs: python -m pip install --upgrade pip wheel pip install -e . python -c "import autogen" - pip install coverage pytest-asyncio + pip install pytest-cov>=5 pytest-asyncio - name: Install packages for test when needed run: | pip install -e .[autobuild] @@ -229,8 +226,7 @@ jobs: AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }} OAI_CONFIG_LIST: ${{ secrets.OAI_CONFIG_LIST }} run: | - coverage run -a -m pytest test/agentchat/contrib/test_agent_builder.py - coverage xml + pytest test/agentchat/contrib/test_agent_builder.py - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: @@ -259,7 +255,7 @@ jobs: python -m pip install --upgrade pip wheel pip install -e .[websurfer] python -c "import autogen" - pip install coverage pytest + pip install pytest-cov>=5 - name: Coverage env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -268,93 +264,90 @@ jobs: OAI_CONFIG_LIST: ${{ secrets.OAI_CONFIG_LIST }} BING_API_KEY: ${{ secrets.BING_API_KEY }} run: | - coverage run -a -m pytest test/agentchat/contrib/test_web_surfer.py - coverage xml + pytest test/agentchat/contrib/test_web_surfer.py - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: file: ./coverage.xml flags: unittests ContextHandling: - strategy: - matrix: - os: [ubuntu-latest] - python-version: ["3.11"] - runs-on: ${{ matrix.os }} - environment: openai1 - steps: - # checkout to pr branch - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install packages and dependencies - run: | - docker --version - python -m pip install --upgrade pip wheel - pip install -e . - python -c "import autogen" - pip install coverage pytest - - name: Coverage - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} - AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }} - OAI_CONFIG_LIST: ${{ secrets.OAI_CONFIG_LIST }} - BING_API_KEY: ${{ secrets.BING_API_KEY }} - run: | - coverage run -a -m pytest test/agentchat/contrib/capabilities/test_context_handling.py - coverage xml - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - file: ./coverage.xml - flags: unittests + strategy: + matrix: + os: [ubuntu-latest] + python-version: ["3.11"] + runs-on: ${{ matrix.os }} + environment: openai1 + steps: + # checkout to pr branch + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install packages and dependencies + run: | + docker --version + python -m pip install --upgrade pip wheel + pip install -e . + python -c "import autogen" + pip install pytest-cov>=5 + - name: Coverage + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} + AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }} + OAI_CONFIG_LIST: ${{ secrets.OAI_CONFIG_LIST }} + BING_API_KEY: ${{ secrets.BING_API_KEY }} + run: | + pytest test/agentchat/contrib/capabilities/test_context_handling.py + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: unittests ImageGen: - strategy: - matrix: - os: [ubuntu-latest] - python-version: ["3.12"] - runs-on: ${{ matrix.os }} - environment: openai1 - steps: - # checkout to pr branch - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install packages and dependencies - run: | - docker --version - python -m pip install --upgrade pip wheel - pip install -e .[lmm] - python -c "import autogen" - pip install coverage pytest - - name: Coverage - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - run: | - coverage run -a -m pytest test/agentchat/contrib/capabilities/test_image_generation_capability.py - coverage xml - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - file: ./coverage.xml - flags: unittests + strategy: + matrix: + os: [ubuntu-latest] + python-version: ["3.12"] + runs-on: ${{ matrix.os }} + environment: openai1 + steps: + # checkout to pr branch + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install packages and dependencies + run: | + docker --version + python -m pip install --upgrade pip wheel + pip install -e .[lmm] + python -c "import autogen" + pip install pytest-cov>=5 + - name: Coverage + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: | + pytest test/agentchat/contrib/capabilities/test_image_generation_capability.py + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: unittests AgentOptimizer: strategy: matrix: - os: [ ubuntu-latest ] - python-version: [ "3.11" ] + os: [ubuntu-latest] + python-version: ["3.11"] runs-on: ${{ matrix.os }} environment: openai1 steps: @@ -373,7 +366,7 @@ jobs: python -m pip install --upgrade pip wheel pip install -e . python -c "import autogen" - pip install coverage pytest + pip install pytest-cov>=5 - name: Coverage env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -381,8 +374,7 @@ jobs: AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }} OAI_CONFIG_LIST: ${{ secrets.OAI_CONFIG_LIST }} run: | - coverage run -a -m pytest test/agentchat/contrib/test_agent_optimizer.py - coverage xml + pytest test/agentchat/contrib/test_agent_optimizer.py - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: diff --git a/.github/workflows/contrib-tests.yml b/.github/workflows/contrib-tests.yml index 46c8433e1f7..d36a9d52e69 100644 --- a/.github/workflows/contrib-tests.yml +++ b/.github/workflows/contrib-tests.yml @@ -41,7 +41,7 @@ jobs: - name: Install packages and dependencies for all tests run: | python -m pip install --upgrade pip wheel - pip install pytest + pip install pytest-cov>=5 - name: Install qdrant_client when python-version is 3.10 if: matrix.python-version == '3.10' run: | @@ -57,9 +57,7 @@ jobs: fi - name: Coverage run: | - pip install coverage>=5.3 - coverage run -a -m pytest test/test_retrieve_utils.py test/agentchat/contrib/retrievechat test/agentchat/contrib/vectordb --skip-openai - coverage xml + pytest test/test_retrieve_utils.py test/agentchat/contrib/retrievechat/test_retrievechat.py test/agentchat/contrib/retrievechat/test_qdrant_retrievechat.py test/agentchat/contrib/vectordb --skip-openai - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: @@ -119,9 +117,8 @@ jobs: echo "AUTOGEN_USE_DOCKER=False" >> $GITHUB_ENV - name: Coverage run: | - pip install coverage>=5.3 - coverage run -a -m pytest test/test_retrieve_utils.py test/agentchat/contrib/retrievechat test/agentchat/contrib/vectordb --skip-openai - coverage xml + pip install pytest-cov>=5 + pytest test/test_retrieve_utils.py test/agentchat/contrib/retrievechat test/agentchat/contrib/vectordb --skip-openai - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: @@ -144,7 +141,7 @@ jobs: - name: Install packages and dependencies for all tests run: | python -m pip install --upgrade pip wheel - pip install pytest + pip install pytest-cov>=5 - name: Install packages and dependencies for Compression run: | pip install -e . @@ -156,9 +153,7 @@ jobs: fi - name: Coverage run: | - pip install coverage>=5.3 - coverage run -a -m pytest test/agentchat/contrib/test_compressible_agent.py --skip-openai - coverage xml + pytest test/agentchat/contrib/test_compressible_agent.py --skip-openai - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: @@ -181,7 +176,7 @@ jobs: - name: Install packages and dependencies for all tests run: | python -m pip install --upgrade pip wheel - pip install pytest + pip install pytest-cov>=5 - name: Install packages and dependencies for GPTAssistantAgent run: | pip install -e . @@ -193,9 +188,7 @@ jobs: fi - name: Coverage run: | - pip install coverage>=5.3 - coverage run -a -m pytest test/agentchat/contrib/test_gpt_assistant.py --skip-openai - coverage xml + pytest test/agentchat/contrib/test_gpt_assistant.py --skip-openai - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: @@ -218,7 +211,7 @@ jobs: - name: Install packages and dependencies for all tests run: | python -m pip install --upgrade pip wheel - pip install pytest + pip install pytest-cov>=5 - name: Install packages and dependencies for Teachability run: | pip install -e .[teachable] @@ -230,9 +223,7 @@ jobs: fi - name: Coverage run: | - pip install coverage>=5.3 - coverage run -a -m pytest test/agentchat/contrib/capabilities/test_teachable_agent.py --skip-openai - coverage xml + pytest test/agentchat/contrib/capabilities/test_teachable_agent.py --skip-openai - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: @@ -255,7 +246,7 @@ jobs: - name: Install packages and dependencies for all tests run: | python -m pip install --upgrade pip wheel - pip install pytest + pip install pytest-cov>=5 - name: Install packages and dependencies for WebSurfer run: | pip install -e .[websurfer] @@ -267,9 +258,7 @@ jobs: fi - name: Coverage run: | - pip install coverage>=5.3 - coverage run -a -m pytest test/test_browser_utils.py test/agentchat/contrib/test_web_surfer.py --skip-openai - coverage xml + pytest test/test_browser_utils.py test/agentchat/contrib/test_web_surfer.py --skip-openai - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: @@ -294,7 +283,7 @@ jobs: - name: Install packages and dependencies for all tests run: | python -m pip install --upgrade pip wheel - pip install pytest + pip install pytest-cov>=5 - name: Install packages and dependencies for LMM run: | pip install -e .[lmm] @@ -306,9 +295,11 @@ jobs: fi - name: Coverage run: | - pip install coverage>=5.3 - coverage run -a -m pytest test/agentchat/contrib/test_img_utils.py test/agentchat/contrib/test_lmm.py test/agentchat/contrib/test_llava.py test/agentchat/contrib/capabilities/test_image_generation_capability.py test/agentchat/contrib/capabilities/test_vision_capability.py --skip-openai - coverage xml + pytest test/agentchat/contrib/test_img_utils.py test/agentchat/contrib/test_lmm.py test/agentchat/contrib/test_llava.py test/agentchat/contrib/capabilities/test_vision_capability.py --skip-openai + - name: Image Gen Coverage + if: ${{ matrix.os != 'windows-2019' && matrix.python-version != '3.12' }} + run: | + pytest test/agentchat/contrib/capabilities/test_image_generation_capability.py --skip-openai - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: @@ -336,7 +327,7 @@ jobs: - name: Install packages and dependencies for all tests run: | python -m pip install --upgrade pip wheel - pip install pytest + pip install pytest-cov>=5 - name: Install packages and dependencies for Gemini run: | pip install -e .[gemini,test] @@ -348,8 +339,7 @@ jobs: fi - name: Coverage run: | - coverage run -a -m pytest test/oai/test_gemini.py --skip-openai - coverage xml + pytest test/oai/test_gemini.py --skip-openai - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: @@ -372,7 +362,7 @@ jobs: - name: Install packages and dependencies for all tests run: | python -m pip install --upgrade pip wheel - pip install pytest + pip install pytest-cov>=5 - name: Install packages and dependencies for Context Handling run: | pip install -e . @@ -384,9 +374,7 @@ jobs: fi - name: Coverage run: | - pip install coverage>=5.3 - coverage run -a -m pytest test/agentchat/contrib/capabilities/test_context_handling.py --skip-openai - coverage xml + pytest test/agentchat/contrib/capabilities/test_context_handling.py --skip-openai - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: @@ -409,7 +397,7 @@ jobs: - name: Install packages and dependencies for all tests run: | python -m pip install --upgrade pip wheel - pip install pytest + pip install pytest-cov>=5 - name: Install packages and dependencies for Transform Messages run: | pip install -e . @@ -421,9 +409,7 @@ jobs: fi - name: Coverage run: | - pip install coverage>=5.3 - coverage run -a -m pytest test/agentchat/contrib/capabilities/test_transform_messages.py --skip-openai - coverage xml + pytest test/agentchat/contrib/capabilities/test_transform_messages.py --skip-openai - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: diff --git a/.github/workflows/openai.yml b/.github/workflows/openai.yml index d2780eea542..a9ab8e9e0c5 100644 --- a/.github/workflows/openai.yml +++ b/.github/workflows/openai.yml @@ -13,7 +13,8 @@ on: - "notebook/agentchat_function_call.ipynb" - "notebook/agentchat_groupchat_finite_state_machine.ipynb" - ".github/workflows/openai.yml" -permissions: {} +permissions: + {} # actions: read # checks: read # contents: read @@ -49,7 +50,7 @@ jobs: python -m pip install --upgrade pip wheel pip install -e. python -c "import autogen" - pip install coverage pytest-asyncio + pip install pytest-cov>=5 pytest-asyncio - name: Install packages for test when needed if: matrix.python-version == '3.9' run: | @@ -63,8 +64,7 @@ jobs: AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }} OAI_CONFIG_LIST: ${{ secrets.OAI_CONFIG_LIST }} run: | - coverage run -a -m pytest test --ignore=test/agentchat/contrib --durations=10 --durations-min=1.0 - coverage xml + pytest test --ignore=test/agentchat/contrib --durations=10 --durations-min=1.0 - name: Coverage and check notebook outputs if: matrix.python-version != '3.9' env: @@ -75,8 +75,7 @@ jobs: OAI_CONFIG_LIST: ${{ secrets.OAI_CONFIG_LIST }} run: | pip install nbconvert nbformat ipykernel - coverage run -a -m pytest test/test_notebook.py --durations=10 --durations-min=1.0 - coverage xml + pytest test/test_notebook.py --durations=10 --durations-min=1.0 cat "$(pwd)/test/executed_openai_notebook_output.txt" - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 diff --git a/.github/workflows/samples-tools-tests.yml b/.github/workflows/samples-tools-tests.yml index af7dc6c4743..e774e5cb0b1 100644 --- a/.github/workflows/samples-tools-tests.yml +++ b/.github/workflows/samples-tools-tests.yml @@ -37,7 +37,7 @@ jobs: run: | python -m pip install --upgrade pip wheel pip install -e . - pip install pytest + pip install pytest-cov>=5 - name: Set AUTOGEN_USE_DOCKER based on OS shell: bash run: | diff --git a/.github/workflows/type-check.yml b/.github/workflows/type-check.yml index f6896d1145d..c66fb6ad7b1 100644 --- a/.github/workflows/type-check.yml +++ b/.github/workflows/type-check.yml @@ -1,6 +1,6 @@ name: Type check # see: https://help.github.com/en/actions/reference/events-that-trigger-workflows -on: # Trigger the workflow on pull request or merge +on: # Trigger the workflow on pull request or merge pull_request: merge_group: types: [checks_requested] @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: ${{ matrix.version }} + python-version: ${{ matrix.version }} # All additional modules should be defined in setup.py - run: pip install ".[types]" # Any additional configuration should be defined in pyproject.toml diff --git a/autogen/cache/cache_factory.py b/autogen/cache/cache_factory.py index 437893570b4..7c9d71884cb 100644 --- a/autogen/cache/cache_factory.py +++ b/autogen/cache/cache_factory.py @@ -1,4 +1,5 @@ import logging +import os from typing import Any, Dict, Optional, Union from .abstract_cache_base import AbstractCache @@ -74,4 +75,5 @@ def cache_factory( logging.warning("CosmosDBCache is not available. Fallback to DiskCache.") # Default to DiskCache if neither Redis nor Cosmos DB configurations are provided - return DiskCache(f"./{cache_path_root}/{seed}") + path = os.path.join(cache_path_root, str(seed)) + return DiskCache(os.path.join(".", path)) diff --git a/autogen/code_utils.py b/autogen/code_utils.py index 5057c8615ea..e556497388f 100644 --- a/autogen/code_utils.py +++ b/autogen/code_utils.py @@ -281,7 +281,7 @@ def in_docker_container() -> bool: return os.path.exists("/.dockerenv") -def decide_use_docker(use_docker) -> bool: +def decide_use_docker(use_docker: Optional[bool]) -> Optional[bool]: if use_docker is None: env_var_use_docker = os.environ.get("AUTOGEN_USE_DOCKER", "True") diff --git a/autogen/oai/openai_utils.py b/autogen/oai/openai_utils.py index 25ac8dae298..7e738b7bd61 100644 --- a/autogen/oai/openai_utils.py +++ b/autogen/oai/openai_utils.py @@ -692,7 +692,11 @@ def detect_gpt_assistant_api_version() -> str: def create_gpt_vector_store(client: OpenAI, name: str, fild_ids: List[str]) -> Any: """Create a openai vector store for gpt assistant""" - vector_store = client.beta.vector_stores.create(name=name) + try: + vector_store = client.beta.vector_stores.create(name=name) + except Exception as e: + raise AttributeError(f"Failed to create vector store, please install the latest OpenAI python package: {e}") + # poll the status of the file batch for completion. batch = client.beta.vector_stores.file_batches.create_and_poll(vector_store_id=vector_store.id, file_ids=fild_ids) diff --git a/pyproject.toml b/pyproject.toml index d1851339743..7981ef4b43d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,10 +4,8 @@ description-file = "README.md" [tool.pytest.ini_options] -addopts = '-m "not conda"' -markers = [ - "conda: test related to conda forge distribution" -] +addopts = '--cov=. --cov-append --cov-branch --cov-report=xml -m "not conda"' +markers = ["conda: test related to conda forge distribution"] [tool.black] # https://github.com/psf/black @@ -16,28 +14,20 @@ exclude = "(.eggs|.git|.hg|.mypy_cache|.venv|_build|buck-out|build|dist)" [tool.ruff] - line-length = 120 [tool.ruff.lint] - - # Enable Pyflakes `E` and `F` codes by default. select = [ - "E", "W", # see: https://pypi.org/project/pycodestyle - "F", # see: https://pypi.org/project/pyflakes -# "D", # see: https://pypi.org/project/pydocstyle -# "N", # see: https://pypi.org/project/pep8-naming -# "S", # see: https://pypi.org/project/flake8-bandit - "I", # see: https://pypi.org/project/isort/ -] - -ignore = [ - "E501", - "F401", - "F403", - "C901", + "E", + "W", # see: https://pypi.org/project/pycodestyle + "F", # see: https://pypi.org/project/pyflakes + # "D", # see: https://pypi.org/project/pydocstyle + # "N", # see: https://pypi.org/project/pep8-naming + # "S", # see: https://pypi.org/project/flake8-bandit + "I", # see: https://pypi.org/project/isort/ ] +ignore = ["E501", "F401", "F403", "C901"] # Exclude a variety of commonly ignored directories. exclude = [ @@ -50,7 +40,7 @@ exclude = [ "build", "dist", "docs", - # This file needs to be either upgraded or removed and therefore should be + # This file needs to be either upgraded or removed and therefore should be # ignore from type checking for now "math_utils\\.py$", "**/cap/py/autogencap/proto/*", @@ -63,7 +53,6 @@ unfixable = ["F401"] max-complexity = 10 [tool.mypy] - files = [ "autogen/logger", "autogen/exception_utils.py", @@ -76,12 +65,12 @@ files = [ "test/test_function_utils.py", "test/io", ] - exclude = [ "autogen/math_utils\\.py", "autogen/oai/completion\\.py", "autogen/agentchat/contrib/compressible_agent\\.py", "autogen/agentchat/contrib/math_user_proxy_agent.py", + "autogen/oai/openai_utils.py", ] strict = true @@ -89,9 +78,7 @@ python_version = "3.8" ignore_missing_imports = true install_types = true non_interactive = true -plugins = [ - "pydantic.mypy" -] +plugins = ["pydantic.mypy"] # remove after all files in the repo are fixed follow_imports = "silent" diff --git a/setup.py b/setup.py index 870a10899ec..a93f1de07aa 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ __version__ = version["__version__"] install_requires = [ - "openai>=1.23.3", + "openai>=1.3", "diskcache", "termcolor", "flaml", @@ -39,11 +39,11 @@ extra_require = { "test": [ - "coverage>=5.3", "ipykernel", "nbconvert", "nbformat", "pre-commit", + "pytest-cov>=5", "pytest-asyncio", "pytest>=6.1.1,<8", "pandas", diff --git a/test/cache/test_cache.py b/test/cache/test_cache.py index d01b1cf4952..05331c165b5 100755 --- a/test/cache/test_cache.py +++ b/test/cache/test_cache.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 -m pytest import unittest -from typing import Optional, TypedDict, Union from unittest.mock import ANY, MagicMock, patch try: from azure.cosmos import CosmosClient + + skip_azure = False except ImportError: - CosmosClient = None + CosmosClient = object + skip_azure = True from autogen.cache.cache import Cache -from autogen.cache.cosmos_db_cache import CosmosDBConfig class TestCache(unittest.TestCase): @@ -37,6 +38,7 @@ def test_redis_cache_initialization(self, mock_cache_factory): mock_cache_factory.assert_called() @patch("autogen.cache.cache_factory.CacheFactory.cache_factory", return_value=MagicMock()) + @unittest.skipIf(skip_azure, "requires azure.cosmos") def test_cosmosdb_cache_initialization(self, mock_cache_factory): cache = Cache(self.cosmos_config) self.assertIsInstance(cache.cache, MagicMock) @@ -65,6 +67,7 @@ def context_manager_common(self, config): def test_redis_context_manager(self): self.context_manager_common(self.redis_config) + @unittest.skipIf(skip_azure, "requires azure.cosmos") def test_cosmos_context_manager(self): self.context_manager_common(self.cosmos_config) @@ -83,6 +86,7 @@ def get_set_common(self, config): def test_redis_get_set(self): self.get_set_common(self.redis_config) + @unittest.skipIf(skip_azure, "requires azure.cosmos") def test_cosmos_get_set(self): self.get_set_common(self.cosmos_config) @@ -96,6 +100,7 @@ def close_common(self, config): def test_redis_close(self): self.close_common(self.redis_config) + @unittest.skipIf(skip_azure, "requires azure.cosmos") def test_cosmos_close(self): self.close_common(self.cosmos_config) diff --git a/test/cache/test_cosmos_db_cache.py b/test/cache/test_cosmos_db_cache.py index f89a4c96cf4..80b97bf57ed 100644 --- a/test/cache/test_cosmos_db_cache.py +++ b/test/cache/test_cosmos_db_cache.py @@ -4,18 +4,28 @@ import unittest from unittest.mock import MagicMock, patch -from azure.cosmos.exceptions import CosmosResourceNotFoundError +try: + from azure.cosmos.exceptions import CosmosResourceNotFoundError -from autogen.cache.cosmos_db_cache import CosmosDBCache + from autogen.cache.cosmos_db_cache import CosmosDBCache + + skip_test = False +except ImportError: + CosmosResourceNotFoundError = Exception + CosmosDBCache = object + skip_test = True class TestCosmosDBCache(unittest.TestCase): def setUp(self): - self.seed = "42" - self.connection_string = "AccountEndpoint=https://example.documents.azure.com:443/;" - self.database_id = "autogen_cache" - self.container_id = "TestContainer" - self.client = MagicMock() + if skip_test: + self.skipTest("requires azure.cosmos") + else: + self.seed = "42" + self.connection_string = "AccountEndpoint=https://example.documents.azure.com:443/;" + self.database_id = "autogen_cache" + self.container_id = "TestContainer" + self.client = MagicMock() @patch("autogen.cache.cosmos_db_cache.CosmosClient.from_connection_string", return_value=MagicMock()) def test_init(self, mock_from_connection_string): diff --git a/test/coding/test_commandline_code_executor.py b/test/coding/test_commandline_code_executor.py index 20041c54b42..09562357235 100644 --- a/test/coding/test_commandline_code_executor.py +++ b/test/coding/test_commandline_code_executor.py @@ -7,7 +7,7 @@ import pytest from autogen.agentchat.conversable_agent import ConversableAgent -from autogen.code_utils import is_docker_running +from autogen.code_utils import decide_use_docker, is_docker_running from autogen.coding.base import CodeBlock, CodeExecutor from autogen.coding.docker_commandline_code_executor import DockerCommandLineCodeExecutor from autogen.coding.factory import CodeExecutorFactory @@ -16,9 +16,11 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "..")) from conftest import MOCK_OPEN_AI_API_KEY, skip_docker # noqa: E402 -if skip_docker or not is_docker_running(): +if skip_docker or not is_docker_running() or not decide_use_docker(use_docker=None): + skip_docker_test = True classes_to_test = [LocalCommandLineCodeExecutor] else: + skip_docker_test = False classes_to_test = [LocalCommandLineCodeExecutor, DockerCommandLineCodeExecutor] UNIX_SHELLS = ["bash", "sh", "shell"] @@ -70,7 +72,7 @@ def test_create_local() -> None: @pytest.mark.skipif( - skip_docker or not is_docker_running(), + skip_docker_test, reason="docker is not running or requested to skip docker tests", ) def test_create_docker() -> None: @@ -99,7 +101,6 @@ def test_commandline_executor_execute_code(cls, py_variant) -> None: @pytest.mark.parametrize("py_variant", PYTHON_VARIANTS) def _test_execute_code(py_variant, executor: CodeExecutor) -> None: - # Test single code block. code_blocks = [CodeBlock(code="import sys; print('hello world!')", language=py_variant)] code_result = executor.execute_code_blocks(code_blocks) @@ -159,7 +160,6 @@ def test_local_commandline_code_executor_save_files_only() -> None: def _test_save_files(executor: CodeExecutor, save_file_only: bool) -> None: - def _check_output(code_result: CodeBlock, expected_output: str) -> None: if save_file_only: return expected_output not in code_result.output @@ -243,7 +243,7 @@ def test_local_commandline_code_executor_restart() -> None: # This is kind of hard to test because each exec is a new env @pytest.mark.skipif( - skip_docker or not is_docker_running(), + skip_docker_test, reason="docker is not running or requested to skip docker tests", ) def test_docker_commandline_code_executor_restart() -> None: diff --git a/test/oai/test_client_stream.py b/test/oai/test_client_stream.py index 9e85bff7039..456a8fe761e 100755 --- a/test/oai/test_client_stream.py +++ b/test/oai/test_client_stream.py @@ -260,29 +260,22 @@ def test_chat_tools_stream() -> None: ] client = OpenAIWrapper(config_list=config_list) response = client.create( - # the intention is to trigger two tool invocations as a response to a single message - messages=[{"role": "user", "content": "What's the weather like today in San Francisco and New York?"}], + messages=[{"role": "user", "content": "What's the weather like today in San Francisco?"}], tools=tools, stream=True, ) - print(f"{response=}") - print(f"{type(response)=}") - print(f"{client.extract_text_or_completion_object(response)=}") # check response choices = response.choices assert isinstance(choices, list) - assert len(choices) == 1 + assert len(choices) > 0 + choice = choices[0] assert choice.finish_reason == "tool_calls" + message = choice.message tool_calls = message.tool_calls assert isinstance(tool_calls, list) - assert len(tool_calls) == 2 - arguments = [tool_call.function.arguments for tool_call in tool_calls] - locations = [json.loads(argument)["location"] for argument in arguments] - print(f"{locations=}") - assert any(["San Francisco" in location for location in locations]) - assert any(["New York" in location for location in locations]) + assert len(tool_calls) > 0 @pytest.mark.skipif(skip, reason="openai>=1 not installed") diff --git a/test/test_code_utils.py b/test/test_code_utils.py index a4c6cf697cc..d6084f9b029 100755 --- a/test/test_code_utils.py +++ b/test/test_code_utils.py @@ -30,6 +30,11 @@ OAI_CONFIG_LIST = "OAI_CONFIG_LIST" here = os.path.abspath(os.path.dirname(__file__)) +if skip_docker or not is_docker_running() or not decide_use_docker(use_docker=None): + skip_docker_test = True +else: + skip_docker_test = False + # def test_find_code(): # try: @@ -302,10 +307,7 @@ def scrape(url): assert len(codeblocks) == 1 and codeblocks[0] == ("", "source setup.sh") -@pytest.mark.skipif( - skip_docker or not is_docker_running(), - reason="docker is not running or requested to skip docker tests", -) +@pytest.mark.skipif(skip_docker_test, reason="docker is not running or requested to skip docker tests") def test_execute_code(use_docker=True): # Test execute code and save the code to a file. with tempfile.TemporaryDirectory() as tempdir: @@ -369,10 +371,7 @@ def test_execute_code(use_docker=True): assert isinstance(image, str) -@pytest.mark.skipif( - skip_docker or not is_docker_running(), - reason="docker is not running or requested to skip docker tests", -) +@pytest.mark.skipif(skip_docker_test, reason="docker is not running or requested to skip docker tests") def test_execute_code_with_custom_filename_on_docker(): with tempfile.TemporaryDirectory() as tempdir: filename = "codetest.py" @@ -387,7 +386,7 @@ def test_execute_code_with_custom_filename_on_docker(): @pytest.mark.skipif( - skip_docker or not is_docker_running(), + skip_docker_test, reason="docker is not running or requested to skip docker tests", ) def test_execute_code_with_misformed_filename_on_docker(): diff --git a/website/docs/contributor-guide/tests.md b/website/docs/contributor-guide/tests.md index 69092cc4d68..c5eabb90732 100644 --- a/website/docs/contributor-guide/tests.md +++ b/website/docs/contributor-guide/tests.md @@ -44,12 +44,12 @@ pytest test --skip-openai --skip-docker ## Coverage -Any code you commit should not decrease coverage. To run all unit tests, install the [test] option: +Any code you commit should not decrease coverage. To ensure your code maintains or increases coverage, use the following commands after installing the required test dependencies: ```bash -pip install -e."[test]" -coverage run -m pytest test +pip install -e ."[test]" + +pytest test --cov-report=html ``` -Then you can see the coverage report by -`coverage report -m` or `coverage html`. +Pytest generated a code coverage report and created a htmlcov directory containing an index.html file and other related files. Open index.html in any web browser to visualize and navigate through the coverage data interactively. This interactive visualization allows you to identify uncovered lines and review coverage statistics for individual files. From 5a007e0d47da4f7a0db1cc51777a0310e06b2d52 Mon Sep 17 00:00:00 2001 From: Chi Wang Date: Sun, 28 Apr 2024 18:35:12 -0700 Subject: [PATCH 16/30] tags for config (#2539) --- autogen/version.py | 2 +- notebook/agentchat_function_call_async.ipynb | 4 ++-- notebook/agentchat_groupchat_stateflow.ipynb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/autogen/version.py b/autogen/version.py index 3b9e925d165..be2d7c2ffe3 100644 --- a/autogen/version.py +++ b/autogen/version.py @@ -1 +1 @@ -__version__ = "0.2.26" +__version__ = "0.2.27" diff --git a/notebook/agentchat_function_call_async.ipynb b/notebook/agentchat_function_call_async.ipynb index 78a8d191915..57233547ebc 100644 --- a/notebook/agentchat_function_call_async.ipynb +++ b/notebook/agentchat_function_call_async.ipynb @@ -44,7 +44,7 @@ "import autogen\n", "from autogen.cache import Cache\n", "\n", - "config_list = autogen.config_list_from_json(env_or_file=\"OAI_CONFIG_LIST\")" + "config_list = autogen.config_list_from_json(env_or_file=\"OAI_CONFIG_LIST\", filter_dict={\"tags\": [\"tool\"]})" ] }, { @@ -384,7 +384,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.10.14" } }, "nbformat": 4, diff --git a/notebook/agentchat_groupchat_stateflow.ipynb b/notebook/agentchat_groupchat_stateflow.ipynb index c824046f73a..b8810d2fb63 100644 --- a/notebook/agentchat_groupchat_stateflow.ipynb +++ b/notebook/agentchat_groupchat_stateflow.ipynb @@ -43,7 +43,7 @@ "config_list = autogen.config_list_from_json(\n", " \"OAI_CONFIG_LIST\",\n", " filter_dict={\n", - " \"model\": [\"gpt-4\", \"gpt-4-1106-preview\"],\n", + " \"tags\": [\"gpt-4\", \"gpt-4-32k\"],\n", " },\n", ")" ] From 11a43421e349113f699b9b706e6515d7023bf879 Mon Sep 17 00:00:00 2001 From: giorgossideris <56915448+giorgossideris@users.noreply.github.com> Date: Mon, 29 Apr 2024 05:11:16 +0300 Subject: [PATCH 17/30] Min tokens in token limiter (#2400) * Add minimum token threshold in MessageHistoryLimiter * Update transforms tests for the threshold * Move min_threshold_tokens from Message to Token Limiter * Optimize _check_tokens_threshold Co-authored-by: Wael Karkoub * Apply requested changes (renaming, phrasing, validations) * Fix format * Fix _check_tokens_threshold logic * Update docs and notebook * Improve phrasing * Add min_tokens example in notebook * Add min_tokens example in website docs * Add min_tokens example in notebook * Update website docs to be in sync with get_logs change --------- Co-authored-by: Wael Karkoub Co-authored-by: Chi Wang --- .../contrib/capabilities/transforms.py | 45 +++- notebook/agentchat_transform_messages.ipynb | 226 ++++++++++++------ .../contrib/capabilities/test_transforms.py | 115 +++++++-- website/docs/topics/long_contexts.md | 117 +++++---- 4 files changed, 351 insertions(+), 152 deletions(-) diff --git a/autogen/agentchat/contrib/capabilities/transforms.py b/autogen/agentchat/contrib/capabilities/transforms.py index 6dc1d59fe9c..279faed8c9d 100644 --- a/autogen/agentchat/contrib/capabilities/transforms.py +++ b/autogen/agentchat/contrib/capabilities/transforms.py @@ -51,8 +51,7 @@ class MessageHistoryLimiter: def __init__(self, max_messages: Optional[int] = None): """ Args: - max_messages (None or int): Maximum number of messages to keep in the context. - Must be greater than 0 if not None. + max_messages Optional[int]: Maximum number of messages to keep in the context. Must be greater than 0 if not None. """ self._validate_max_messages(max_messages) self._max_messages = max_messages @@ -70,6 +69,7 @@ def apply_transform(self, messages: List[Dict]) -> List[Dict]: Returns: List[Dict]: A new list containing the most recent messages up to the specified maximum. """ + if self._max_messages is None: return messages @@ -108,13 +108,15 @@ class MessageTokenLimiter: The truncation process follows these steps in order: - 1. Messages are processed in reverse order (newest to oldest). - 2. Individual messages are truncated based on max_tokens_per_message. For multimodal messages containing both text + 1. The minimum tokens threshold (`min_tokens`) is checked (0 by default). If the total number of tokens in messages + are less than this threshold, then the messages are returned as is. In other case, the following process is applied. + 2. Messages are processed in reverse order (newest to oldest). + 3. Individual messages are truncated based on max_tokens_per_message. For multimodal messages containing both text and other types of content, only the text content is truncated. - 3. The overall conversation history is truncated based on the max_tokens limit. Once the accumulated token count + 4. The overall conversation history is truncated based on the max_tokens limit. Once the accumulated token count exceeds this limit, the current message being processed get truncated to meet the total token count and any remaining messages get discarded. - 4. The truncated conversation history is reconstructed by prepending the messages to a new list to preserve the + 5. The truncated conversation history is reconstructed by prepending the messages to a new list to preserve the original message order. """ @@ -122,6 +124,7 @@ def __init__( self, max_tokens_per_message: Optional[int] = None, max_tokens: Optional[int] = None, + min_tokens: Optional[int] = None, model: str = "gpt-3.5-turbo-0613", ): """ @@ -130,11 +133,14 @@ def __init__( Must be greater than or equal to 0 if not None. max_tokens (Optional[int]): Maximum number of tokens to keep in the chat history. Must be greater than or equal to 0 if not None. + min_tokens (Optional[int]): Minimum number of tokens in messages to apply the transformation. + Must be greater than or equal to 0 if not None. model (str): The target OpenAI model for tokenization alignment. """ self._model = model self._max_tokens_per_message = self._validate_max_tokens(max_tokens_per_message) self._max_tokens = self._validate_max_tokens(max_tokens) + self._min_tokens = self._validate_min_tokens(min_tokens, max_tokens) def apply_transform(self, messages: List[Dict]) -> List[Dict]: """Applies token truncation to the conversation history. @@ -147,6 +153,11 @@ def apply_transform(self, messages: List[Dict]) -> List[Dict]: """ assert self._max_tokens_per_message is not None assert self._max_tokens is not None + assert self._min_tokens is not None + + # if the total number of tokens in the messages is less than the min_tokens, return the messages as is + if not self._are_min_tokens_reached(messages): + return messages temp_messages = copy.deepcopy(messages) processed_messages = [] @@ -194,6 +205,19 @@ def get_logs(self, pre_transform_messages: List[Dict], post_transform_messages: return logs_str, True return "No tokens were truncated.", False + def _are_min_tokens_reached(self, messages: List[Dict]) -> bool: + """ + Returns True if no minimum tokens restrictions are applied. + + Either if the total number of tokens in the messages is greater than or equal to the `min_theshold_tokens`, + or no minimum tokens threshold is set. + """ + if not self._min_tokens: + return True + + messages_tokens = sum(_count_tokens(msg["content"]) for msg in messages if "content" in msg) + return messages_tokens >= self._min_tokens + def _truncate_str_to_tokens(self, contents: Union[str, List], n_tokens: int) -> Union[str, List]: if isinstance(contents, str): return self._truncate_tokens(contents, n_tokens) @@ -244,6 +268,15 @@ def _validate_max_tokens(self, max_tokens: Optional[int] = None) -> Optional[int return max_tokens if max_tokens is not None else sys.maxsize + def _validate_min_tokens(self, min_tokens: int, max_tokens: int) -> int: + if min_tokens is None: + return 0 + if min_tokens < 0: + raise ValueError("min_tokens must be None or greater than or equal to 0.") + if max_tokens is not None and min_tokens > max_tokens: + raise ValueError("min_tokens must not be more than max_tokens.") + return min_tokens + def _count_tokens(content: Union[str, List[Dict[str, Any]]]) -> int: token_count = 0 diff --git a/notebook/agentchat_transform_messages.ipynb b/notebook/agentchat_transform_messages.ipynb index ab8bc762fc7..d0216e05dd2 100644 --- a/notebook/agentchat_transform_messages.ipynb +++ b/notebook/agentchat_transform_messages.ipynb @@ -24,16 +24,15 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "id": "47773f79-c0fd-4993-bc6e-3d1a57690118", "metadata": {}, "outputs": [], "source": [ "import copy\n", - "import os\n", "import pprint\n", "import re\n", - "from typing import Dict, List\n", + "from typing import Dict, List, Tuple\n", "\n", "import autogen\n", "from autogen.agentchat.contrib.capabilities import transform_messages, transforms" @@ -41,7 +40,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "id": "9f09246b-a7d0-4238-b62c-1e72c7d815b3", "metadata": {}, "outputs": [], @@ -95,7 +94,7 @@ "Imagine a scenario where the LLM generates an extensive amount of text, surpassing the token limit imposed by your API provider. To address this issue, you can leverage `TransformMessages` along with its constituent transformations, `MessageHistoryLimiter` and `MessageTokenLimiter`.\n", "\n", "- `MessageHistoryLimiter`: You can restrict the total number of messages considered as context history. This transform is particularly useful when you want to limit the conversational context to a specific number of recent messages, ensuring efficient processing and response generation.\n", - "- `MessageTokenLimiter`: Enables you to cap the total number of tokens, either on a per-message basis or across the entire context history (or both). This transformation is invaluable when you need to adhere to strict token limits imposed by your API provider, preventing unnecessary costs or errors caused by exceeding the allowed token count." + "- `MessageTokenLimiter`: Enables you to cap the total number of tokens, either on a per-message basis or across the entire context history (or both). This transformation is invaluable when you need to adhere to strict token limits imposed by your API provider, preventing unnecessary costs or errors caused by exceeding the allowed token count. Additionally, a `min_tokens` threshold can be applied, ensuring that the transformation is only applied when the number of tokens is not less than the specified threshold." ] }, { @@ -109,7 +108,7 @@ "max_msg_transfrom = transforms.MessageHistoryLimiter(max_messages=3)\n", "\n", "# Limit the token limit per message to 10 tokens\n", - "token_limit_transform = transforms.MessageTokenLimiter(max_tokens_per_message=3)" + "token_limit_transform = transforms.MessageTokenLimiter(max_tokens_per_message=3, min_tokens=10)" ] }, { @@ -170,7 +169,6 @@ "name": "stdout", "output_type": "stream", "text": [ - "\u001b[33mTruncated 6 tokens. Tokens reduced from 15 to 9\u001b[0m\n", "[{'content': 'hello', 'role': 'user'},\n", " {'content': [{'text': 'there', 'type': 'text'}], 'role': 'assistant'},\n", " {'content': 'how', 'role': 'user'},\n", @@ -185,6 +183,40 @@ "pprint.pprint(processed_messages)" ] }, + { + "cell_type": "markdown", + "id": "86a98e08", + "metadata": {}, + "source": [ + "Also, the `min_tokens` threshold is set to 10, indicating that the transformation will not be applied if the total number of tokens in the messages is less than that. This is especially beneficial when the transformation should only occur after a certain number of tokens has been reached, such as in the context window of the model. An example is provided below." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "05c42ffc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[{'content': 'hello there, how are you?', 'role': 'user'},\n", + " {'content': [{'text': 'hello', 'type': 'text'}], 'role': 'assistant'}]\n" + ] + } + ], + "source": [ + "short_messages = [\n", + " {\"role\": \"user\", \"content\": \"hello there, how are you?\"},\n", + " {\"role\": \"assistant\", \"content\": [{\"type\": \"text\", \"text\": \"hello\"}]},\n", + "]\n", + "\n", + "processed_short_messages = token_limit_transform.apply_transform(copy.deepcopy(short_messages))\n", + "\n", + "pprint.pprint(processed_short_messages)" + ] + }, { "cell_type": "markdown", "id": "35fa2844-bd83-42ac-8275-959f093b7bc7", @@ -197,7 +229,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "80e53623-2830-41b7-8ae2-bf3668071657", "metadata": {}, "outputs": [ @@ -211,7 +243,7 @@ "\n", "--------------------------------------------------------------------------------\n", "Encountered an error with the base assistant\n", - "Error code: 429 - {'error': {'message': 'Request too large for gpt-3.5-turbo in organization org-U58JZBsXUVAJPlx2MtPYmdx1 on tokens per min (TPM): Limit 60000, Requested 1252546. The input or output tokens must be reduced in order to run successfully. Visit https://platform.openai.com/account/rate-limits to learn more.', 'type': 'tokens', 'param': None, 'code': 'rate_limit_exceeded'}}\n", + "Error code: 400 - {'error': {'message': \"This model's maximum context length is 16385 tokens. However, your messages resulted in 1009487 tokens. Please reduce the length of the messages.\", 'type': 'invalid_request_error', 'param': 'messages', 'code': 'context_length_exceeded'}}\n", "\n", "\n", "\n", @@ -220,38 +252,42 @@ "plot and save a graph of x^2 from -10 to 10\n", "\n", "--------------------------------------------------------------------------------\n", - "\u001b[33mTruncated 3804 tokens. Tokens reduced from 4019 to 215\u001b[0m\n", + "\u001b[33mRemoved 1991 messages. Number of messages reduced from 2001 to 10.\u001b[0m\n", + "\u001b[33mTruncated 3804 tokens. Number of tokens reduced from 4019 to 215\u001b[0m\n", "\u001b[33massistant\u001b[0m (to user_proxy):\n", "\n", - "To plot the graph of \\( x^2 \\) from -10 to 10 and save it, we can use Python with the matplotlib library. Here is the code to achieve this:\n", - "\n", "```python\n", - "# filename: plot_graph.py\n", + "# filename: plot_x_squared.py\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "\n", - "x = np.linspace(-10, 10, 100)\n", + "# Generate an array of x values from -10 to 10\n", + "x = np.linspace(-10, 10, 400)\n", + "# Calculate the y values by squaring the x values\n", "y = x**2\n", "\n", + "# Create the plot\n", + "plt.figure()\n", "plt.plot(x, y)\n", + "\n", + "# Title and labels\n", + "plt.title('Graph of y = x^2')\n", "plt.xlabel('x')\n", - "plt.ylabel('x^2')\n", - "plt.title('Graph of x^2')\n", - "plt.grid(True)\n", - "plt.savefig('x_squared_graph.png')\n", + "plt.ylabel('y')\n", + "\n", + "# Save the plot as a file\n", + "plt.savefig('x_squared_plot.png')\n", + "\n", + "# Show the plot\n", "plt.show()\n", "```\n", "\n", - "After executing this code, you should see the graph of \\( x^2 \\) displayed and saved as `x_squared_graph.png`.\n", - "\n", - "Please make sure you have matplotlib installed. If not, you can install it using pip:\n", + "Please save the above code into a file named `plot_x_squared.py`. After saving the code, you can execute it to generate and save the graph of y = x^2 from -10 to 10. The graph will also be displayed to you and the file `x_squared_plot.png` will be created in the current directory. Make sure you have `matplotlib` and `numpy` libraries installed in your Python environment before executing the code. If they are not installed, you can install them using `pip`:\n", "\n", "```sh\n", - "pip install matplotlib\n", + "pip install matplotlib numpy\n", "```\n", "\n", - "Go ahead and execute the Python script provided above to plot and save the graph of \\( x^2 \\). Let me know if you encounter any issues.\n", - "\n", "--------------------------------------------------------------------------------\n", "\u001b[31m\n", ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", @@ -263,36 +299,83 @@ "Code output: \n", "Figure(640x480)\n", "\n", - "Requirement already satisfied: matplotlib in /home/wael/workspaces/autogen/.venv/lib/python3.11/site-packages (3.8.2)\n", - "Requirement already satisfied: contourpy>=1.0.1 in /home/wael/workspaces/autogen/.venv/lib/python3.11/site-packages (from matplotlib) (1.2.0)\n", - "Requirement already satisfied: cycler>=0.10 in /home/wael/workspaces/autogen/.venv/lib/python3.11/site-packages (from matplotlib) (0.12.1)\n", - "Requirement already satisfied: fonttools>=4.22.0 in /home/wael/workspaces/autogen/.venv/lib/python3.11/site-packages (from matplotlib) (4.48.1)\n", - "Requirement already satisfied: kiwisolver>=1.3.1 in /home/wael/workspaces/autogen/.venv/lib/python3.11/site-packages (from matplotlib) (1.4.5)\n", - "Requirement already satisfied: numpy<2,>=1.21 in /home/wael/workspaces/autogen/.venv/lib/python3.11/site-packages (from matplotlib) (1.26.4)\n", - "Requirement already satisfied: packaging>=20.0 in /home/wael/workspaces/autogen/.venv/lib/python3.11/site-packages (from matplotlib) (23.2)\n", - "Requirement already satisfied: pillow>=8 in /home/wael/workspaces/autogen/.venv/lib/python3.11/site-packages (from matplotlib) (10.2.0)\n", - "Requirement already satisfied: pyparsing>=2.3.1 in /home/wael/workspaces/autogen/.venv/lib/python3.11/site-packages (from matplotlib) (3.1.1)\n", - "Requirement already satisfied: python-dateutil>=2.7 in /home/wael/workspaces/autogen/.venv/lib/python3.11/site-packages (from matplotlib) (2.8.2)\n", - "Requirement already satisfied: six>=1.5 in /home/wael/workspaces/autogen/.venv/lib/python3.11/site-packages (from python-dateutil>=2.7->matplotlib) (1.16.0)\n", + "Requirement already satisfied: matplotlib in c:\\users\\bt314mc\\appdata\\local\\programs\\python\\python311\\lib\\site-packages (3.8.0)\n", + "Requirement already satisfied: numpy in c:\\users\\bt314mc\\appdata\\local\\programs\\python\\python311\\lib\\site-packages (1.26.0)\n", + "Requirement already satisfied: contourpy>=1.0.1 in c:\\users\\bt314mc\\appdata\\local\\programs\\python\\python311\\lib\\site-packages (from matplotlib) (1.1.1)\n", + "Requirement already satisfied: cycler>=0.10 in c:\\users\\bt314mc\\appdata\\local\\programs\\python\\python311\\lib\\site-packages (from matplotlib) (0.11.0)\n", + "Requirement already satisfied: fonttools>=4.22.0 in c:\\users\\bt314mc\\appdata\\local\\programs\\python\\python311\\lib\\site-packages (from matplotlib) (4.42.1)\n", + "Requirement already satisfied: kiwisolver>=1.0.1 in c:\\users\\bt314mc\\appdata\\local\\programs\\python\\python311\\lib\\site-packages (from matplotlib) (1.4.5)\n", + "Requirement already satisfied: packaging>=20.0 in c:\\users\\bt314mc\\appdata\\local\\programs\\python\\python311\\lib\\site-packages (from matplotlib) (23.2)\n", + "Requirement already satisfied: pillow>=6.2.0 in c:\\users\\bt314mc\\appdata\\local\\programs\\python\\python311\\lib\\site-packages (from matplotlib) (10.0.1)\n", + "Requirement already satisfied: pyparsing>=2.3.1 in c:\\users\\bt314mc\\appdata\\local\\programs\\python\\python311\\lib\\site-packages (from matplotlib) (3.1.1)\n", + "Requirement already satisfied: python-dateutil>=2.7 in c:\\users\\bt314mc\\appdata\\local\\programs\\python\\python311\\lib\\site-packages (from matplotlib) (2.8.2)\n", + "Requirement already satisfied: six>=1.5 in c:\\users\\bt314mc\\appdata\\local\\programs\\python\\python311\\lib\\site-packages (from python-dateutil>=2.7->matplotlib) (1.16.0)\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mRemoved 1993 messages. Number of messages reduced from 2003 to 10.\u001b[0m\n", + "\u001b[33mTruncated 3523 tokens. Number of tokens reduced from 3788 to 265\u001b[0m\n", + "\u001b[33massistant\u001b[0m (to user_proxy):\n", + "\n", + "It appears that the matplotlib library is already installed on your system, and the previous script started successfully but did not finish because the plotting code was incomplete.\n", + "\n", + "I will provide you with the full code to plot and save the graph of \\( x^2 \\) from -10 to 10.\n", + "\n", + "```python\n", + "# filename: plot_x_squared.py\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "# Generate an array of x values from -10 to 10\n", + "x = np.linspace(-10, 10, 400)\n", + "# Calculate the y values based on the x values\n", + "y = x**2\n", + "\n", + "# Create the plot\n", + "plt.figure(figsize=(8, 6))\n", + "plt.plot(x, y, label='y = x^2')\n", + "\n", + "# Add a title and labels\n", + "plt.title('Plot of y = x^2')\n", + "plt.xlabel('x')\n", + "plt.ylabel('y')\n", + "\n", + "# Add a legend\n", + "plt.legend()\n", + "\n", + "# Save the figure\n", + "plt.savefig('plot_x_squared.png')\n", + "\n", + "# Show the plot\n", + "plt.show()\n", + "```\n", + "\n", + "Please execute this Python code in its entirety. It will create a graph of \\( y = x^2 \\) with x values ranging from -10 to 10, and then it will save the graph as a PNG file named 'plot_x_squared.png' in the current working directory. It will also display the plot window with the graph.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[31m\n", + ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", + "\u001b[33muser_proxy\u001b[0m (to assistant):\n", + "\n", + "exitcode: 0 (execution succeeded)\n", + "Code output: \n", + "Figure(800x600)\n", "\n", "\n", "--------------------------------------------------------------------------------\n", - "\u001b[33mTruncated 3435 tokens. Tokens reduced from 3700 to 265\u001b[0m\n", + "\u001b[33mRemoved 1995 messages. Number of messages reduced from 2005 to 10.\u001b[0m\n", + "\u001b[33mTruncated 2802 tokens. Number of tokens reduced from 3086 to 284\u001b[0m\n", "\u001b[33massistant\u001b[0m (to user_proxy):\n", "\n", - "The graph has been successfully created and saved. You can find the graph as a file named \"x_squared_plot.png\" in the directory where you ran the script. You can open and view this file to see the plotted graph of \\(x^2\\) from -10 to 10.\n", + "It seems the graph has been generated, but the output doesn't tell us if the graph was saved. The expected behavior was to have a file saved in the current working directory. Can you please check in your current directory for a file named `plot_x_squared.png`? If it exists, then the task is complete.\n", "\n", - "TERMINATE\n", + "If you don't find the file, let me know, and I will troubleshoot further.\n", "\n", "--------------------------------------------------------------------------------\n" ] } ], "source": [ - "llm_config = {\n", - " \"config_list\": [{\"model\": \"gpt-3.5-turbo\", \"api_key\": os.environ.get(\"OPENAI_API_KEY\")}],\n", - "}\n", - "\n", "assistant_base = autogen.AssistantAgent(\n", " \"assistant\",\n", " llm_config=llm_config,\n", @@ -306,7 +389,7 @@ "context_handling = transform_messages.TransformMessages(\n", " transforms=[\n", " transforms.MessageHistoryLimiter(max_messages=10),\n", - " transforms.MessageTokenLimiter(max_tokens=1000, max_tokens_per_message=50),\n", + " transforms.MessageTokenLimiter(max_tokens=1000, max_tokens_per_message=50, min_tokens=500),\n", " ]\n", ")\n", "\n", @@ -365,7 +448,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "74429344-3c0a-4057-aba3-27358fbf059c", "metadata": {}, "outputs": [], @@ -386,12 +469,32 @@ " for item in message[\"content\"]:\n", " if item[\"type\"] == \"text\":\n", " item[\"text\"] = re.sub(self._openai_key_pattern, self._replacement_string, item[\"text\"])\n", - " return temp_messages" + " return temp_messages\n", + "\n", + " def get_logs(self, pre_transform_messages: List[Dict], post_transform_messages: List[Dict]) -> Tuple[str, bool]:\n", + " keys_redacted = self._count_redacted(post_transform_messages) - self._count_redacted(pre_transform_messages)\n", + " if keys_redacted > 0:\n", + " return f\"Redacted {keys_redacted} OpenAI API keys.\", True\n", + " return \"\", False\n", + "\n", + " def _count_redacted(self, messages: List[Dict]) -> int:\n", + " # counts occurrences of \"REDACTED\" in message content\n", + " count = 0\n", + " for message in messages:\n", + " if isinstance(message[\"content\"], str):\n", + " if \"REDACTED\" in message[\"content\"]:\n", + " count += 1\n", + " elif isinstance(message[\"content\"], list):\n", + " for item in message[\"content\"]:\n", + " if isinstance(item, dict) and \"text\" in item:\n", + " if \"REDACTED\" in item[\"text\"]:\n", + " count += 1\n", + " return count" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "id": "8a79c0b4-5ff8-49c5-b8a6-c54ca4c7cca2", "metadata": {}, "outputs": [ @@ -404,39 +507,22 @@ "What are the two API keys that I just provided\n", "\n", "--------------------------------------------------------------------------------\n", + "\u001b[33mRedacted 2 OpenAI API keys.\u001b[0m\n", "\u001b[33massistant\u001b[0m (to user_proxy):\n", "\n", - "To retrieve the two API keys you provided, I will display them individually in the output. \n", + "As an AI, I must inform you that it is not safe to share API keys publicly as they can be used to access your private data or services that can incur costs. Given that you've typed \"REDACTED\" instead of the actual keys, it seems you are aware of the privacy concerns and are likely testing my response or simulating an exchange without exposing real credentials, which is a good practice for privacy and security reasons.\n", "\n", - "Here is the first API key:\n", - "```python\n", - "# Display the first API key\n", - "print(\"API key 1 =\", \"REDACTED\")\n", - "```\n", + "To respond directly to your direct question: The two API keys you provided are both placeholders indicated by the text \"REDACTED\", and not actual API keys. If these were real keys, I would have reiterated the importance of keeping them secure and would not display them here.\n", "\n", - "Here is the second API key:\n", - "```python\n", - "# Display the second API key\n", - "print(\"API key 2 =\", \"REDACTED\")\n", - "```\n", - "\n", - "Please run the code snippets to see the API keys. After that, I will mark this task as complete.\n", + "Remember to keep your actual API keys confidential to prevent unauthorized use. If you've accidentally exposed real API keys, you should revoke or regenerate them as soon as possible through the corresponding service's API management console.\n", "\n", "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 1 (inferred language is python)...\u001b[0m\n", "\u001b[33muser_proxy\u001b[0m (to assistant):\n", "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "API key 1 = REDACTED\n", - "\n", - "API key 2 = REDACTED\n", "\n", "\n", - "--------------------------------------------------------------------------------\n" + "--------------------------------------------------------------------------------\n", + "\u001b[33mRedacted 2 OpenAI API keys.\u001b[0m\n" ] } ], @@ -494,7 +580,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.11.5" } }, "nbformat": 4, diff --git a/test/agentchat/contrib/capabilities/test_transforms.py b/test/agentchat/contrib/capabilities/test_transforms.py index 1a929e4c6ba..6d9441d53e6 100644 --- a/test/agentchat/contrib/capabilities/test_transforms.py +++ b/test/agentchat/contrib/capabilities/test_transforms.py @@ -20,7 +20,7 @@ def get_short_messages() -> List[Dict]: return [ {"role": "user", "content": "hello"}, {"role": "assistant", "content": [{"type": "text", "text": "there"}]}, - {"role": "user", "content": "how"}, + {"role": "user", "content": "how are you"}, ] @@ -38,7 +38,12 @@ def message_token_limiter() -> MessageTokenLimiter: return MessageTokenLimiter(max_tokens_per_message=3) -# MessageHistoryLimiter tests +@pytest.fixture +def message_token_limiter_with_threshold() -> MessageTokenLimiter: + return MessageTokenLimiter(max_tokens_per_message=1, min_tokens=10) + + +# MessageHistoryLimiter @pytest.mark.parametrize( @@ -71,7 +76,7 @@ def test_message_history_limiter_get_logs(message_history_limiter, messages, exp @pytest.mark.parametrize( "messages, expected_token_count, expected_messages_len", - [(get_long_messages(), 9, 5), (get_short_messages(), 3, 3), (get_no_content_messages(), 0, 2)], + [(get_long_messages(), 9, 5), (get_short_messages(), 5, 3), (get_no_content_messages(), 0, 2)], ) def test_message_token_limiter_apply_transform( message_token_limiter, messages, expected_token_count, expected_messages_len @@ -83,6 +88,20 @@ def test_message_token_limiter_apply_transform( assert len(transformed_messages) == expected_messages_len +@pytest.mark.parametrize( + "messages, expected_token_count, expected_messages_len", + [(get_long_messages(), 5, 5), (get_short_messages(), 5, 3), (get_no_content_messages(), 0, 2)], +) +def test_message_token_limiter_with_threshold_apply_transform( + message_token_limiter_with_threshold, messages, expected_token_count, expected_messages_len +): + transformed_messages = message_token_limiter_with_threshold.apply_transform(messages) + assert ( + sum(_count_tokens(msg["content"]) for msg in transformed_messages if "content" in msg) == expected_token_count + ) + assert len(transformed_messages) == expected_messages_len + + @pytest.mark.parametrize( "messages, expected_logs, expected_effect", [ @@ -102,21 +121,87 @@ def test_message_token_limiter_get_logs(message_token_limiter, messages, expecte if __name__ == "__main__": long_messages = get_long_messages() short_messages = get_short_messages() + no_content_messages = get_no_content_messages() message_history_limiter = MessageHistoryLimiter(max_messages=3) message_token_limiter = MessageTokenLimiter(max_tokens_per_message=3) + message_token_limiter_with_threshold = MessageTokenLimiter(max_tokens_per_message=1, min_tokens=10) + + # Test Parameters + message_history_limiter_apply_transform_parameters = { + "messages": [long_messages, short_messages, no_content_messages], + "expected_messages_len": [3, 3, 2], + } + + message_history_limiter_get_logs_parameters = { + "messages": [long_messages, short_messages, no_content_messages], + "expected_logs": [ + "Removed 2 messages. Number of messages reduced from 5 to 3.", + "No messages were removed.", + "No messages were removed.", + ], + "expected_effect": [True, False, False], + } + + message_token_limiter_apply_transform_parameters = { + "messages": [long_messages, short_messages, no_content_messages], + "expected_token_count": [9, 5, 0], + "expected_messages_len": [5, 3, 2], + } + + message_token_limiter_with_threshold_apply_transform_parameters = { + "messages": [long_messages, short_messages, no_content_messages], + "expected_token_count": [5, 5, 0], + "expected_messages_len": [5, 3, 2], + } + + message_token_limiter_get_logs_parameters = { + "messages": [long_messages, short_messages, no_content_messages], + "expected_logs": [ + "Truncated 6 tokens. Number of tokens reduced from 15 to 9", + "No tokens were truncated.", + "No tokens were truncated.", + ], + "expected_effect": [True, False, False], + } # Call the MessageHistoryLimiter tests - test_message_history_limiter_apply_transform(message_history_limiter, long_messages, 3) - test_message_history_limiter_apply_transform(message_history_limiter, short_messages, 3) - test_message_history_limiter_get_logs( - message_history_limiter, long_messages, "Removed 2 messages. Number of messages reduced from 5 to 3.", True - ) - test_message_history_limiter_get_logs(message_history_limiter, short_messages, "No messages were removed.", False) + + for messages, expected_messages_len in zip( + message_history_limiter_apply_transform_parameters["messages"], + message_history_limiter_apply_transform_parameters["expected_messages_len"], + ): + test_message_history_limiter_apply_transform(message_history_limiter, messages, expected_messages_len) + + for messages, expected_logs, expected_effect in zip( + message_history_limiter_get_logs_parameters["messages"], + message_history_limiter_get_logs_parameters["expected_logs"], + message_history_limiter_get_logs_parameters["expected_effect"], + ): + test_message_history_limiter_get_logs(message_history_limiter, messages, expected_logs, expected_effect) # Call the MessageTokenLimiter tests - test_message_token_limiter_apply_transform(message_token_limiter, long_messages, 9) - test_message_token_limiter_apply_transform(message_token_limiter, short_messages, 3) - test_message_token_limiter_get_logs( - message_token_limiter, long_messages, "Truncated 6 tokens. Number of tokens reduced from 15 to 9", True - ) - test_message_token_limiter_get_logs(message_token_limiter, short_messages, "No tokens were truncated.", False) + + for messages, expected_token_count, expected_messages_len in zip( + message_token_limiter_apply_transform_parameters["messages"], + message_token_limiter_apply_transform_parameters["expected_token_count"], + message_token_limiter_apply_transform_parameters["expected_messages_len"], + ): + test_message_token_limiter_apply_transform( + message_token_limiter, messages, expected_token_count, expected_messages_len + ) + + for messages, expected_token_count, expected_messages_len in zip( + message_token_limiter_with_threshold_apply_transform_parameters["messages"], + message_token_limiter_with_threshold_apply_transform_parameters["expected_token_count"], + message_token_limiter_with_threshold_apply_transform_parameters["expected_messages_len"], + ): + test_message_token_limiter_with_threshold_apply_transform( + message_token_limiter_with_threshold, messages, expected_token_count, expected_messages_len + ) + + for messages, expected_logs, expected_effect in zip( + message_token_limiter_get_logs_parameters["messages"], + message_token_limiter_get_logs_parameters["expected_logs"], + message_token_limiter_get_logs_parameters["expected_effect"], + ): + test_message_token_limiter_get_logs(message_token_limiter, messages, expected_logs, expected_effect) diff --git a/website/docs/topics/long_contexts.md b/website/docs/topics/long_contexts.md index 0d867619104..51648c5c549 100644 --- a/website/docs/topics/long_contexts.md +++ b/website/docs/topics/long_contexts.md @@ -62,11 +62,11 @@ By applying the `MessageHistoryLimiter`, we can see that we were able to limit t #### Example 2: Limiting the Number of Tokens -To adhere to token limitations, use the `MessageTokenLimiter` transformation. This limits tokens per message and the total token count across all messages: +To adhere to token limitations, use the `MessageTokenLimiter` transformation. This limits tokens per message and the total token count across all messages. Additionally, a `min_tokens` threshold can be applied: ```python # Limit the token limit per message to 3 tokens -token_limit_transform = transforms.MessageTokenLimiter(max_tokens_per_message=3) +token_limit_transform = transforms.MessageTokenLimiter(max_tokens_per_message=3, min_tokens=10) processed_messages = token_limit_transform.apply_transform(copy.deepcopy(messages)) @@ -83,6 +83,26 @@ pprint.pprint(processed_messages) We can see that we were able to limit the number of tokens to 3, which is equivalent to 3 words for this instance. +In the following example we will explore the effect of the `min_tokens` threshold. + +```python +short_messages = [ + {"role": "user", "content": "hello there, how are you?"}, + {"role": "assistant", "content": [{"type": "text", "text": "hello"}]}, +] + +processed_short_messages = token_limit_transform.apply_transform(copy.deepcopy(short_messages)) + +pprint.pprint(processed_short_messages) +``` + +```console +[{'content': 'hello there, how are you?', 'role': 'user'}, + {'content': [{'text': 'hello', 'type': 'text'}], 'role': 'assistant'}] + ``` + + We can see that no transformation was applied, because the threshold of 10 total tokens was not reached. + ### Apply Transformations Using Agents So far, we have only tested the `MessageHistoryLimiter` and `MessageTokenLimiter` transformations individually, let's test these transformations with AutoGen's agents. @@ -159,7 +179,7 @@ Now let's add the `TransformMessages` capability to the assistant and run the sa context_handling = transform_messages.TransformMessages( transforms=[ transforms.MessageHistoryLimiter(max_messages=10), - transforms.MessageTokenLimiter(max_tokens=1000, max_tokens_per_message=50), + transforms.MessageTokenLimiter(max_tokens=1000, max_tokens_per_message=50, min_tokens=500), ] ) context_handling.add_to_agent(assistant) @@ -249,6 +269,27 @@ class MessageRedact: item["text"] = re.sub(self._openai_key_pattern, self._replacement_string, item["text"]) return temp_messages + def get_logs(self, pre_transform_messages: List[Dict], post_transform_messages: List[Dict]) -> Tuple[str, bool]: + keys_redacted = self._count_redacted(post_transform_messages) - self._count_redacted(pre_transform_messages) + if keys_redacted > 0: + return f"Redacted {keys_redacted} OpenAI API keys.", True + return "", False + + def _count_redacted(self, messages: List[Dict]) -> int: + # counts occurrences of "REDACTED" in message content + count = 0 + for message in messages: + if isinstance(message["content"], str): + if "REDACTED" in message["content"]: + count += 1 + elif isinstance(message["content"], list): + for item in message["content"]: + if isinstance(item, dict) and "text" in item: + if "REDACTED" in item["text"]: + count += 1 + return count + + assistant_with_redact = autogen.AssistantAgent( "assistant", llm_config=llm_config, @@ -278,71 +319,25 @@ result = user_proxy.initiate_chat( ``` ````console - user_proxy (to assistant): - - - - What are the two API keys that I just provided - - - - -------------------------------------------------------------------------------- - - assistant (to user_proxy): - - - - To retrieve the two API keys you provided, I will display them individually in the output. - - - - Here is the first API key: - - ```python - - # Display the first API key - - print("API key 1 =", "REDACTED") - - ``` - - - - Here is the second API key: - - ```python - - # Display the second API key - - print("API key 2 =", "REDACTED") - - ``` - - - - Please run the code snippets to see the API keys. After that, I will mark this task as complete. - - - - -------------------------------------------------------------------------------- - - - - >>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)... - +user_proxy (to assistant): +What are the two API keys that I just provided - >>>>>>>> EXECUTING CODE BLOCK 1 (inferred language is python)... +-------------------------------------------------------------------------------- +Redacted 2 OpenAI API keys. +assistant (to user_proxy): - user_proxy (to assistant): +As an AI, I must inform you that it is not safe to share API keys publicly as they can be used to access your private data or services that can incur costs. Given that you've typed "REDACTED" instead of the actual keys, it seems you are aware of the privacy concerns and are likely testing my response or simulating an exchange without exposing real credentials, which is a good practice for privacy and security reasons. +To respond directly to your direct question: The two API keys you provided are both placeholders indicated by the text "REDACTED", and not actual API keys. If these were real keys, I would have reiterated the importance of keeping them secure and would not display them here. +Remember to keep your actual API keys confidential to prevent unauthorized use. If you've accidentally exposed real API keys, you should revoke or regenerate them as soon as possible through the corresponding service's API management console. - exitcode: 0 (execution succeeded) +-------------------------------------------------------------------------------- +user_proxy (to assistant): - Code output: - API key 1 = REDACTED - API key 2 = REDACTED +-------------------------------------------------------------------------------- +Redacted 2 OpenAI API keys. ```` From 5e29ac84dc32e913dd81bb5989285f20099df9fd Mon Sep 17 00:00:00 2001 From: Xiaoyun Zhang Date: Mon, 29 Apr 2024 13:27:57 -0700 Subject: [PATCH 18/30] [.Net] fix code ql for dotnet build && update trigger for dotnet workflow (#2529) * fix formatting * update dotnet build pipieline --- .github/workflows/dotnet-build.yml | 4 ++-- .github/workflows/dotnet-release.yml | 1 - dotnet/Directory.Build.props | 2 +- dotnet/global.json | 2 +- dotnet/nuget/nuget-package.props | 4 ++-- .../Example12_TwoAgent_Fill_Application.cs | 6 +++--- dotnet/src/AutoGen.Core/AutoGen.Core.csproj | 2 +- dotnet/src/AutoGen.Core/GroupChat/Graph.cs | 2 +- .../AutoGen.DotnetInteractive.csproj | 2 +- dotnet/src/AutoGen.LMStudio/AutoGen.LMStudio.csproj | 2 +- dotnet/src/AutoGen.Mistral/AutoGen.Mistral.csproj | 2 +- dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs | 5 ++++- dotnet/src/AutoGen.Mistral/DTOs/Error.cs | 5 ++++- dotnet/src/AutoGen.Mistral/DTOs/Model.cs | 5 ++++- dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj | 2 +- .../AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj | 2 +- .../AutoGen.SourceGenerator/AutoGen.SourceGenerator.csproj | 2 +- dotnet/src/AutoGen/AutoGen.csproj | 2 +- 18 files changed, 30 insertions(+), 22 deletions(-) diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index e337f714334..332e656c9f1 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -6,11 +6,11 @@ name: dotnet-ci on: workflow_dispatch: pull_request: - branches: [ "dotnet" ] + branches: [ "main" ] paths: - 'dotnet/**' push: - branches: [ "dotnet" ] + branches: [ "main" ] concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }} diff --git a/.github/workflows/dotnet-release.yml b/.github/workflows/dotnet-release.yml index 84b1f43b71e..b512b4c1696 100644 --- a/.github/workflows/dotnet-release.yml +++ b/.github/workflows/dotnet-release.yml @@ -8,7 +8,6 @@ on: push: branches: - dotnet/release/** - - dotnet/release concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }} diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props index 03a11d92c23..5641c6cacbb 100644 --- a/dotnet/Directory.Build.props +++ b/dotnet/Directory.Build.props @@ -18,6 +18,6 @@ - $(MSBuildThisFileDirectory)../ + $(MSBuildThisFileDirectory) \ No newline at end of file diff --git a/dotnet/global.json b/dotnet/global.json index a93054a455c..a604954f983 100644 --- a/dotnet/global.json +++ b/dotnet/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.101", + "version": "8.0.104", "rollForward": "latestMinor" } } \ No newline at end of file diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index 237fa96bcb2..c6ddf38916f 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -44,8 +44,8 @@ - - + + diff --git a/dotnet/sample/AutoGen.BasicSamples/Example12_TwoAgent_Fill_Application.cs b/dotnet/sample/AutoGen.BasicSamples/Example12_TwoAgent_Fill_Application.cs index c5e6773d01e..b622a3e641e 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example12_TwoAgent_Fill_Application.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example12_TwoAgent_Fill_Application.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// Example11_TwoAgent_Fill_Application.cs +// Example12_TwoAgent_Fill_Application.cs using System.Text; -using AutoGen.OpenAI; using AutoGen.Core; -using Azure.AI.OpenAI; +using AutoGen.OpenAI; using AutoGen.OpenAI.Extension; +using Azure.AI.OpenAI; namespace AutoGen.BasicSample; diff --git a/dotnet/src/AutoGen.Core/AutoGen.Core.csproj b/dotnet/src/AutoGen.Core/AutoGen.Core.csproj index 018cd23a446..409b6bc1aaf 100644 --- a/dotnet/src/AutoGen.Core/AutoGen.Core.csproj +++ b/dotnet/src/AutoGen.Core/AutoGen.Core.csproj @@ -4,7 +4,7 @@ AutoGen.Core - + diff --git a/dotnet/src/AutoGen.Core/GroupChat/Graph.cs b/dotnet/src/AutoGen.Core/GroupChat/Graph.cs index 483ce63bebc..78d92508611 100644 --- a/dotnet/src/AutoGen.Core/GroupChat/Graph.cs +++ b/dotnet/src/AutoGen.Core/GroupChat/Graph.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// Workflow.cs +// Graph.cs using System; using System.Collections.Generic; diff --git a/dotnet/src/AutoGen.DotnetInteractive/AutoGen.DotnetInteractive.csproj b/dotnet/src/AutoGen.DotnetInteractive/AutoGen.DotnetInteractive.csproj index e17356994f8..57fcb1fce16 100644 --- a/dotnet/src/AutoGen.DotnetInteractive/AutoGen.DotnetInteractive.csproj +++ b/dotnet/src/AutoGen.DotnetInteractive/AutoGen.DotnetInteractive.csproj @@ -8,7 +8,7 @@ true - + diff --git a/dotnet/src/AutoGen.LMStudio/AutoGen.LMStudio.csproj b/dotnet/src/AutoGen.LMStudio/AutoGen.LMStudio.csproj index b738fe02bb7..f45a2f7eba5 100644 --- a/dotnet/src/AutoGen.LMStudio/AutoGen.LMStudio.csproj +++ b/dotnet/src/AutoGen.LMStudio/AutoGen.LMStudio.csproj @@ -5,7 +5,7 @@ AutoGen.LMStudio - + diff --git a/dotnet/src/AutoGen.Mistral/AutoGen.Mistral.csproj b/dotnet/src/AutoGen.Mistral/AutoGen.Mistral.csproj index f7de19ca0c9..f1bb8e0afab 100644 --- a/dotnet/src/AutoGen.Mistral/AutoGen.Mistral.csproj +++ b/dotnet/src/AutoGen.Mistral/AutoGen.Mistral.csproj @@ -5,7 +5,7 @@ AutoGen.Mistral - + diff --git a/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs b/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs index 58dcf5297a3..ff241f8d340 100644 --- a/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs +++ b/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +// Copyright (c) Microsoft Corporation. All rights reserved. +// ChatCompletionResponse.cs + +using System.Collections.Generic; using System.Text.Json.Serialization; namespace AutoGen.Mistral; diff --git a/dotnet/src/AutoGen.Mistral/DTOs/Error.cs b/dotnet/src/AutoGen.Mistral/DTOs/Error.cs index ed04721c67d..77eb2d341fb 100644 --- a/dotnet/src/AutoGen.Mistral/DTOs/Error.cs +++ b/dotnet/src/AutoGen.Mistral/DTOs/Error.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Error.cs + +using System.Text.Json.Serialization; namespace AutoGen.Mistral { diff --git a/dotnet/src/AutoGen.Mistral/DTOs/Model.cs b/dotnet/src/AutoGen.Mistral/DTOs/Model.cs index 2d653f71859..915d2f737ec 100644 --- a/dotnet/src/AutoGen.Mistral/DTOs/Model.cs +++ b/dotnet/src/AutoGen.Mistral/DTOs/Model.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Model.cs + +using System; using System.Text.Json.Serialization; namespace AutoGen.Mistral; diff --git a/dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj b/dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj index 182d112227b..7220cfe5c62 100644 --- a/dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj +++ b/dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj @@ -4,7 +4,7 @@ AutoGen.OpenAI - + diff --git a/dotnet/src/AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj b/dotnet/src/AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj index 70d75006701..06c464fc95f 100644 --- a/dotnet/src/AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj +++ b/dotnet/src/AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj @@ -5,7 +5,7 @@ AutoGen.SemanticKernel - + diff --git a/dotnet/src/AutoGen.SourceGenerator/AutoGen.SourceGenerator.csproj b/dotnet/src/AutoGen.SourceGenerator/AutoGen.SourceGenerator.csproj index a9d2766318c..4558160722d 100644 --- a/dotnet/src/AutoGen.SourceGenerator/AutoGen.SourceGenerator.csproj +++ b/dotnet/src/AutoGen.SourceGenerator/AutoGen.SourceGenerator.csproj @@ -13,7 +13,7 @@ $(DefineConstants);LAUNCH_DEBUGGER - + diff --git a/dotnet/src/AutoGen/AutoGen.csproj b/dotnet/src/AutoGen/AutoGen.csproj index 4d0d791eb8b..2b9aaed6dd5 100644 --- a/dotnet/src/AutoGen/AutoGen.csproj +++ b/dotnet/src/AutoGen/AutoGen.csproj @@ -4,7 +4,7 @@ AutoGen - + From 5b6ae324e2918ab50e099c0344c76bc1b1e03464 Mon Sep 17 00:00:00 2001 From: Mark Sze <66362098+marklysze@users.noreply.github.com> Date: Tue, 30 Apr 2024 14:16:08 +1000 Subject: [PATCH 19/30] Re-query speaker name when multiple speaker names returned during Group Chat speaker selection (#2304) * Added requery_on_multiple_speaker_names to GroupChat and updated _finalize_speaker to requery on multiple speaker names (if enabled) * Removed unnecessary comments * Update to current main * Tweak error message. * Comment clarity * Expanded description of Group Chat requery_on_multiple_speaker_names * Reworked to two-way nested chat for speaker selection with default of 2 retries. * Adding validation of new GroupChat attributes * Updates as per @ekzhu's suggestions * Update groupchat - Added select_speaker_auto_multiple_template and select_speaker_auto_none_template - Added max_attempts comment - Re-instated support for role_for_select_speaker_messages - * Update conversable_agent.py Added ability to force override role for a message to support select speaker prompt. * Update test_groupchat.py Updated existing select_speaker test functions as underlying approach has changed, added necessary tests for new functionality. * Removed block for manual selection in select_speaker function. * Catered for no-selection during manual selection mode --------- Co-authored-by: Chi Wang --- autogen/agentchat/conversable_agent.py | 5 + autogen/agentchat/groupchat.py | 390 +++++++++++++++++++++++-- test/agentchat/test_groupchat.py | 369 ++++++++++++++++++++++- 3 files changed, 732 insertions(+), 32 deletions(-) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index 262fc513d23..b04222f514e 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -576,6 +576,11 @@ def _append_oai_message(self, message: Union[Dict, str], role, conversation_id: if message.get("role") in ["function", "tool"]: oai_message["role"] = message.get("role") + elif "override_role" in message: + # If we have a direction to override the role then set the + # role accordingly. Used to customise the role for the + # select speaker prompt. + oai_message["role"] = message.get("override_role") else: oai_message["role"] = role diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index f5b6106863a..86492455080 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -7,6 +7,7 @@ from ..code_utils import content_str from ..exception_utils import AgentNameConflict, NoEligibleSpeaker, UndefinedNextAgent +from ..formatting_utils import colored from ..graph_utils import check_graph_validity, invert_disallowed_to_allowed from ..io.base import IOStream from ..runtime_logging import log_new_agent, logging_enabled @@ -28,13 +29,28 @@ class GroupChat: When set to True and when a message is a function call suggestion, the next speaker will be chosen from an agent which contains the corresponding function name in its `function_map`. - - select_speaker_message_template: customize the select speaker message (used in "auto" speaker selection), which appears first in the message context and generally includes the agent descriptions and list of agents. The string value will be converted to an f-string, use "{roles}" to output the agent's and their role descriptions and "{agentlist}" for a comma-separated list of agent names in square brackets. The default value is: + - select_speaker_message_template: customize the select speaker message (used in "auto" speaker selection), which appears first in the message context and generally includes the agent descriptions and list of agents. If the string contains "{roles}" it will replaced with the agent's and their role descriptions. If the string contains "{agentlist}" it will be replaced with a comma-separated list of agent names in square brackets. The default value is: "You are in a role play game. The following roles are available: {roles}. Read the following conversation. Then select the next role from {agentlist} to play. Only return the role." - - select_speaker_prompt_template: customize the select speaker prompt (used in "auto" speaker selection), which appears last in the message context and generally includes the list of agents and guidance for the LLM to select the next agent. The string value will be converted to an f-string, use "{agentlist}" for a comma-separated list of agent names in square brackets. The default value is: + - select_speaker_prompt_template: customize the select speaker prompt (used in "auto" speaker selection), which appears last in the message context and generally includes the list of agents and guidance for the LLM to select the next agent. If the string contains "{agentlist}" it will be replaced with a comma-separated list of agent names in square brackets. The default value is: "Read the above conversation. Then select the next role from {agentlist} to play. Only return the role." + - select_speaker_auto_multiple_template: customize the follow-up prompt used when selecting a speaker fails with a response that contains multiple agent names. This prompt guides the LLM to return just one agent name. Applies only to "auto" speaker selection method. If the string contains "{agentlist}" it will be replaced with a comma-separated list of agent names in square brackets. The default value is: + "You provided more than one name in your text, please return just the name of the next speaker. To determine the speaker use these prioritised rules: + 1. If the context refers to themselves as a speaker e.g. "As the..." , choose that speaker's name + 2. If it refers to the "next" speaker name, choose that name + 3. Otherwise, choose the first provided speaker's name in the context + The names are case-sensitive and should not be abbreviated or changed. + Respond with ONLY the name of the speaker and DO NOT provide a reason." + - select_speaker_auto_none_template: customize the follow-up prompt used when selecting a speaker fails with a response that contains no agent names. This prompt guides the LLM to return an agent name and provides a list of agent names. Applies only to "auto" speaker selection method. If the string contains "{agentlist}" it will be replaced with a comma-separated list of agent names in square brackets. The default value is: + "You didn't choose a speaker. As a reminder, to determine the speaker use these prioritised rules: + 1. If the context refers to themselves as a speaker e.g. "As the..." , choose that speaker's name + 2. If it refers to the "next" speaker name, choose that name + 3. Otherwise, choose the first provided speaker's name in the context + The names are case-sensitive and should not be abbreviated or changed. + The only names that are accepted are {agentlist}. + Respond with ONLY the name of the speaker and DO NOT provide a reason." - speaker_selection_method: the method for selecting the next speaker. Default is "auto". Could be any of the following (case insensitive), will raise ValueError if not recognized: - "auto": the next speaker is selected automatically by LLM. @@ -51,6 +67,15 @@ def custom_speaker_selection_func( last_speaker: Agent, groupchat: GroupChat ) -> Union[Agent, str, None]: ``` + - max_retries_for_selecting_speaker: the maximum number of times the speaker selection requery process will run. + If, during speaker selection, multiple agent names or no agent names are returned by the LLM as the next agent, it will be queried again up to the maximum number + of times until a single agent is returned or it exhausts the maximum attempts. + Applies only to "auto" speaker selection method. + Default is 2. + - select_speaker_auto_verbose: whether to output the select speaker responses and selections + If set to True, the outputs from the two agents in the nested select speaker chat will be output, along with + whether the responses were successful, or not, in selecting an agent + Applies only to "auto" speaker selection method. - allow_repeat_speaker: whether to allow the same speaker to speak consecutively. Default is True, in which case all speakers are allowed to speak consecutively. If `allow_repeat_speaker` is a list of Agents, then only those listed agents are allowed to repeat. @@ -77,6 +102,7 @@ def custom_speaker_selection_func( admin_name: Optional[str] = "Admin" func_call_filter: Optional[bool] = True speaker_selection_method: Union[Literal["auto", "manual", "random", "round_robin"], Callable] = "auto" + max_retries_for_selecting_speaker: Optional[int] = 2 allow_repeat_speaker: Optional[Union[bool, List[Agent]]] = None allowed_or_disallowed_speaker_transitions: Optional[Dict] = None speaker_transitions_type: Literal["allowed", "disallowed", None] = None @@ -89,6 +115,20 @@ def custom_speaker_selection_func( select_speaker_prompt_template: str = ( "Read the above conversation. Then select the next role from {agentlist} to play. Only return the role." ) + select_speaker_auto_multiple_template: str = """You provided more than one name in your text, please return just the name of the next speaker. To determine the speaker use these prioritised rules: + 1. If the context refers to themselves as a speaker e.g. "As the..." , choose that speaker's name + 2. If it refers to the "next" speaker name, choose that name + 3. Otherwise, choose the first provided speaker's name in the context + The names are case-sensitive and should not be abbreviated or changed. + Respond with ONLY the name of the speaker and DO NOT provide a reason.""" + select_speaker_auto_none_template: str = """You didn't choose a speaker. As a reminder, to determine the speaker use these prioritised rules: + 1. If the context refers to themselves as a speaker e.g. "As the..." , choose that speaker's name + 2. If it refers to the "next" speaker name, choose that name + 3. Otherwise, choose the first provided speaker's name in the context + The names are case-sensitive and should not be abbreviated or changed. + The only names that are accepted are {agentlist}. + Respond with ONLY the name of the speaker and DO NOT provide a reason.""" + select_speaker_auto_verbose: Optional[bool] = False role_for_select_speaker_messages: Optional[str] = "system" _VALID_SPEAKER_SELECTION_METHODS = ["auto", "manual", "random", "round_robin"] @@ -178,7 +218,7 @@ def __post_init__(self): agents=self.agents, ) - # Check select_speaker_message_template and select_speaker_prompt_template have values + # Check select speaker messages, prompts, roles, and retries have values if self.select_speaker_message_template is None or len(self.select_speaker_message_template) == 0: raise ValueError("select_speaker_message_template cannot be empty or None.") @@ -188,6 +228,27 @@ def __post_init__(self): if self.role_for_select_speaker_messages is None or len(self.role_for_select_speaker_messages) == 0: raise ValueError("role_for_select_speaker_messages cannot be empty or None.") + if self.select_speaker_auto_multiple_template is None or len(self.select_speaker_auto_multiple_template) == 0: + raise ValueError("select_speaker_auto_multiple_template cannot be empty or None.") + + if self.select_speaker_auto_none_template is None or len(self.select_speaker_auto_none_template) == 0: + raise ValueError("select_speaker_auto_none_template cannot be empty or None.") + + if self.max_retries_for_selecting_speaker is None or len(self.role_for_select_speaker_messages) == 0: + raise ValueError("role_for_select_speaker_messages cannot be empty or None.") + + # Validate max select speakers retries + if self.max_retries_for_selecting_speaker is None or not isinstance( + self.max_retries_for_selecting_speaker, int + ): + raise ValueError("max_retries_for_selecting_speaker cannot be None or non-int") + elif self.max_retries_for_selecting_speaker < 0: + raise ValueError("max_retries_for_selecting_speaker must be greater than or equal to zero") + + # Validate select_speaker_auto_verbose + if self.select_speaker_auto_verbose is None or not isinstance(self.select_speaker_auto_verbose, bool): + raise ValueError("select_speaker_auto_verbose cannot be None or non-bool") + @property def agent_names(self) -> List[str]: """Return the names of the agents in the group chat.""" @@ -450,33 +511,34 @@ def _prepare_and_select_agents( select_speaker_messages[-1] = dict(select_speaker_messages[-1], function_call=None) if select_speaker_messages[-1].get("tool_calls", False): select_speaker_messages[-1] = dict(select_speaker_messages[-1], tool_calls=None) - select_speaker_messages = select_speaker_messages + [ - { - "role": self.role_for_select_speaker_messages, - "content": self.select_speaker_prompt(graph_eligible_agents), - } - ] return selected_agent, graph_eligible_agents, select_speaker_messages def select_speaker(self, last_speaker: Agent, selector: ConversableAgent) -> Agent: - """Select the next speaker.""" + """Select the next speaker (with requery).""" + + # Prepare the list of available agents and select an agent if selection method allows (non-auto) selected_agent, agents, messages = self._prepare_and_select_agents(last_speaker) if selected_agent: return selected_agent - # auto speaker selection - selector.update_system_message(self.select_speaker_msg(agents)) - final, name = selector.generate_oai_reply(messages) - return self._finalize_speaker(last_speaker, final, name, agents) + elif self.speaker_selection_method == "manual": + # An agent has not been selected while in manual mode, so move to the next agent + return self.next_agent(last_speaker) + + # auto speaker selection with 2-agent chat + return self._auto_select_speaker(last_speaker, selector, messages, agents) async def a_select_speaker(self, last_speaker: Agent, selector: ConversableAgent) -> Agent: - """Select the next speaker.""" + """Select the next speaker (with requery), asynchronously.""" + selected_agent, agents, messages = self._prepare_and_select_agents(last_speaker) if selected_agent: return selected_agent - # auto speaker selection - selector.update_system_message(self.select_speaker_msg(agents)) - final, name = await selector.a_generate_oai_reply(messages) - return self._finalize_speaker(last_speaker, final, name, agents) + elif self.speaker_selection_method == "manual": + # An agent has not been selected while in manual mode, so move to the next agent + return self.next_agent(last_speaker) + + # auto speaker selection with 2-agent chat + return await self.a_auto_select_speaker(last_speaker, selector, messages, agents) def _finalize_speaker(self, last_speaker: Agent, final: bool, name: str, agents: Optional[List[Agent]]) -> Agent: if not final: @@ -496,6 +558,296 @@ def _finalize_speaker(self, last_speaker: Agent, final: bool, name: str, agents: agent = self.agent_by_name(name) return agent if agent else self.next_agent(last_speaker, agents) + def _auto_select_speaker( + self, + last_speaker: Agent, + selector: ConversableAgent, + messages: Optional[List[Dict]], + agents: Optional[List[Agent]], + ) -> Agent: + """Selects next speaker for the "auto" speaker selection method. Utilises its own two-agent chat to determine the next speaker and supports requerying. + + Speaker selection for "auto" speaker selection method: + 1. Create a two-agent chat with a speaker selector agent and a speaker validator agent, like a nested chat + 2. Inject the group messages into the new chat + 3. Run the two-agent chat, evaluating the result of response from the speaker selector agent: + - If a single agent is provided then we return it and finish. If not, we add an additional message to this nested chat in an attempt to guide the LLM to a single agent response + 4. Chat continues until a single agent is nominated or there are no more attempts left + 5. If we run out of turns and no single agent can be determined, the next speaker in the list of agents is returned + + Args: + last_speaker Agent: The previous speaker in the group chat + selector ConversableAgent: + messages Optional[List[Dict]]: Current chat messages + agents Optional[List[Agent]]: Valid list of agents for speaker selection + + Returns: + Dict: a counter for mentioned agents. + """ + + # If no agents are passed in, assign all the group chat's agents + if agents is None: + agents = self.agents + + # The maximum number of speaker selection attempts (including requeries) + # is the initial speaker selection attempt plus the maximum number of retries. + # We track these and use them in the validation function as we can't + # access the max_turns from within validate_speaker_name. + max_attempts = 1 + self.max_retries_for_selecting_speaker + attempts_left = max_attempts + attempt = 0 + + # Registered reply function for checking_agent, checks the result of the response for agent names + def validate_speaker_name(recipient, messages, sender, config) -> Tuple[bool, Union[str, Dict, None]]: + + # The number of retries left, starting at max_retries_for_selecting_speaker + nonlocal attempts_left + nonlocal attempt + + attempt = attempt + 1 + attempts_left = attempts_left - 1 + + return self._validate_speaker_name(recipient, messages, sender, config, attempts_left, attempt, agents) + + # Two-agent chat for speaker selection + + # Agent for checking the response from the speaker_select_agent + checking_agent = ConversableAgent("checking_agent", default_auto_reply=max_attempts) + + # Register the speaker validation function with the checking agent + checking_agent.register_reply( + [ConversableAgent, None], + reply_func=validate_speaker_name, # Validate each response + remove_other_reply_funcs=True, + ) + + # Agent for selecting a single agent name from the response + speaker_selection_agent = ConversableAgent( + "speaker_selection_agent", + system_message=self.select_speaker_msg(agents), + chat_messages={checking_agent: messages}, + llm_config=selector.llm_config, + human_input_mode="NEVER", # Suppresses some extra terminal outputs, outputs will be handled by select_speaker_auto_verbose + ) + + # Run the speaker selection chat + result = checking_agent.initiate_chat( + speaker_selection_agent, + cache=None, # don't use caching for the speaker selection chat + message={ + "content": self.select_speaker_prompt(agents), + "override_role": self.role_for_select_speaker_messages, + }, + max_turns=2 + * max(1, max_attempts), # Limiting the chat to the number of attempts, including the initial one + clear_history=False, + silent=not self.select_speaker_auto_verbose, # Base silence on the verbose attribute + ) + + return self._process_speaker_selection_result(result, last_speaker, agents) + + async def a_auto_select_speaker( + self, + last_speaker: Agent, + selector: ConversableAgent, + messages: Optional[List[Dict]], + agents: Optional[List[Agent]], + ) -> Agent: + """(Asynchronous) Selects next speaker for the "auto" speaker selection method. Utilises its own two-agent chat to determine the next speaker and supports requerying. + + Speaker selection for "auto" speaker selection method: + 1. Create a two-agent chat with a speaker selector agent and a speaker validator agent, like a nested chat + 2. Inject the group messages into the new chat + 3. Run the two-agent chat, evaluating the result of response from the speaker selector agent: + - If a single agent is provided then we return it and finish. If not, we add an additional message to this nested chat in an attempt to guide the LLM to a single agent response + 4. Chat continues until a single agent is nominated or there are no more attempts left + 5. If we run out of turns and no single agent can be determined, the next speaker in the list of agents is returned + + Args: + last_speaker Agent: The previous speaker in the group chat + selector ConversableAgent: + messages Optional[List[Dict]]: Current chat messages + agents Optional[List[Agent]]: Valid list of agents for speaker selection + + Returns: + Dict: a counter for mentioned agents. + """ + + # If no agents are passed in, assign all the group chat's agents + if agents is None: + agents = self.agents + + # The maximum number of speaker selection attempts (including requeries) + # We track these and use them in the validation function as we can't + # access the max_turns from within validate_speaker_name + max_attempts = 1 + self.max_retries_for_selecting_speaker + attempts_left = max_attempts + attempt = 0 + + # Registered reply function for checking_agent, checks the result of the response for agent names + def validate_speaker_name(recipient, messages, sender, config) -> Tuple[bool, Union[str, Dict, None]]: + + # The number of retries left, starting at max_retries_for_selecting_speaker + nonlocal attempts_left + nonlocal attempt + + attempt = attempt + 1 + attempts_left = attempts_left - 1 + + return self._validate_speaker_name(recipient, messages, sender, config, attempts_left, attempt, agents) + + # Two-agent chat for speaker selection + + # Agent for checking the response from the speaker_select_agent + checking_agent = ConversableAgent("checking_agent", default_auto_reply=max_attempts) + + # Register the speaker validation function with the checking agent + checking_agent.register_reply( + [ConversableAgent, None], + reply_func=validate_speaker_name, # Validate each response + remove_other_reply_funcs=True, + ) + + # Agent for selecting a single agent name from the response + speaker_selection_agent = ConversableAgent( + "speaker_selection_agent", + system_message=self.select_speaker_msg(agents), + chat_messages={checking_agent: messages}, + llm_config=selector.llm_config, + human_input_mode="NEVER", # Suppresses some extra terminal outputs, outputs will be handled by select_speaker_auto_verbose + ) + + # Run the speaker selection chat + result = await checking_agent.a_initiate_chat( + speaker_selection_agent, + cache=None, # don't use caching for the speaker selection chat + message=self.select_speaker_prompt(agents), + max_turns=2 + * max(1, max_attempts), # Limiting the chat to the number of attempts, including the initial one + clear_history=False, + silent=not self.select_speaker_auto_verbose, # Base silence on the verbose attribute + ) + + return self._process_speaker_selection_result(result, last_speaker, agents) + + def _validate_speaker_name( + self, recipient, messages, sender, config, attempts_left, attempt, agents + ) -> Tuple[bool, Union[str, Dict, None]]: + """Validates the speaker response for each round in the internal 2-agent + chat within the auto select speaker method. + + Used by auto_select_speaker and a_auto_select_speaker. + """ + + # Output the query and requery results + if self.select_speaker_auto_verbose: + iostream = IOStream.get_default() + + # Validate the speaker name selected + select_name = messages[-1]["content"].strip() + + mentions = self._mentioned_agents(select_name, agents) + + if len(mentions) == 1: + + # Success on retry, we have just one name mentioned + selected_agent_name = next(iter(mentions)) + + # Add the selected agent to the response so we can return it + messages.append({"role": "user", "content": f"[AGENT SELECTED]{selected_agent_name}"}) + + if self.select_speaker_auto_verbose: + iostream.print( + colored( + f">>>>>>>> Select speaker attempt {attempt} of {attempt + attempts_left} successfully selected: {selected_agent_name}", + "green", + ), + flush=True, + ) + + elif len(mentions) > 1: + # More than one name on requery so add additional reminder prompt for next retry + + if self.select_speaker_auto_verbose: + iostream.print( + colored( + f">>>>>>>> Select speaker attempt {attempt} of {attempt + attempts_left} failed as it included multiple agent names.", + "red", + ), + flush=True, + ) + + if attempts_left: + # Message to return to the chat for the next attempt + agentlist = f"{[agent.name for agent in agents]}" + + return True, { + "content": self.select_speaker_auto_multiple_template.format(agentlist=agentlist), + "override_role": self.role_for_select_speaker_messages, + } + else: + # Final failure, no attempts left + messages.append( + { + "role": "user", + "content": f"[AGENT SELECTION FAILED]Select speaker attempt #{attempt} of {attempt + attempts_left} failed as it returned multiple names.", + } + ) + + else: + # No names at all on requery so add additional reminder prompt for next retry + + if self.select_speaker_auto_verbose: + iostream.print( + colored( + f">>>>>>>> Select speaker attempt #{attempt} failed as it did not include any agent names.", + "red", + ), + flush=True, + ) + + if attempts_left: + # Message to return to the chat for the next attempt + agentlist = f"{[agent.name for agent in agents]}" + + return True, { + "content": self.select_speaker_auto_none_template.format(agentlist=agentlist), + "override_role": self.role_for_select_speaker_messages, + } + else: + # Final failure, no attempts left + messages.append( + { + "role": "user", + "content": f"[AGENT SELECTION FAILED]Select speaker attempt #{attempt} of {attempt + attempts_left} failed as it did not include any agent names.", + } + ) + + return True, None + + def _process_speaker_selection_result(self, result, last_speaker: ConversableAgent, agents: Optional[List[Agent]]): + """Checks the result of the auto_select_speaker function, returning the + agent to speak. + + Used by auto_select_speaker and a_auto_select_speaker.""" + if len(result.chat_history) > 0: + + # Use the final message, which will have the selected agent or reason for failure + final_message = result.chat_history[-1]["content"] + + if "[AGENT SELECTED]" in final_message: + + # Have successfully selected an agent, return it + return self.agent_by_name(final_message.replace("[AGENT SELECTED]", "")) + + else: # "[AGENT SELECTION FAILED]" + + # Failed to select an agent, so we'll select the next agent in the list + next_agent = self.next_agent(last_speaker, agents) + + # No agent, return the failed reason + return next_agent + def _participant_roles(self, agents: List[Agent] = None) -> str: # Default to all agents registered if agents is None: diff --git a/test/agentchat/test_groupchat.py b/test/agentchat/test_groupchat.py index 8a4758d2d37..a4689bd539f 100755 --- a/test/agentchat/test_groupchat.py +++ b/test/agentchat/test_groupchat.py @@ -1196,28 +1196,46 @@ def test_role_for_select_speaker_messages(): agents=[agent1, agent2], messages=[{"role": "user", "content": "Let's have a chat!"}], max_round=3, + role_for_select_speaker_messages="system", ) - # Run the select agents function to get the select speaker messages - selected_agent, agents, messages = groupchat._prepare_and_select_agents(agent1) + # Replicate the _auto_select_speaker nested chat. + + # Agent for checking the response from the speaker_select_agent + checking_agent = autogen.ConversableAgent("checking_agent") + + # Agent for selecting a single agent name from the response + speaker_selection_agent = autogen.ConversableAgent( + "speaker_selection_agent", + llm_config=None, + human_input_mode="NEVER", # Suppresses some extra terminal outputs, outputs will be handled by select_speaker_auto_verbose + ) + + # The role_for_select_speaker_message is put into the initiate_chat of the nested two-way chat + # into a message attribute called 'override_role'. This is evaluated in Conversable Agent's _append_oai_message function + # e.g.: message={'content':self.select_speaker_prompt(agents),'override_role':self.role_for_select_speaker_messages}, + message = {"content": "A prompt goes here.", "override_role": groupchat.role_for_select_speaker_messages} + checking_agent._append_oai_message(message, "assistant", speaker_selection_agent) # Test default is "system" - assert len(messages) == 2 - assert messages[-1]["role"] == "system" + assert len(checking_agent.chat_messages) == 1 + assert checking_agent.chat_messages[speaker_selection_agent][-1]["role"] == "system" # Test as "user" groupchat.role_for_select_speaker_messages = "user" - selected_agent, agents, messages = groupchat._prepare_and_select_agents(agent1) + message = {"content": "A prompt goes here.", "override_role": groupchat.role_for_select_speaker_messages} + checking_agent._append_oai_message(message, "assistant", speaker_selection_agent) - assert len(messages) == 2 - assert messages[-1]["role"] == "user" + assert len(checking_agent.chat_messages) == 1 + assert checking_agent.chat_messages[speaker_selection_agent][-1]["role"] == "user" # Test as something unusual groupchat.role_for_select_speaker_messages = "SockS" - selected_agent, agents, messages = groupchat._prepare_and_select_agents(agent1) + message = {"content": "A prompt goes here.", "override_role": groupchat.role_for_select_speaker_messages} + checking_agent._append_oai_message(message, "assistant", speaker_selection_agent) - assert len(messages) == 2 - assert messages[-1]["role"] == "SockS" + assert len(checking_agent.chat_messages) == 1 + assert checking_agent.chat_messages[speaker_selection_agent][-1]["role"] == "SockS" # Test empty string and None isn't accepted @@ -1307,7 +1325,7 @@ def test_select_speaker_message_and_prompt_templates(): speaker_selection_method="auto", max_round=10, select_speaker_message_template="Not empty.", - select_speaker_prompt_template=None, + select_speaker_prompt_template="", ) # Test with None @@ -1328,7 +1346,7 @@ def test_select_speaker_message_and_prompt_templates(): speaker_selection_method="auto", max_round=10, select_speaker_message_template="Not empty.", - select_speaker_prompt_template="", + select_speaker_prompt_template=None, ) @@ -1426,6 +1444,328 @@ def test_speaker_selection_agent_name_match(): assert result == {} +def test_speaker_selection_auto_process_result(): + """ + Tests the return result of the 2-agent chat used for speaker selection for the auto method. + The last message of the messages passed in will contain a pass or fail. + If passed, the message will contain the name of the correct agent and that agent will be returned. + If failed, the message will contain the reason for failure for the last attempt and the next + agent in the sequence will be returned. + """ + cmo = autogen.ConversableAgent( + name="Chief_Marketing_Officer", + human_input_mode="NEVER", + llm_config=False, + default_auto_reply="This is alice speaking.", + ) + pm = autogen.ConversableAgent( + name="Product_Manager", + human_input_mode="NEVER", + llm_config=False, + default_auto_reply="This is bob speaking.", + function_map={"test_func": lambda x: x}, + ) + + agent_list = [cmo, pm] + groupchat = autogen.GroupChat(agents=agent_list, messages=[], max_round=3) + + chat_result = autogen.ChatResult( + chat_id=None, + chat_history=[ + { + "content": "Let's get this meeting started. First the Product_Manager will create 3 new product ideas.", + "name": "Chairperson", + "role": "assistant", + }, + {"content": "You are an expert at finding the next speaker.", "role": "assistant"}, + {"content": "Product_Manager", "role": "user"}, + {"content": "UPDATED_BELOW", "role": "user"}, + ], + ) + + ### Agent selected successfully + chat_result.chat_history[3]["content"] = "[AGENT SELECTED]Product_Manager" + + # Product_Manager should be returned + assert groupchat._process_speaker_selection_result(chat_result, cmo, agent_list) == pm + + ### Agent not selected successfully + chat_result.chat_history[3][ + "content" + ] = "[AGENT SELECTION FAILED]Select speaker attempt #3 of 3 failed as it did not include any agent names." + + # The next speaker in the list will be selected, which will be the Product_Manager (as the last speaker is the Chief_Marketing_Officer) + assert groupchat._process_speaker_selection_result(chat_result, cmo, agent_list) == pm + + ### Invalid result messages, will return the next agent + chat_result.chat_history[3]["content"] = "This text should not be here." + + # The next speaker in the list will be selected, which will be the Chief_Marketing_Officer (as the last speaker is the Product_Maanger) + assert groupchat._process_speaker_selection_result(chat_result, pm, agent_list) == cmo + + +def test_speaker_selection_validate_speaker_name(): + """ + Tests the speaker name validation function used to evaluate the return result of the LLM + during speaker selection in 'auto' mode. + + Function: _validate_speaker_name + + If a single agent name is returned by the LLM, it will add a relevant message to the chat messages and return True, None + If multiple agent names are returned and there are attempts left, it will return a message to be used to prompt the LLM to try again + If multiple agent names are return and there are no attempts left, it will add a relevant message to the chat messages and return True, None + If no agent names are returned and there are attempts left, it will return a message to be used to prompt the LLM to try again + If no agent names are returned and there are no attempts left, it will add a relevant message to the chat messages and return True, None + + When returning a message, it will include the 'override_role' key and value to support the GroupChat role_for_select_speaker_messages attribute + """ + + # Group Chat setup + cmo = autogen.ConversableAgent( + name="Chief_Marketing_Officer", + human_input_mode="NEVER", + llm_config=False, + default_auto_reply="This is alice speaking.", + ) + pm = autogen.ConversableAgent( + name="Product_Manager", + human_input_mode="NEVER", + llm_config=False, + default_auto_reply="This is bob speaking.", + function_map={"test_func": lambda x: x}, + ) + + agent_list = [cmo, pm] + agent_list_string = f"{[agent.name for agent in agent_list]}" + groupchat = autogen.GroupChat(agents=agent_list, messages=[], max_round=3) + + # Speaker Selection 2-agent chat setup + + # Agent for selecting a single agent name from the response + speaker_selection_agent = autogen.ConversableAgent( + "speaker_selection_agent", + ) + + # Agent for checking the response from the speaker_select_agent + checking_agent = autogen.ConversableAgent("checking_agent") + + # Select speaker messages + select_speaker_messages = [ + { + "content": "Let's get this meeting started. First the Product_Manager will create 3 new product ideas.", + "name": "Chairperson", + "role": "assistant", + }, + {"content": "You are an expert at finding the next speaker.", "role": "assistant"}, + {"content": "UPDATED_BELOW", "role": "user"}, + ] + + ### Single agent name returned + attempts_left = 2 + attempt = 1 + select_speaker_messages[-1]["content"] = "Product_Manager is the next to speak" + + result = groupchat._validate_speaker_name( + recipient=checking_agent, + messages=select_speaker_messages, + sender=speaker_selection_agent, + config=None, + attempts_left=attempts_left, + attempt=attempt, + agents=agent_list, + ) + + assert result == (True, None) + assert select_speaker_messages[-1]["content"] == "[AGENT SELECTED]Product_Manager" + + select_speaker_messages.pop(-1) # Remove the last message before the next test + + ### Multiple agent names returned with attempts left + attempts_left = 2 + attempt = 1 + select_speaker_messages[-1]["content"] = "Product_Manager must speak after the Chief_Marketing_Officer" + + result = groupchat._validate_speaker_name( + recipient=checking_agent, + messages=select_speaker_messages, + sender=speaker_selection_agent, + config=None, + attempts_left=attempts_left, + attempt=attempt, + agents=agent_list, + ) + + assert result == ( + True, + { + "content": groupchat.select_speaker_auto_multiple_template.format(agentlist=agent_list_string), + "override_role": groupchat.role_for_select_speaker_messages, + }, + ) + + ### Multiple agent names returned with no attempts left + attempts_left = 0 + attempt = 1 + select_speaker_messages[-1]["content"] = "Product_Manager must speak after the Chief_Marketing_Officer" + + result = groupchat._validate_speaker_name( + recipient=checking_agent, + messages=select_speaker_messages, + sender=speaker_selection_agent, + config=None, + attempts_left=attempts_left, + attempt=attempt, + agents=agent_list, + ) + + assert result == (True, None) + assert ( + select_speaker_messages[-1]["content"] + == f"[AGENT SELECTION FAILED]Select speaker attempt #{attempt} of {attempt + attempts_left} failed as it returned multiple names." + ) + + select_speaker_messages.pop(-1) # Remove the last message before the next test + + ### No agent names returned with attempts left + attempts_left = 3 + attempt = 2 + select_speaker_messages[-1]["content"] = "The PM must speak after the CMO" + + result = groupchat._validate_speaker_name( + recipient=checking_agent, + messages=select_speaker_messages, + sender=speaker_selection_agent, + config=None, + attempts_left=attempts_left, + attempt=attempt, + agents=agent_list, + ) + + assert result == ( + True, + { + "content": groupchat.select_speaker_auto_none_template.format(agentlist=agent_list_string), + "override_role": groupchat.role_for_select_speaker_messages, + }, + ) + + ### Multiple agents returned with no attempts left + attempts_left = 0 + attempt = 3 + select_speaker_messages[-1]["content"] = "The PM must speak after the CMO" + + result = groupchat._validate_speaker_name( + recipient=checking_agent, + messages=select_speaker_messages, + sender=speaker_selection_agent, + config=None, + attempts_left=attempts_left, + attempt=attempt, + agents=agent_list, + ) + + assert result == (True, None) + assert ( + select_speaker_messages[-1]["content"] + == f"[AGENT SELECTION FAILED]Select speaker attempt #{attempt} of {attempt + attempts_left} failed as it did not include any agent names." + ) + + +def test_select_speaker_auto_messages(): + """ + In this test, two agents are part of a group chat which has customized select speaker "auto" multiple and no-name prompt messages. Both valid and empty string values will be used. + The expected behaviour is that the customized speaker selection "auto" messages will override the default values or throw exceptions if empty. + """ + + agent1 = autogen.ConversableAgent( + "Alice", + description="A wonderful employee named Alice.", + human_input_mode="NEVER", + llm_config=False, + ) + agent2 = autogen.ConversableAgent( + "Bob", + description="An amazing employee named Bob.", + human_input_mode="NEVER", + llm_config=False, + ) + + # Customised message for select speaker auto method where multiple agent names are returned + custom_multiple_names_msg = "You mentioned multiple names but we need just one. Select the best one. A reminder that the options are {agentlist}." + + # Customised message for select speaker auto method where no agent names are returned + custom_no_names_msg = "You forgot to select a single names and we need one, and only one. Select the best one. A reminder that the options are {agentlist}." + + # Test empty is_termination_msg function + groupchat = autogen.GroupChat( + agents=[agent1, agent2], + messages=[], + speaker_selection_method="auto", + max_round=10, + select_speaker_auto_multiple_template=custom_multiple_names_msg, + select_speaker_auto_none_template=custom_no_names_msg, + ) + + # Test using the _validate_speaker_name function, checking for the correct string and agentlist to be included + agents = [agent1, agent2] + + messages = [{"content": "Alice and Bob should both speak.", "name": "speaker_selector", "role": "user"}] + assert groupchat._validate_speaker_name(None, messages, None, None, 1, 1, agents) == ( + True, + { + "content": custom_multiple_names_msg.replace("{agentlist}", "['Alice', 'Bob']"), + "override_role": groupchat.role_for_select_speaker_messages, + }, + ) + + messages = [{"content": "Fred should both speak.", "name": "speaker_selector", "role": "user"}] + assert groupchat._validate_speaker_name(None, messages, None, None, 1, 1, agents) == ( + True, + { + "content": custom_no_names_msg.replace("{agentlist}", "['Alice', 'Bob']"), + "override_role": groupchat.role_for_select_speaker_messages, + }, + ) + + # Test with empty strings + with pytest.raises(ValueError, match="select_speaker_auto_multiple_template cannot be empty or None."): + groupchat = autogen.GroupChat( + agents=[agent1, agent2], + messages=[], + speaker_selection_method="auto", + max_round=10, + select_speaker_auto_multiple_template="", + ) + + with pytest.raises(ValueError, match="select_speaker_auto_none_template cannot be empty or None."): + groupchat = autogen.GroupChat( + agents=[agent1, agent2], + messages=[], + speaker_selection_method="auto", + max_round=10, + select_speaker_auto_none_template="", + ) + + # Test with None + with pytest.raises(ValueError, match="select_speaker_auto_multiple_template cannot be empty or None."): + groupchat = autogen.GroupChat( + agents=[agent1, agent2], + messages=[], + speaker_selection_method="auto", + max_round=10, + select_speaker_auto_multiple_template=None, + ) + + with pytest.raises(ValueError, match="select_speaker_auto_none_template cannot be empty or None."): + groupchat = autogen.GroupChat( + agents=[agent1, agent2], + messages=[], + speaker_selection_method="auto", + max_round=10, + select_speaker_auto_none_template=None, + ) + + if __name__ == "__main__": # test_func_call_groupchat() # test_broadcast() @@ -1443,5 +1783,8 @@ def test_speaker_selection_agent_name_match(): # test_custom_speaker_selection_overrides_transition_graph() # test_role_for_select_speaker_messages() # test_select_speaker_message_and_prompt_templates() - test_speaker_selection_agent_name_match() + # test_speaker_selection_agent_name_match() + test_speaker_selection_auto_process_result() + test_speaker_selection_validate_speaker_name() + test_select_speaker_auto_messages() # pass From ba9ff45adbf94b0632642b9633cff8fac1d22cc6 Mon Sep 17 00:00:00 2001 From: Arun <43626691+beyonddream@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:26:36 -0700 Subject: [PATCH 20/30] Remove unneeded duplicate check for pydantic v1 since we are already checking that in the else block. (#2467) * Remove unneeded duplicate check for pydantic v1 since we are already in the else block. * fix formatting --- autogen/_pydantic.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/autogen/_pydantic.py b/autogen/_pydantic.py index 9a37208c406..c463dbb3875 100644 --- a/autogen/_pydantic.py +++ b/autogen/_pydantic.py @@ -64,27 +64,27 @@ def type2schema(t: Any) -> JsonSchemaValue: Returns: JsonSchemaValue: The JSON schema """ - if PYDANTIC_V1: - if t is None: - return {"type": "null"} - elif get_origin(t) is Union: - return {"anyOf": [type2schema(tt) for tt in get_args(t)]} - elif get_origin(t) in [Tuple, tuple]: - prefixItems = [type2schema(tt) for tt in get_args(t)] - return { - "maxItems": len(prefixItems), - "minItems": len(prefixItems), - "prefixItems": prefixItems, - "type": "array", - } - - d = schema_of(t) - if "title" in d: - d.pop("title") - if "description" in d: - d.pop("description") - - return d + + if t is None: + return {"type": "null"} + elif get_origin(t) is Union: + return {"anyOf": [type2schema(tt) for tt in get_args(t)]} + elif get_origin(t) in [Tuple, tuple]: + prefixItems = [type2schema(tt) for tt in get_args(t)] + return { + "maxItems": len(prefixItems), + "minItems": len(prefixItems), + "prefixItems": prefixItems, + "type": "array", + } + else: + d = schema_of(t) + if "title" in d: + d.pop("title") + if "description" in d: + d.pop("description") + + return d def model_dump(model: BaseModel) -> Dict[str, Any]: """Convert a pydantic model to a dict From bcb6117c97b097030815b48a7a7df9645dce1879 Mon Sep 17 00:00:00 2001 From: Jinhua Wang Date: Tue, 30 Apr 2024 21:42:38 +0100 Subject: [PATCH 21/30] Update token_count_utils.py (#2531) * Update token_count_utils.py Update the token counts of new gpt models ref: https://platform.openai.com/docs/models/gpt-4-turbo-and-gpt-4 * format code with pre-commit --------- Co-authored-by: Chi Wang Co-authored-by: Eric Zhu --- autogen/token_count_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/autogen/token_count_utils.py b/autogen/token_count_utils.py index d68e4ee8152..589d7b404a7 100644 --- a/autogen/token_count_utils.py +++ b/autogen/token_count_utils.py @@ -14,7 +14,8 @@ def get_max_token_limit(model: str = "gpt-3.5-turbo-0613") -> int: model = re.sub(r"^gpt4", "gpt-4", model) max_token_limit = { - "gpt-3.5-turbo": 4096, + "gpt-3.5-turbo": 16385, + "gpt-3.5-turbo-0125": 16385, "gpt-3.5-turbo-0301": 4096, "gpt-3.5-turbo-0613": 4096, "gpt-3.5-turbo-instruct": 4096, @@ -22,6 +23,8 @@ def get_max_token_limit(model: str = "gpt-3.5-turbo-0613") -> int: "gpt-3.5-turbo-16k-0613": 16385, "gpt-3.5-turbo-1106": 16385, "gpt-4": 8192, + "gpt-4-turbo": 128000, + "gpt-4-turbo-2024-04-09": 128000, "gpt-4-32k": 32768, "gpt-4-32k-0314": 32768, # deprecate in Sep "gpt-4-0314": 8192, # deprecate in Sep From a0fffc815bf25e21a719a3549229b1e218add8c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Bezerra=20Val=C3=A9rio?= Date: Tue, 30 Apr 2024 17:49:04 -0300 Subject: [PATCH 22/30] feat: add bind_dir arg to DockerCommandLineExecutor + docs update (#2309) * add bind_dir arg and update docs * lint --- .../coding/docker_commandline_code_executor.py | 17 ++++++++++++++++- .../code-execution/cli-code-executor.ipynb | 4 +++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/autogen/coding/docker_commandline_code_executor.py b/autogen/coding/docker_commandline_code_executor.py index 143b241c2cf..0828786a64f 100644 --- a/autogen/coding/docker_commandline_code_executor.py +++ b/autogen/coding/docker_commandline_code_executor.py @@ -45,6 +45,7 @@ def __init__( container_name: Optional[str] = None, timeout: int = 60, work_dir: Union[Path, str] = Path("."), + bind_dir: Optional[Union[Path, str]] = None, auto_remove: bool = True, stop_container: bool = True, ): @@ -67,6 +68,9 @@ def __init__( timeout (int, optional): The timeout for code execution. Defaults to 60. work_dir (Union[Path, str], optional): The working directory for the code execution. Defaults to Path("."). + bind_dir (Union[Path, str], optional): The directory that will be bound + to the code executor container. Useful for cases where you want to spawn + the container from within a container. Defaults to work_dir. auto_remove (bool, optional): If true, will automatically remove the Docker container when it is stopped. Defaults to True. stop_container (bool, optional): If true, will automatically stop the @@ -85,6 +89,11 @@ def __init__( work_dir.mkdir(exist_ok=True) + if bind_dir is None: + bind_dir = work_dir + elif isinstance(bind_dir, str): + bind_dir = Path(bind_dir) + client = docker.from_env() # Check if the image exists @@ -105,7 +114,7 @@ def __init__( entrypoint="/bin/sh", tty=True, auto_remove=auto_remove, - volumes={str(work_dir.resolve()): {"bind": "/workspace", "mode": "rw"}}, + volumes={str(bind_dir.resolve()): {"bind": "/workspace", "mode": "rw"}}, working_dir="/workspace", ) self._container.start() @@ -132,6 +141,7 @@ def cleanup() -> None: self._timeout = timeout self._work_dir: Path = work_dir + self._bind_dir: Path = bind_dir @property def timeout(self) -> int: @@ -143,6 +153,11 @@ def work_dir(self) -> Path: """(Experimental) The working directory for the code execution.""" return self._work_dir + @property + def bind_dir(self) -> Path: + """(Experimental) The binding directory for the code execution container.""" + return self._bind_dir + @property def code_extractor(self) -> CodeExtractor: """(Experimental) Export a code extractor that can be used by an agent.""" diff --git a/website/docs/topics/code-execution/cli-code-executor.ipynb b/website/docs/topics/code-execution/cli-code-executor.ipynb index d8b45bfe6a0..69df79754d0 100644 --- a/website/docs/topics/code-execution/cli-code-executor.ipynb +++ b/website/docs/topics/code-execution/cli-code-executor.ipynb @@ -73,7 +73,9 @@ "-v /var/run/docker.sock:/var/run/docker.sock\n", "```\n", "\n", - "This will allow the AutoGen container to spawn and control sibling containers on the host." + "This will allow the AutoGen container to spawn and control sibling containers on the host.\n", + "\n", + "If you need to bind a working directory to the AutoGen container but the directory belongs to your host machine, use the `bind_dir` parameter. This will allow the main AutoGen container to bind the *host* directory to the new spawned containers and allow it to access the files within the said directory. If the `bind_dir` is not specified, it will fallback to `work_dir`." ] }, { From d7dda9be09b5ab631ca84fa6adf27b3277dcb686 Mon Sep 17 00:00:00 2001 From: Xiaoyun Zhang Date: Tue, 30 Apr 2024 14:08:47 -0700 Subject: [PATCH 23/30] use conditional check to replace path filter in build and dotnet-ci workflow (#2546) * use conditional check * update --- .github/workflows/build.yml | 37 ++++++++++++++++++++++++++---- .github/workflows/dotnet-build.yml | 19 +++++++++++++-- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ce3654e5868..3ec01d7a022 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,11 +6,6 @@ name: Build on: push: branches: ["main"] - paths: - - "autogen/**" - - "test/**" - - ".github/workflows/build.yml" - - "setup.py" pull_request: branches: ["main"] merge_group: @@ -21,7 +16,39 @@ concurrency: cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} permissions: {} jobs: + paths-filter: + runs-on: ubuntu-latest + outputs: + hasChanges: ${{ steps.filter.outputs.autogen == 'true' || steps.filter.outputs.test == 'true' || steps.filter.outputs.workflows == 'true' || steps.filter.outputs.setup == 'true' }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + autogen: + - "autogen/**" + test: + - "test/**" + workflows: + - ".github/workflows/build.yml" + setup: + - "setup.py" + - name: autogen has changes + run: echo "autogen has changes" + if: steps.filter.outputs.autogen == 'true' + - name: test has changes + run: echo "test has changes" + if: steps.filter.outputs.test == 'true' + - name: workflows has changes + run: echo "workflows has changes" + if: steps.filter.outputs.workflows == 'true' + - name: setup has changes + run: echo "setup has changes" + if: steps.filter.outputs.setup == 'true' build: + needs: paths-filter + if: needs.paths-filter.outputs.hasChanges == 'true' runs-on: ${{ matrix.os }} env: AUTOGEN_USE_DOCKER: ${{ matrix.os != 'ubuntu-latest' && 'False' }} diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 332e656c9f1..046a5b6d79b 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -7,8 +7,6 @@ on: workflow_dispatch: pull_request: branches: [ "main" ] - paths: - - 'dotnet/**' push: branches: [ "main" ] @@ -21,9 +19,26 @@ permissions: packages: write jobs: + paths-filter: + runs-on: ubuntu-latest + outputs: + hasChanges: ${{ steps.filter.outputs.dotnet == 'true'}} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + dotnet: + - "dotnet/**" + - name: dotnet has changes + run: echo "dotnet has changes" + if: steps.filter.outputs.dotnet == 'true' build: name: Dotnet Build runs-on: ubuntu-latest + needs: paths-filter + if: needs.paths-filter.outputs.hasChanges == 'true' defaults: run: working-directory: dotnet From 83f9f3e73336fbad4d441e3db16c3a94a78d9443 Mon Sep 17 00:00:00 2001 From: Li Jiang Date: Thu, 2 May 2024 02:05:45 +0800 Subject: [PATCH 24/30] Fix chroma import error (#2557) * Fix chroma import error * fix format --- setup.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a93f1de07aa..a5481c90dfb 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,15 @@ "ipykernel>=6.29.0", ] -retrieve_chat = ["chromadb", "sentence_transformers", "pypdf", "ipython", "beautifulsoup4", "markdownify"] +retrieve_chat = [ + "protobuf==4.25.3", + "chromadb", + "sentence_transformers", + "pypdf", + "ipython", + "beautifulsoup4", + "markdownify", +] extra_require = { "test": [ From 5fdaf1a8c0b2b3cab1c017c2297666a8c55b07d7 Mon Sep 17 00:00:00 2001 From: asandez1 <161049415+asandez1@users.noreply.github.com> Date: Wed, 1 May 2024 18:14:09 -0300 Subject: [PATCH 25/30] Docker multilanguage executor saver with policy (#2522) * feat: update executor saver policy * feat: languages * feat: add test _cmd * fix: try catch * fix: log * fix: test docker mock * fix: invalid path test * fix: invalid path message * fix: invalid path message * fix: is_docker test * fix: delete old test * fix: cmd lang --- autogen/code_utils.py | 2 + .../docker_commandline_code_executor.py | 57 +++++++++++++------ test/coding/test_commandline_code_executor.py | 35 ++++++++++-- 3 files changed, 72 insertions(+), 22 deletions(-) diff --git a/autogen/code_utils.py b/autogen/code_utils.py index e556497388f..e1bc951f099 100644 --- a/autogen/code_utils.py +++ b/autogen/code_utils.py @@ -251,6 +251,8 @@ def _cmd(lang: str) -> str: return lang if lang in ["shell"]: return "sh" + if lang == "javascript": + return "node" if lang in ["ps1", "pwsh", "powershell"]: powershell_command = get_powershell_command() return powershell_command diff --git a/autogen/coding/docker_commandline_code_executor.py b/autogen/coding/docker_commandline_code_executor.py index 0828786a64f..6d8f4e309c8 100644 --- a/autogen/coding/docker_commandline_code_executor.py +++ b/autogen/coding/docker_commandline_code_executor.py @@ -8,7 +8,7 @@ from pathlib import Path from time import sleep from types import TracebackType -from typing import Any, List, Optional, Type, Union +from typing import Any, ClassVar, Dict, List, Optional, Type, Union import docker from docker.errors import ImageNotFound @@ -39,6 +39,20 @@ def _wait_for_ready(container: Any, timeout: int = 60, stop_time: float = 0.1) - class DockerCommandLineCodeExecutor(CodeExecutor): + DEFAULT_EXECUTION_POLICY: ClassVar[Dict[str, bool]] = { + "bash": True, + "shell": True, + "sh": True, + "pwsh": True, + "powershell": True, + "ps1": True, + "python": True, + "javascript": False, + "html": False, + "css": False, + } + LANGUAGE_ALIASES: ClassVar[Dict[str, str]] = {"py": "python", "js": "javascript"} + def __init__( self, image: str = "python:3-slim", @@ -48,6 +62,7 @@ def __init__( bind_dir: Optional[Union[Path, str]] = None, auto_remove: bool = True, stop_container: bool = True, + execution_policies: Optional[Dict[str, bool]] = None, ): """(Experimental) A code executor class that executes code through a command line environment in a Docker container. @@ -80,13 +95,11 @@ def __init__( Raises: ValueError: On argument error, or if the container fails to start. """ - if timeout < 1: raise ValueError("Timeout must be greater than or equal to 1.") if isinstance(work_dir, str): work_dir = Path(work_dir) - work_dir.mkdir(exist_ok=True) if bind_dir is None: @@ -95,7 +108,6 @@ def __init__( bind_dir = Path(bind_dir) client = docker.from_env() - # Check if the image exists try: client.images.get(image) @@ -127,7 +139,6 @@ def cleanup() -> None: container.stop() except docker.errors.NotFound: pass - atexit.unregister(cleanup) if stop_container: @@ -142,6 +153,9 @@ def cleanup() -> None: self._timeout = timeout self._work_dir: Path = work_dir self._bind_dir: Path = bind_dir + self.execution_policies = self.DEFAULT_EXECUTION_POLICY.copy() + if execution_policies is not None: + self.execution_policies.update(execution_policies) @property def timeout(self) -> int: @@ -179,35 +193,42 @@ def execute_code_blocks(self, code_blocks: List[CodeBlock]) -> CommandLineCodeRe files = [] last_exit_code = 0 for code_block in code_blocks: - lang = code_block.language + lang = self.LANGUAGE_ALIASES.get(code_block.language.lower(), code_block.language.lower()) + if lang not in self.DEFAULT_EXECUTION_POLICY: + outputs.append(f"Unsupported language {lang}\n") + last_exit_code = 1 + break + + execute_code = self.execution_policies.get(lang, False) code = silence_pip(code_block.code, lang) + # Check if there is a filename comment try: - # Check if there is a filename comment - filename = _get_file_name_from_content(code, Path("/workspace")) + filename = _get_file_name_from_content(code, self._work_dir) except ValueError: - return CommandLineCodeResult(exit_code=1, output="Filename is not in the workspace") + outputs.append("Filename is not in the workspace") + last_exit_code = 1 + break - if filename is None: - # create a file with an automatically generated name - code_hash = md5(code.encode()).hexdigest() - filename = f"tmp_code_{code_hash}.{'py' if lang.startswith('python') else lang}" + if not filename: + filename = f"tmp_code_{md5(code.encode()).hexdigest()}.{lang}" code_path = self._work_dir / filename with code_path.open("w", encoding="utf-8") as fout: fout.write(code) + files.append(code_path) - command = ["timeout", str(self._timeout), _cmd(lang), filename] + if not execute_code: + outputs.append(f"Code saved to {str(code_path)}\n") + continue + command = ["timeout", str(self._timeout), _cmd(lang), filename] result = self._container.exec_run(command) exit_code = result.exit_code output = result.output.decode("utf-8") if exit_code == 124: - output += "\n" - output += TIMEOUT_MSG - + output += "\n" + TIMEOUT_MSG outputs.append(output) - files.append(code_path) last_exit_code = exit_code if exit_code != 0: diff --git a/test/coding/test_commandline_code_executor.py b/test/coding/test_commandline_code_executor.py index 09562357235..0a0ded71e6c 100644 --- a/test/coding/test_commandline_code_executor.py +++ b/test/coding/test_commandline_code_executor.py @@ -143,16 +143,18 @@ def _test_execute_code(py_variant, executor: CodeExecutor) -> None: assert file_line.strip() == code_line.strip() -def test_local_commandline_code_executor_save_files() -> None: +@pytest.mark.parametrize("cls", classes_to_test) +def test_local_commandline_code_executor_save_files(cls) -> None: with tempfile.TemporaryDirectory() as temp_dir: - executor = LocalCommandLineCodeExecutor(work_dir=temp_dir) + executor = cls(work_dir=temp_dir) _test_save_files(executor, save_file_only=False) -def test_local_commandline_code_executor_save_files_only() -> None: +@pytest.mark.parametrize("cls", classes_to_test) +def test_local_commandline_code_executor_save_files_only(cls) -> None: with tempfile.TemporaryDirectory() as temp_dir: # Using execution_policies to specify that no languages should execute - executor = LocalCommandLineCodeExecutor( + executor = cls( work_dir=temp_dir, execution_policies={"python": False, "bash": False, "javascript": False, "html": False, "css": False}, ) @@ -255,6 +257,31 @@ def test_docker_commandline_code_executor_restart() -> None: assert result.exit_code == 0 +@pytest.mark.skipif( + skip_docker_test, + reason="docker is not running or requested to skip docker tests", +) +def test_policy_override(): + default_policy = DockerCommandLineCodeExecutor.DEFAULT_EXECUTION_POLICY + custom_policy = { + "python": False, + "javascript": True, + } + + executor = DockerCommandLineCodeExecutor(execution_policies=custom_policy) + + assert not executor.execution_policies["python"], "Python execution should be disabled" + assert executor.execution_policies["javascript"], "JavaScript execution should be enabled" + + for lang, should_execute in default_policy.items(): + if lang not in custom_policy: + assert executor.execution_policies[lang] == should_execute, f"Policy for {lang} should not be changed" + + assert set(executor.execution_policies.keys()) == set( + default_policy.keys() + ), "Execution policies should only contain known languages" + + def _test_restart(executor: CodeExecutor) -> None: # Check warning. with pytest.warns(UserWarning, match=r".*No action is taken."): From e3ccf228e23940f7e27c28ea71dc3167fd6cb6e1 Mon Sep 17 00:00:00 2001 From: Xiaoyun Zhang Date: Wed, 1 May 2024 15:44:27 -0700 Subject: [PATCH 26/30] Adding an action to set workflow as success when no change is made in target paths (#2553) * update * Update build.yml * Update build.yml * test workflow * add build_check * update --- .github/workflows/build.yml | 31 +++++++++++++++++++++++++++++- .github/workflows/dotnet-build.yml | 2 +- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3ec01d7a022..d06999db34c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,7 @@ jobs: test: - "test/**" workflows: - - ".github/workflows/build.yml" + - ".github/workflows/**" setup: - "setup.py" - name: autogen has changes @@ -109,3 +109,32 @@ jobs: with: file: ./coverage.xml flags: unittests + build-check: + if: always() + runs-on: ubuntu-latest + needs: [build] + steps: + - name: Get Date + shell: bash + run: | + echo "date=$(date +'%m/%d/%Y %H:%M:%S')" >> "$GITHUB_ENV" + + - name: Run Type is ${{ github.event_name }} + if: ${{ github.event_name != 'schedule' && github.event_name != 'workflow_dispatch'}} + shell: bash + run: | + echo "run_type=${{ github.event_name }}" >> "$GITHUB_ENV" + + - name: Fail workflow if build failed + id: check_build_failed + if: contains(join(needs.*.result, ','), 'failure') + uses: actions/github-script@v6 + with: + script: core.setFailed('Build Failed!') + + - name: Fail workflow if build cancelled + id: check_build_cancelled + if: contains(join(needs.*.result, ','), 'cancelled') + uses: actions/github-script@v6 + with: + script: core.setFailed('Build Cancelled!') diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 046a5b6d79b..1735e71a8f1 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -67,7 +67,7 @@ jobs: defaults: run: working-directory: dotnet - if: success() && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dotnet') + if: success() && (github.ref == 'refs/heads/main') needs: build steps: - uses: actions/checkout@v4 From 1f501b210c62520ce77fff61e379ceff62fb485b Mon Sep 17 00:00:00 2001 From: Xiaoyun Zhang Date: Wed, 1 May 2024 18:09:45 -0700 Subject: [PATCH 27/30] Update dotnet-build.yml to add merge_group trigger (#2567) * Update dotnet-build.yml * Update dotnet-build.yml --- .github/workflows/dotnet-build.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 1735e71a8f1..2e679412f63 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -9,6 +9,8 @@ on: branches: [ "main" ] push: branches: [ "main" ] + merge_group: + types: [checks_requested] concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }} @@ -31,9 +33,14 @@ jobs: filters: | dotnet: - "dotnet/**" + workflows: + - ".github/workflows/**" - name: dotnet has changes run: echo "dotnet has changes" if: steps.filter.outputs.dotnet == 'true' + - name: workflows has changes + run: echo "workflows has changes" + if: steps.filter.outputs.workflows == 'true' build: name: Dotnet Build runs-on: ubuntu-latest @@ -157,4 +164,4 @@ jobs: ls -R ./output/nightly dotnet nuget push --api-key ${{ secrets.MYGET_TOKEN }} --source "https://www.myget.org/F/agentchat/api/v3/index.json" ./output/nightly/*.nupkg --skip-duplicate env: - MYGET_TOKEN: ${{ secrets.MYGET_TOKEN }} \ No newline at end of file + MYGET_TOKEN: ${{ secrets.MYGET_TOKEN }} From f4a07ff0eda2f9c9531e9ba20d1fe36bf1ccf33b Mon Sep 17 00:00:00 2001 From: David Luong Date: Wed, 1 May 2024 21:30:42 -0400 Subject: [PATCH 28/30] [.Net] Support raw-data in ImageMessage (#2552) * update * add sample project * revert notebook change back * update * update interactive version * add nuget package * refactor Message * update example * add azure nightly build pipeline * Set up CI with Azure Pipelines [skip ci] * Update nightly-build.yml for Azure Pipelines * add dotnet interactive package * add dotnet interactive package * update pipeline * add nuget feed back * remove dotnet-tool feed * remove dotnet-tool feed comment * update pipeline * update build name * Update nightly-build.yml * Delete .github/workflows/dotnet-ci.yml * update * add working_dir to use step * add initateChat api * update oai package * Update dotnet-build.yml * Update dotnet-run-openai-test-and-notebooks.yml * update build workflow * update build workflow * update nuget feed * update nuget feed * update aoai and sk version * Update InteractiveService.cs * add support for GPT 4V * add DalleAndGPT4V example * update example * add user proxy agent * add readme * bump version * update example * add dotnet interactive hook * update * udpate tests * add website * update index.md * add docs * update doc * move sk dependency out of core package * udpate doc * Update Use-function-call.md * add type safe function call document * update doc * update doc * add dock * Update Use-function-call.md * add GenerateReplyOptions * remove IChatLLM * update version * update doc * update website * add sample * fix link * add middleware agent * clean up doc * bump version * update doc * update * add Other Language * remove warnings * add sign.props * add sign step * fix pipelien * auth * real sign * disable PR trigger * update * disable PR trigger * use microbuild machine * update build pipeline to add publish to internal feed * add internal feed * fix build pipeline * add dotnet prefix * update ci * add build number * update run number * update source * update token * update * remove adding source * add publish to github package * try again * try again * ask for write pacakge * disable package when branch is not main * update * implement streaming agent * add test for streaming function call * update * fix #1588 * enable PR check for dotnet branch * add website readme * only publish to dotnet feed when pushing to dotnet branch * remove openai-test-and-notebooks workflow * update readme * update readme * update workflow * update getting-start * upgrade test and sample proejct to use .net 8 * fix global.json format && make loadFromConfig API internal only before implementing * update * add support for LM studio * add doc * Update README.md * add push and workflow_dispatch trigger * disable PR for main * add dotnet env * Update Installation.md * add nuget * refer to newtonsoft 13 * update branch to dotnet in docfx * Update Installation.md * pull out HumanInputMiddleware and FunctionCallMiddleware * fix tests * add link to sample folder * refactor message * refactor over IMessage * add more tests * add more test * fix build error * rename header * add semantic kernel project * update sk example * update dotnet version * add LMStudio function call example * rename LLaMAFunctin * remove dotnet run openai test and notebook workflow * add FunctionContract and test * update doc * add documents * add workflow * update * update sample * fix warning in test * reult length can be less then maximumOutputToKeep (#1804) * merge with main * add option to retrieve inner agent and middlewares from MiddlewareAgent * update doc * adjust namespace * update readme * fix test * use IMessage * more updates * update * fix test * add comments * use FunctionContract to replace FunctionDefinition * move AutoGen contrac to AutoGen.Core * update installation * refactor streamingAgent by adding StreamingMessage type * update sample * update samples * update * update * add test * fix test * bump version * add openaichat test * update * Update Example03_Agent_FunctionCall.cs * [.Net] improve docs (#1862) * add doc * add doc * add doc * add doc * add doc * add doc * update * fix test error * fix some error * fix test * fix test * add more tests * edits --------- Co-authored-by: ekzhu * [.Net] Add fill form example (#1911) * add form filler example * update * fix ci error * [.Net] Add using AutoGen.Core in source generator (#1983) * fix using namespace bug in source generator * remove using in sourcegenerator test * disable PR test * Add .idea to .gitignore (#1988) * [.Net] publish to nuget.org feed (#1987) * publish to nuget * update ci * update dotnet-release * update release pipeline * add source * remove empty symbol package * update pipeline * remove tag * update installation guide * [.Net] Rename some classes && APIs based on doc review (#1980) * rename sequential group chat to round robin group chat * rename to sendInstruction * rename workflow to graph * rename some api * bump version * move Graph to GroupChat folder * rename fill application example * [.Net] Improve package description (#2161) * add discord link and update package description * Update getting-start.md * [.Net] Fix document comment from the most recent AutoGen.Net engineer sync (#2231) * update * rename RegisterPrintMessageHook to RegisterPrintMessage * update website * update update.md * fix link error * [.Net] Enable JsonMode and deterministic output in AutoGen.OpenAI OpenAIChatAgent (#2347) * update openai version && add sample for json output * add example in web * update update.md * update image url * [.Net] Add AutoGen.Mistral package (#2330) * add mstral client * enable streaming support * add mistralClientAgent * add test for function call * add extension * add support for toolcall and toolcall result message * add support for aggregate message * implement streaming function call * track (#2471) * [.Net] add mistral example (#2482) * update existing examples to use messageCOnnector * add overview * add function call document * add example 14 * add mistral token count usage example * update version * Update dotnet-release.yml (#2488) * update * revert gitattributes * WIP : Binary ImageMessage * WIP : Able to pass unit test * Add example, cover more usages * Rename File --------- Co-authored-by: XiaoYun Zhang Co-authored-by: Xiaoyun Zhang Co-authored-by: mhensen Co-authored-by: ekzhu Co-authored-by: Krzysztof Kasprowicz <60486987+Krzysztof318@users.noreply.github.com> Co-authored-by: luongdavid --- .../AutoGen.BasicSample.csproj | 6 ++ .../Example15_GPT4V_BinaryDataImageMessage.cs | 62 +++++++++++++++++++ .../ImageResources/square.png | 3 + dotnet/src/AutoGen.Core/AutoGen.Core.csproj | 1 + .../src/AutoGen.Core/Message/ImageMessage.cs | 31 +++++++++- .../DTOs/ChatCompletionResponse.cs | 2 +- dotnet/src/AutoGen.Mistral/DTOs/Error.cs | 2 +- dotnet/src/AutoGen.Mistral/DTOs/Model.cs | 2 +- .../Extension/MessageExtension.cs | 4 +- .../OpenAIChatRequestMessageConnector.cs | 4 +- ...manticKernelChatMessageContentConnector.cs | 7 +-- .../AutoGen.Tests/ApprovalTests/square.png | 3 + .../test/AutoGen.Tests/AutoGen.Tests.csproj | 6 ++ dotnet/test/AutoGen.Tests/BasicSampleTest.cs | 6 ++ dotnet/test/AutoGen.Tests/SingleAgentTest.cs | 14 +++++ 15 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 dotnet/sample/AutoGen.BasicSamples/Example15_GPT4V_BinaryDataImageMessage.cs create mode 100644 dotnet/sample/AutoGen.BasicSamples/ImageResources/square.png create mode 100644 dotnet/test/AutoGen.Tests/ApprovalTests/square.png diff --git a/dotnet/sample/AutoGen.BasicSamples/AutoGen.BasicSample.csproj b/dotnet/sample/AutoGen.BasicSamples/AutoGen.BasicSample.csproj index c4e41261933..3c2b5166988 100644 --- a/dotnet/sample/AutoGen.BasicSamples/AutoGen.BasicSample.csproj +++ b/dotnet/sample/AutoGen.BasicSamples/AutoGen.BasicSample.csproj @@ -16,4 +16,10 @@ + + + + PreserveNewest + + diff --git a/dotnet/sample/AutoGen.BasicSamples/Example15_GPT4V_BinaryDataImageMessage.cs b/dotnet/sample/AutoGen.BasicSamples/Example15_GPT4V_BinaryDataImageMessage.cs new file mode 100644 index 00000000000..7a3422cb863 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/Example15_GPT4V_BinaryDataImageMessage.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Example15_ImageMessage.cs + +using AutoGen.Core; +using AutoGen.OpenAI; + +namespace AutoGen.BasicSample; + +/// +/// This example shows usage of ImageMessage. The image is loaded as BinaryData and sent to GPT-4V +///
    +///
    +/// Add additional images to the ImageResources to load and send more images to GPT-4V +///
    +public static class Example15_GPT4V_BinaryDataImageMessage +{ + private static readonly string ImageResourcePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ImageResources"); + + private static Dictionary _mediaTypeMappings = new() + { + { ".png", "image/png" }, + { ".jpeg", "image/jpeg" }, + { ".jpg", "image/jpeg" }, + { ".gif", "image/gif" }, + { ".webp", "image/webp" } + }; + + public static async Task RunAsync() + { + var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); + var openAiConfig = new OpenAIConfig(openAIKey, "gpt-4-vision-preview"); + + var visionAgent = new GPTAgent( + name: "gpt", + systemMessage: "You are a helpful AI assistant", + config: openAiConfig, + temperature: 0); + + List messages = + [new TextMessage(Role.User, "What is this image?", from: "user")]; + AddMessagesFromResource(ImageResourcePath, messages); + + var multiModalMessage = new MultiModalMessage(Role.User, messages, from: "user"); + var response = await visionAgent.SendAsync(multiModalMessage); + } + + private static void AddMessagesFromResource(string imageResourcePath, List messages) + { + foreach (string file in Directory.GetFiles(imageResourcePath)) + { + if (!_mediaTypeMappings.TryGetValue(Path.GetExtension(file).ToLowerInvariant(), out var mediaType)) + continue; + + using var fs = new FileStream(file, FileMode.Open, FileAccess.Read); + var ms = new MemoryStream(); + fs.CopyTo(ms); + ms.Seek(0, SeekOrigin.Begin); + var imageData = BinaryData.FromStream(ms, mediaType); + messages.Add(new ImageMessage(Role.Assistant, imageData, from: "user")); + } + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/ImageResources/square.png b/dotnet/sample/AutoGen.BasicSamples/ImageResources/square.png new file mode 100644 index 00000000000..afb4f4cd4df --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/ImageResources/square.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8323d0b8eceb752e14c29543b2e28bb2fc648ed9719095c31b7708867a4dc918 +size 491 diff --git a/dotnet/src/AutoGen.Core/AutoGen.Core.csproj b/dotnet/src/AutoGen.Core/AutoGen.Core.csproj index 409b6bc1aaf..ebbec3f0a46 100644 --- a/dotnet/src/AutoGen.Core/AutoGen.Core.csproj +++ b/dotnet/src/AutoGen.Core/AutoGen.Core.csproj @@ -16,6 +16,7 @@ + diff --git a/dotnet/src/AutoGen.Core/Message/ImageMessage.cs b/dotnet/src/AutoGen.Core/Message/ImageMessage.cs index 18ceea0d111..1239785c411 100644 --- a/dotnet/src/AutoGen.Core/Message/ImageMessage.cs +++ b/dotnet/src/AutoGen.Core/Message/ImageMessage.cs @@ -21,14 +21,41 @@ public ImageMessage(Role role, Uri uri, string? from = null) this.Url = uri.ToString(); } + public ImageMessage(Role role, BinaryData data, string? from = null) + { + if (data.IsEmpty) + { + throw new ArgumentException("Data cannot be empty", nameof(data)); + } + + if (string.IsNullOrWhiteSpace(data.MediaType)) + { + throw new ArgumentException("MediaType is needed for DataUri Images", nameof(data)); + } + + this.Role = role; + this.From = from; + this.Data = data; + } + public Role Role { get; set; } - public string Url { get; set; } + public string? Url { get; set; } public string? From { get; set; } + public BinaryData? Data { get; set; } + + public string BuildDataUri() + { + if (this.Data is null) + throw new NullReferenceException($"{nameof(Data)}"); + + return $"data:{this.Data.MediaType};base64,{Convert.ToBase64String(this.Data.ToArray())}"; + } + public override string ToString() { - return $"ImageMessage({this.Role}, {this.Url}, {this.From})"; + return $"ImageMessage({this.Role}, {(this.Data != null ? BuildDataUri() : this.Url) ?? string.Empty}, {this.From})"; } } diff --git a/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs b/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs index ff241f8d340..13e29e7139b 100644 --- a/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs +++ b/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // ChatCompletionResponse.cs using System.Collections.Generic; diff --git a/dotnet/src/AutoGen.Mistral/DTOs/Error.cs b/dotnet/src/AutoGen.Mistral/DTOs/Error.cs index 77eb2d341fb..8bddcfc776c 100644 --- a/dotnet/src/AutoGen.Mistral/DTOs/Error.cs +++ b/dotnet/src/AutoGen.Mistral/DTOs/Error.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Error.cs using System.Text.Json.Serialization; diff --git a/dotnet/src/AutoGen.Mistral/DTOs/Model.cs b/dotnet/src/AutoGen.Mistral/DTOs/Model.cs index 915d2f737ec..70a4b3c997d 100644 --- a/dotnet/src/AutoGen.Mistral/DTOs/Model.cs +++ b/dotnet/src/AutoGen.Mistral/DTOs/Model.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Model.cs using System; diff --git a/dotnet/src/AutoGen.OpenAI/Extension/MessageExtension.cs b/dotnet/src/AutoGen.OpenAI/Extension/MessageExtension.cs index 92e0f3776f5..b3dfb1e8668 100644 --- a/dotnet/src/AutoGen.OpenAI/Extension/MessageExtension.cs +++ b/dotnet/src/AutoGen.OpenAI/Extension/MessageExtension.cs @@ -77,7 +77,7 @@ public static IEnumerable ToOpenAIChatRequestMessage(this IA else if (message is ImageMessage imageMessage) { // multi-modal - var msg = new ChatRequestUserMessage(new ChatMessageImageContentItem(new Uri(imageMessage.Url))); + var msg = new ChatRequestUserMessage(new ChatMessageImageContentItem(new Uri(imageMessage.Url ?? imageMessage.BuildDataUri()))); return [msg]; } @@ -101,7 +101,7 @@ public static IEnumerable ToOpenAIChatRequestMessage(this IA return m switch { TextMessage textMessage => new ChatMessageTextContentItem(textMessage.Content), - ImageMessage imageMessage => new ChatMessageImageContentItem(new Uri(imageMessage.Url)), + ImageMessage imageMessage => new ChatMessageImageContentItem(new Uri(imageMessage.Url ?? imageMessage.BuildDataUri())), _ => throw new ArgumentException($"Unknown message type: {m.GetType()}") }; }); diff --git a/dotnet/src/AutoGen.OpenAI/Middleware/OpenAIChatRequestMessageConnector.cs b/dotnet/src/AutoGen.OpenAI/Middleware/OpenAIChatRequestMessageConnector.cs index c1581cbec08..1276e93f9fb 100644 --- a/dotnet/src/AutoGen.OpenAI/Middleware/OpenAIChatRequestMessageConnector.cs +++ b/dotnet/src/AutoGen.OpenAI/Middleware/OpenAIChatRequestMessageConnector.cs @@ -336,7 +336,7 @@ private IEnumerable ProcessIncomingMessagesForOther(TextMess private IEnumerable ProcessIncomingMessagesForOther(ImageMessage message) { return new[] { new ChatRequestUserMessage([ - new ChatMessageImageContentItem(new Uri(message.Url)), + new ChatMessageImageContentItem(new Uri(message.Url ?? message.BuildDataUri())), ])}; } @@ -345,7 +345,7 @@ private IEnumerable ProcessIncomingMessagesForOther(MultiMod IEnumerable items = message.Content.Select(ci => ci switch { TextMessage text => new ChatMessageTextContentItem(text.Content), - ImageMessage image => new ChatMessageImageContentItem(new Uri(image.Url)), + ImageMessage image => new ChatMessageImageContentItem(new Uri(image.Url ?? image.BuildDataUri())), _ => throw new NotImplementedException(), }); diff --git a/dotnet/src/AutoGen.SemanticKernel/Middleware/SemanticKernelChatMessageContentConnector.cs b/dotnet/src/AutoGen.SemanticKernel/Middleware/SemanticKernelChatMessageContentConnector.cs index e4b7527cd05..557683c9615 100644 --- a/dotnet/src/AutoGen.SemanticKernel/Middleware/SemanticKernelChatMessageContentConnector.cs +++ b/dotnet/src/AutoGen.SemanticKernel/Middleware/SemanticKernelChatMessageContentConnector.cs @@ -92,7 +92,7 @@ private IMessage PostProcessMessage(IMessage messageEnvelope { TextContent txt => new TextMessage(Role.Assistant, txt.Text!, messageEnvelope.From), ImageContent img when img.Uri is Uri uri => new ImageMessage(Role.Assistant, uri.ToString(), from: messageEnvelope.From), - ImageContent img when img.Uri is null => throw new InvalidOperationException("ImageContent.Uri is null"), + ImageContent img when img.Data is ReadOnlyMemory data => new ImageMessage(Role.Assistant, BinaryData.FromBytes(data), from: messageEnvelope.From), _ => throw new InvalidOperationException("Unsupported content type"), }); @@ -185,9 +185,8 @@ private IEnumerable ProcessMessageForOthers(TextMessage mess private IEnumerable ProcessMessageForOthers(ImageMessage message) { - var imageContent = new ImageContent(new Uri(message.Url)); var collectionItems = new ChatMessageContentItemCollection(); - collectionItems.Add(imageContent); + collectionItems.Add(new ImageContent(new Uri(message.Url ?? message.BuildDataUri()))); return [new ChatMessageContent(AuthorRole.User, collectionItems)]; } @@ -207,7 +206,7 @@ private IEnumerable ProcessMessageForOthers(MultiModalMessag } else if (item is ImageMessage imageContent) { - collections.Add(new ImageContent(new Uri(imageContent.Url))); + collections.Add(new ImageContent(new Uri(imageContent.Url ?? imageContent.BuildDataUri()))); } else { diff --git a/dotnet/test/AutoGen.Tests/ApprovalTests/square.png b/dotnet/test/AutoGen.Tests/ApprovalTests/square.png new file mode 100644 index 00000000000..afb4f4cd4df --- /dev/null +++ b/dotnet/test/AutoGen.Tests/ApprovalTests/square.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8323d0b8eceb752e14c29543b2e28bb2fc648ed9719095c31b7708867a4dc918 +size 491 diff --git a/dotnet/test/AutoGen.Tests/AutoGen.Tests.csproj b/dotnet/test/AutoGen.Tests/AutoGen.Tests.csproj index f7e6b036506..9a7b07b34dd 100644 --- a/dotnet/test/AutoGen.Tests/AutoGen.Tests.csproj +++ b/dotnet/test/AutoGen.Tests/AutoGen.Tests.csproj @@ -21,4 +21,10 @@ + + + PreserveNewest + + + diff --git a/dotnet/test/AutoGen.Tests/BasicSampleTest.cs b/dotnet/test/AutoGen.Tests/BasicSampleTest.cs index 19de2bdef4b..b9eea67397c 100644 --- a/dotnet/test/AutoGen.Tests/BasicSampleTest.cs +++ b/dotnet/test/AutoGen.Tests/BasicSampleTest.cs @@ -68,6 +68,12 @@ public async Task DalleAndGPT4VTestAsync() await Example05_Dalle_And_GPT4V.RunAsync(); } + [ApiKeyFact("OPENAI_API_KEY")] + public async Task GPT4ImageMessage() + { + await Example15_GPT4V_BinaryDataImageMessage.RunAsync(); + } + public class ConsoleWriter : StringWriter { private ITestOutputHelper output; diff --git a/dotnet/test/AutoGen.Tests/SingleAgentTest.cs b/dotnet/test/AutoGen.Tests/SingleAgentTest.cs index d314b391bae..6dfb61761eb 100644 --- a/dotnet/test/AutoGen.Tests/SingleAgentTest.cs +++ b/dotnet/test/AutoGen.Tests/SingleAgentTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using AutoGen.OpenAI; @@ -80,11 +81,24 @@ public async Task GPTAgentVisionTestAsync() var imageMessage = new ImageMessage(Role.User, imageUri, from: "user"); + string imagePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ApprovalTests", "square.png"); + ImageMessage imageMessageData; + using (var fs = new FileStream(imagePath, FileMode.Open, FileAccess.Read)) + { + var ms = new MemoryStream(); + await fs.CopyToAsync(ms); + ms.Seek(0, SeekOrigin.Begin); + var imageData = await BinaryData.FromStreamAsync(ms, "image/png"); + imageMessageData = new ImageMessage(Role.Assistant, imageData, from: "user"); + } + IMessage[] messages = [ MessageEnvelope.Create(oaiMessage), multiModalMessage, imageMessage, + imageMessageData ]; + foreach (var message in messages) { var response = await visionAgent.SendAsync(message); From 3e693578bf503e3ec9ee22617cf67e9d5b22a7d1 Mon Sep 17 00:00:00 2001 From: David Luong Date: Wed, 1 May 2024 21:36:34 -0400 Subject: [PATCH 29/30] [.NET] Return ChatCompletions instead of ChatResponseMessage for token usage. (#2545) * update * update * update * update * update * add sample project * revert notebook change back * update * update interactive version * add nuget package * refactor Message * update example * add azure nightly build pipeline * Set up CI with Azure Pipelines [skip ci] * Update nightly-build.yml for Azure Pipelines * add dotnet interactive package * add dotnet interactive package * update pipeline * add nuget feed back * remove dotnet-tool feed * remove dotnet-tool feed comment * update pipeline * update build name * Update nightly-build.yml * Delete .github/workflows/dotnet-ci.yml * update * add working_dir to use step * add initateChat api * update oai package * Update dotnet-build.yml * Update dotnet-run-openai-test-and-notebooks.yml * update build workflow * update build workflow * update nuget feed * update nuget feed * update aoai and sk version * Update InteractiveService.cs * add support for GPT 4V * add DalleAndGPT4V example * update example * add user proxy agent * add readme * bump version * update example * add dotnet interactive hook * update * udpate tests * add website * update index.md * add docs * update doc * move sk dependency out of core package * udpate doc * Update Use-function-call.md * add type safe function call document * update doc * update doc * add dock * Update Use-function-call.md * add GenerateReplyOptions * remove IChatLLM * update version * update doc * update website * add sample * fix link * add middleware agent * clean up doc * bump version * update doc * update * add Other Language * remove warnings * add sign.props * add sign step * fix pipelien * auth * real sign * disable PR trigger * update * disable PR trigger * use microbuild machine * update build pipeline to add publish to internal feed * add internal feed * fix build pipeline * add dotnet prefix * update ci * add build number * update run number * update source * update token * update * remove adding source * add publish to github package * try again * try again * ask for write pacakge * disable package when branch is not main * update * implement streaming agent * add test for streaming function call * update * fix #1588 * enable PR check for dotnet branch * add website readme * only publish to dotnet feed when pushing to dotnet branch * remove openai-test-and-notebooks workflow * update readme * update readme * update workflow * update getting-start * upgrade test and sample proejct to use .net 8 * fix global.json format && make loadFromConfig API internal only before implementing * update * add support for LM studio * add doc * Update README.md * add push and workflow_dispatch trigger * disable PR for main * add dotnet env * Update Installation.md * add nuget * refer to newtonsoft 13 * update branch to dotnet in docfx * Update Installation.md * pull out HumanInputMiddleware and FunctionCallMiddleware * fix tests * add link to sample folder * refactor message * refactor over IMessage * add more tests * add more test * fix build error * rename header * add semantic kernel project * update sk example * update dotnet version * add LMStudio function call example * rename LLaMAFunctin * remove dotnet run openai test and notebook workflow * add FunctionContract and test * update doc * add documents * add workflow * update * update sample * fix warning in test * reult length can be less then maximumOutputToKeep (#1804) * merge with main * add option to retrieve inner agent and middlewares from MiddlewareAgent * update doc * adjust namespace * update readme * fix test * use IMessage * more updates * update * fix test * add comments * use FunctionContract to replace FunctionDefinition * move AutoGen contrac to AutoGen.Core * update installation * refactor streamingAgent by adding StreamingMessage type * update sample * update samples * update * update * add test * fix test * bump version * add openaichat test * update * Update Example03_Agent_FunctionCall.cs * [.Net] improve docs (#1862) * add doc * add doc * add doc * add doc * add doc * add doc * update * fix test error * fix some error * fix test * fix test * add more tests * edits --------- Co-authored-by: ekzhu * [.Net] Add fill form example (#1911) * add form filler example * update * fix ci error * [.Net] Add using AutoGen.Core in source generator (#1983) * fix using namespace bug in source generator * remove using in sourcegenerator test * disable PR test * Add .idea to .gitignore (#1988) * [.Net] publish to nuget.org feed (#1987) * publish to nuget * update ci * update dotnet-release * update release pipeline * add source * remove empty symbol package * update pipeline * remove tag * update installation guide * [.Net] Rename some classes && APIs based on doc review (#1980) * rename sequential group chat to round robin group chat * rename to sendInstruction * rename workflow to graph * rename some api * bump version * move Graph to GroupChat folder * rename fill application example * [.Net] Improve package description (#2161) * add discord link and update package description * Update getting-start.md * [.Net] Fix document comment from the most recent AutoGen.Net engineer sync (#2231) * update * rename RegisterPrintMessageHook to RegisterPrintMessage * update website * update update.md * fix link error * [.Net] Enable JsonMode and deterministic output in AutoGen.OpenAI OpenAIChatAgent (#2347) * update openai version && add sample for json output * add example in web * update update.md * update image url * [.Net] Add AutoGen.Mistral package (#2330) * add mstral client * enable streaming support * add mistralClientAgent * add test for function call * add extension * add support for toolcall and toolcall result message * add support for aggregate message * implement streaming function call * track (#2471) * [.Net] add mistral example (#2482) * update existing examples to use messageCOnnector * add overview * add function call document * add example 14 * add mistral token count usage example * update version * Update dotnet-release.yml (#2488) * update * revert gitattributes * Return ChatCompletions instead of ChatResponseMessage for token usage. --------- Co-authored-by: XiaoYun Zhang Co-authored-by: Xiaoyun Zhang Co-authored-by: mhensen Co-authored-by: ekzhu Co-authored-by: Krzysztof Kasprowicz <60486987+Krzysztof318@users.noreply.github.com> Co-authored-by: luongdavid --- .../AutoGen.OpenAI/Agent/OpenAIChatAgent.cs | 4 ++-- .../OpenAIChatRequestMessageConnector.cs | 18 ++++++++++++++---- .../test/AutoGen.Tests/OpenAIChatAgentTest.cs | 7 ++++--- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/dotnet/src/AutoGen.OpenAI/Agent/OpenAIChatAgent.cs b/dotnet/src/AutoGen.OpenAI/Agent/OpenAIChatAgent.cs index ecebe7fc3fa..487a361d7de 100644 --- a/dotnet/src/AutoGen.OpenAI/Agent/OpenAIChatAgent.cs +++ b/dotnet/src/AutoGen.OpenAI/Agent/OpenAIChatAgent.cs @@ -84,7 +84,7 @@ public async Task GenerateReplyAsync( var settings = this.CreateChatCompletionsOptions(options, messages); var reply = await this.openAIClient.GetChatCompletionsAsync(settings, cancellationToken); - return new MessageEnvelope(reply.Value.Choices.First().Message, from: this.Name); + return new MessageEnvelope(reply, from: this.Name); } public Task> GenerateStreamingReplyAsync( @@ -101,7 +101,7 @@ private async IAsyncEnumerable StreamingReplyAsync( [EnumeratorCancellation] CancellationToken cancellationToken = default) { var settings = this.CreateChatCompletionsOptions(options, messages); - var response = await this.openAIClient.GetChatCompletionsStreamingAsync(settings); + var response = await this.openAIClient.GetChatCompletionsStreamingAsync(settings, cancellationToken); await foreach (var update in response.WithCancellation(cancellationToken)) { if (update.ChoiceIndex > 0) diff --git a/dotnet/src/AutoGen.OpenAI/Middleware/OpenAIChatRequestMessageConnector.cs b/dotnet/src/AutoGen.OpenAI/Middleware/OpenAIChatRequestMessageConnector.cs index 1276e93f9fb..118d99703ab 100644 --- a/dotnet/src/AutoGen.OpenAI/Middleware/OpenAIChatRequestMessageConnector.cs +++ b/dotnet/src/AutoGen.OpenAI/Middleware/OpenAIChatRequestMessageConnector.cs @@ -98,6 +98,7 @@ public IMessage PostProcessMessage(IMessage message) Message => message, AggregateMessage => message, IMessage m => PostProcessMessage(m), + IMessage m => PostProcessMessage(m), _ => throw new InvalidOperationException("The type of message is not supported. Must be one of TextMessage, ImageMessage, MultiModalMessage, ToolCallMessage, ToolCallResultMessage, Message, IMessage, AggregateMessage"), }; } @@ -129,15 +130,24 @@ public IMessage PostProcessMessage(IMessage message) private IMessage PostProcessMessage(IMessage message) { - var chatResponseMessage = message.Content; + return PostProcessMessage(message.Content, message.From); + } + + private IMessage PostProcessMessage(IMessage message) + { + return PostProcessMessage(message.Content.Choices[0].Message, message.From); + } + + private IMessage PostProcessMessage(ChatResponseMessage chatResponseMessage, string? from) + { if (chatResponseMessage.Content is string content) { - return new TextMessage(Role.Assistant, content, message.From); + return new TextMessage(Role.Assistant, content, from); } if (chatResponseMessage.FunctionCall is FunctionCall functionCall) { - return new ToolCallMessage(functionCall.Name, functionCall.Arguments, message.From); + return new ToolCallMessage(functionCall.Name, functionCall.Arguments, from); } if (chatResponseMessage.ToolCalls.Where(tc => tc is ChatCompletionsFunctionToolCall).Any()) @@ -148,7 +158,7 @@ private IMessage PostProcessMessage(IMessage message) var toolCalls = functionToolCalls.Select(tc => new ToolCall(tc.Name, tc.Arguments)); - return new ToolCallMessage(toolCalls, message.From); + return new ToolCallMessage(toolCalls, from); } throw new InvalidOperationException("Invalid ChatResponseMessage"); diff --git a/dotnet/test/AutoGen.Tests/OpenAIChatAgentTest.cs b/dotnet/test/AutoGen.Tests/OpenAIChatAgentTest.cs index 8626618fea7..a4753b66871 100644 --- a/dotnet/test/AutoGen.Tests/OpenAIChatAgentTest.cs +++ b/dotnet/test/AutoGen.Tests/OpenAIChatAgentTest.cs @@ -41,9 +41,10 @@ public async Task BasicConversationTestAsync() var chatMessageContent = MessageEnvelope.Create(new ChatRequestUserMessage("Hello")); var reply = await openAIChatAgent.SendAsync(chatMessageContent); - reply.Should().BeOfType>(); - reply.As>().From.Should().Be("assistant"); - reply.As>().Content.Role.Should().Be(ChatRole.Assistant); + reply.Should().BeOfType>(); + reply.As>().From.Should().Be("assistant"); + reply.As>().Content.Choices.First().Message.Role.Should().Be(ChatRole.Assistant); + reply.As>().Content.Usage.TotalTokens.Should().BeGreaterThan(0); // test streaming var streamingReply = await openAIChatAgent.GenerateStreamingReplyAsync(new[] { chatMessageContent }); From 10bb25ba7d933f83e6f2f28a024e7a163cd77f5d Mon Sep 17 00:00:00 2001 From: Justin Trugman Date: Thu, 2 May 2024 12:56:15 -0400 Subject: [PATCH 30/30] Function Calling with GPTAssistantAgent (#2375) * Function Calling with GPTAssistantAgent * Add Link to Notebook in Website * Add metadata to the notebook * formatting of H2 and H3 text * updated to new method of function calling * Run Pre-commit * utilize get_function_schema --- .../gpt_assistant_agent_function_call.ipynb | 566 ++++++++++++++++++ website/docs/Examples.md | 1 + 2 files changed, 567 insertions(+) create mode 100644 notebook/gpt_assistant_agent_function_call.ipynb diff --git a/notebook/gpt_assistant_agent_function_call.ipynb b/notebook/gpt_assistant_agent_function_call.ipynb new file mode 100644 index 00000000000..6febb89cc9b --- /dev/null +++ b/notebook/gpt_assistant_agent_function_call.ipynb @@ -0,0 +1,566 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "hLnLsw8SaMa0" + }, + "source": [ + "# From Dad Jokes To Sad Jokes: Function Calling with GPTAssistantAgent\n", + "\n", + "Autogen allows `GPTAssistantAgent` to be augmented with \"tools\" — pre-defined functions or capabilities — that extend its ability to handle specific tasks, similar to how one might natively utilize tools in the [OpenAI Assistant's API](https://platform.openai.com/docs/assistants/tools).\n", + "\n", + "In this notebook, we create a basic Multi-Agent System using Autogen's `GPTAssistantAgent` to convert Dad jokes on a specific topic into Sad jokes. It consists of a \"Dad\" agent which has the ability to search the [Dad Joke API](https://icanhazdadjoke.com/api) and a \"Sad Joker\" agent which converts the Dad jokes into Sad jokes. The Sad Joker then writes the sad jokes into a txt file.\n", + "\n", + "In this process we demonstrate how to call tools and perform function calling for `GPTAssistantAgent`." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9E3_0867da8p" + }, + "source": [ + "## Requirements\n", + "AutoGen requires Python 3.8 or newer. For this notebook, please install `pyautogen`:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "pWFw6-8lMleD" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: pyautogen in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (0.2.8)\n", + "Requirement already satisfied: openai>=1.3 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from pyautogen) (1.6.1)\n", + "Requirement already satisfied: diskcache in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from pyautogen) (5.6.3)\n", + "Requirement already satisfied: termcolor in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from pyautogen) (2.4.0)\n", + "Requirement already satisfied: flaml in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from pyautogen) (2.1.1)\n", + "Requirement already satisfied: python-dotenv in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from pyautogen) (1.0.0)\n", + "Requirement already satisfied: tiktoken in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from pyautogen) (0.5.2)\n", + "Requirement already satisfied: pydantic<3,>=1.10 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from pyautogen) (2.5.3)\n", + "Requirement already satisfied: docker in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from pyautogen) (7.0.0)\n", + "Requirement already satisfied: anyio<5,>=3.5.0 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from openai>=1.3->pyautogen) (4.2.0)\n", + "Requirement already satisfied: distro<2,>=1.7.0 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from openai>=1.3->pyautogen) (1.8.0)\n", + "Requirement already satisfied: httpx<1,>=0.23.0 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from openai>=1.3->pyautogen) (0.26.0)\n", + "Requirement already satisfied: sniffio in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from openai>=1.3->pyautogen) (1.3.0)\n", + "Requirement already satisfied: tqdm>4 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from openai>=1.3->pyautogen) (4.66.1)\n", + "Requirement already satisfied: typing-extensions<5,>=4.7 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from openai>=1.3->pyautogen) (4.9.0)\n", + "Requirement already satisfied: annotated-types>=0.4.0 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from pydantic<3,>=1.10->pyautogen) (0.6.0)\n", + "Requirement already satisfied: pydantic-core==2.14.6 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from pydantic<3,>=1.10->pyautogen) (2.14.6)\n", + "Requirement already satisfied: packaging>=14.0 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from docker->pyautogen) (23.2)\n", + "Requirement already satisfied: requests>=2.26.0 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from docker->pyautogen) (2.31.0)\n", + "Requirement already satisfied: urllib3>=1.26.0 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from docker->pyautogen) (2.1.0)\n", + "Requirement already satisfied: NumPy>=1.17.0rc1 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from flaml->pyautogen) (1.26.2)\n", + "Requirement already satisfied: regex>=2022.1.18 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from tiktoken->pyautogen) (2023.10.3)\n", + "Requirement already satisfied: idna>=2.8 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from anyio<5,>=3.5.0->openai>=1.3->pyautogen) (3.6)\n", + "Requirement already satisfied: certifi in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from httpx<1,>=0.23.0->openai>=1.3->pyautogen) (2023.11.17)\n", + "Requirement already satisfied: httpcore==1.* in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from httpx<1,>=0.23.0->openai>=1.3->pyautogen) (1.0.2)\n", + "Requirement already satisfied: h11<0.15,>=0.13 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from httpcore==1.*->httpx<1,>=0.23.0->openai>=1.3->pyautogen) (0.14.0)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in /Users/justintrugman/.pyenv/versions/3.11.7/lib/python3.11/site-packages (from requests>=2.26.0->docker->pyautogen) (3.3.2)\n", + "\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.3.2\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m24.0\u001b[0m\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "pip install pyautogen" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jnH9U6MIdwUl" + }, + "source": [ + "Import Dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "Ga-yZeoBMzHs" + }, + "outputs": [], + "source": [ + "from typing import Annotated, Literal\n", + "\n", + "import requests\n", + "\n", + "import autogen\n", + "from autogen import UserProxyAgent\n", + "from autogen.agentchat.contrib.gpt_assistant_agent import GPTAssistantAgent\n", + "from autogen.function_utils import get_function_schema\n", + "\n", + "config_list = autogen.config_list_from_json(\n", + " env_or_file=\"OAI_CONFIG_LIST\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "02lZOEAQd1qi" + }, + "source": [ + "## Creating the Functions\n", + "We need to create functions for our Agents to call.\n", + "\n", + "This function calls the Dad Joke API with a search term that the agent creates and returns a list of dad jokes." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "jcti0u08NJ2g" + }, + "outputs": [], + "source": [ + "def get_dad_jokes(search_term: str, page: int = 1, limit: int = 10) -> str:\n", + " \"\"\"\n", + " Fetches a list of dad jokes based on a search term.\n", + "\n", + " Parameters:\n", + " - search_term: The search term to find jokes about.\n", + " - page: The page number of results to fetch (default is 1).\n", + " - limit: The number of results to return per page (default is 20, max is 30).\n", + "\n", + " Returns:\n", + " A list of dad jokes.\n", + " \"\"\"\n", + " url = \"https://icanhazdadjoke.com/search\"\n", + " headers = {\"Accept\": \"application/json\"}\n", + " params = {\"term\": search_term, \"page\": page, \"limit\": limit}\n", + "\n", + " response = requests.get(url, headers=headers, params=params)\n", + "\n", + " if response.status_code == 200:\n", + " data = response.json()\n", + " jokes = [joke[\"joke\"] for joke in data[\"results\"]]\n", + " return jokes\n", + " else:\n", + " return f\"Failed to fetch jokes, status code: {response.status_code}\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "2FgsfBK1NsPj" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['Where do cats write notes?\\r\\nScratch Paper!', 'It was raining cats and dogs the other day. I almost stepped in a poodle.', 'What do you call a group of disorganized cats? A cat-tastrophe.', 'I accidentally took my cats meds last night. Don’t ask meow.', 'What do you call a pile of cats? A Meowtain.', 'Animal Fact #25: Most bobcats are not named bob.']\n" + ] + } + ], + "source": [ + "# Example Dad Jokes Function Usage:\n", + "jokes = get_dad_jokes(\"cats\")\n", + "print(jokes)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DC9D5bKEeoKP" + }, + "source": [ + "This function allows the Agents to write to a txt file." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "wXAA2MtoOS_w" + }, + "outputs": [], + "source": [ + "def write_to_txt(content: str, filename: str = \"dad_jokes.txt\"):\n", + " \"\"\"\n", + " Writes a formatted string to a text file.\n", + " Parameters:\n", + "\n", + " - content: The formatted string to write.\n", + " - filename: The name of the file to write to. Defaults to \"output.txt\".\n", + " \"\"\"\n", + " with open(filename, \"w\") as file:\n", + " file.write(content)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "xAgcFXEHOfcl" + }, + "outputs": [], + "source": [ + "# Example Write to TXT Function Usage:\n", + "content = \"\\n\".join(jokes) # Format the jokes from the above example\n", + "write_to_txt(content)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create Function Schemas\n", + "In order to use the functions within our GPTAssistantAgents, we need to generate function schemas. This can be done by using `get_function_schema`" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# Assistant API Tool Schema for get_dad_jokes\n", + "get_dad_jokes_schema = get_function_schema(\n", + " get_dad_jokes,\n", + " name=\"get_dad_jokes\",\n", + " description=\"Fetches a list of dad jokes based on a search term. Allows pagination with page and limit parameters.\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "The return type of the function 'write_to_txt' is not annotated. Although annotating it is optional, the function should return either a string, a subclass of 'pydantic.BaseModel'.\n" + ] + } + ], + "source": [ + "# Assistant API Tool Schema for write_to_txt\n", + "write_to_txt_schema = get_function_schema(\n", + " write_to_txt,\n", + " name=\"write_to_txt\",\n", + " description=\"Writes a formatted string to a text file. If the file does not exist, it will be created. If the file does exist, it will be overwritten.\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sgpx2JQme2kv" + }, + "source": [ + "## Creating the Agents\n", + "In this section we create and configure our Dad and Sad Joker Agents" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6X40-Sk6Pcs8" + }, + "source": [ + "### Set up the User Proxy" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "id": "mEpxEaPdPSDp" + }, + "outputs": [], + "source": [ + "user_proxy = UserProxyAgent(\n", + " name=\"user_proxy\",\n", + " is_termination_msg=lambda msg: \"TERMINATE\" in msg[\"content\"],\n", + " human_input_mode=\"NEVER\",\n", + " max_consecutive_auto_reply=1,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "q4ym9KlMPenf" + }, + "source": [ + "### The Dad Agent\n", + "We create the Dad agent using `GPTAssistantAgent`, in order for us to enable the Dad to use the `get_dad_jokes` function we need to provide it the function's specification in our `llm_config`.\n", + "\n", + "We format the `tools` within our `llm_config` in the same format as provided in the [OpenAI Assistant tools docs](https://platform.openai.com/docs/assistants/tools/function-calling)." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "id": "kz0c_tVIPgi6" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "OpenAI client config of GPTAssistantAgent(the_dad) - model: gpt-4-1106-preview\n", + "Matching assistant found, using the first matching assistant: {'id': 'asst_BLBUwYPugb1UR2jQMGAA7RtU', 'created_at': 1714660644, 'description': None, 'file_ids': [], 'instructions': \"\\n As 'The Dad', your primary role is to entertain by fetching dad jokes which the sad joker will transform into 'sad jokes' based on a given theme. When provided with a theme, such as 'plants' or 'animals', your task is as follows:\\n\\n 1. Use the 'get_dad_jokes' function to search for dad jokes related to the provided theme by providing a search term related to the theme. Fetch a list of jokes that are relevant to the theme.\\n 2. Present these jokes to the sad joker in a format that is clear and easy to read, preparing them for transformation.\\n\\n Remember, the team's goal is to creatively adapt the essence of each dad joke to fit the 'sad joke' format, all while staying true to the theme provided by the user.\\n \", 'metadata': {}, 'model': 'gpt-4-1106-preview', 'name': 'the_dad', 'object': 'assistant', 'tools': [ToolFunction(function=FunctionDefinition(name='get_dad_jokes', description='Fetches a list of dad jokes based on a search term. Allows pagination with page and limit parameters.', parameters={'type': 'object', 'properties': {'search_term': {'type': 'string', 'description': 'search_term'}, 'page': {'type': 'integer', 'default': 1, 'description': 'page'}, 'limit': {'type': 'integer', 'default': 10, 'description': 'limit'}}, 'required': ['search_term']}), type='function')]}\n" + ] + } + ], + "source": [ + "the_dad = GPTAssistantAgent(\n", + " name=\"the_dad\",\n", + " instructions=\"\"\"\n", + " As 'The Dad', your primary role is to entertain by fetching dad jokes which the sad joker will transform into 'sad jokes' based on a given theme. When provided with a theme, such as 'plants' or 'animals', your task is as follows:\n", + "\n", + " 1. Use the 'get_dad_jokes' function to search for dad jokes related to the provided theme by providing a search term related to the theme. Fetch a list of jokes that are relevant to the theme.\n", + " 2. Present these jokes to the sad joker in a format that is clear and easy to read, preparing them for transformation.\n", + "\n", + " Remember, the team's goal is to creatively adapt the essence of each dad joke to fit the 'sad joke' format, all while staying true to the theme provided by the user.\n", + " \"\"\",\n", + " overwrite_instructions=True, # overwrite any existing instructions with the ones provided\n", + " overwrite_tools=True, # overwrite any existing tools with the ones provided\n", + " llm_config={\n", + " \"config_list\": config_list,\n", + " \"tools\": [get_dad_jokes_schema],\n", + " },\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we register the `get_dad_jokes` function with the Dad `GPTAssistantAgent`" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "# Register get_dad_jokes with the_dad GPTAssistantAgent\n", + "the_dad.register_function(\n", + " function_map={\n", + " \"get_dad_jokes\": get_dad_jokes,\n", + " },\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cpv2yiyqRWl2" + }, + "source": [ + "### The Sad Joker Agent\n", + "We then create and configure the Sad Joker agent in a similar manner to the Dad agent above." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "id": "vghN1WwLRXtW" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "OpenAI client config of GPTAssistantAgent(the_sad_joker) - model: gpt-4-1106-preview\n", + "Matching assistant found, using the first matching assistant: {'id': 'asst_HzB75gkobafXZhkuIAmiBiai', 'created_at': 1714660668, 'description': None, 'file_ids': [], 'instructions': \"\\n As 'The Sad Joker', your unique role is to take dad jokes and creatively transform them into 'sad jokes'. When you receive a list of dad jokes, themed around topics like 'plants' or 'animals', you should:\\n\\n 1. Read through each dad joke carefully, understanding its theme and punchline.\\n 2. Creatively alter the joke to change its mood from humorous to somber or melancholic. This may involve tweaking the punchline, modifying the setup, or even completely reimagining the joke while keeping it relevant to the original theme.\\n 3. Ensure your transformations maintain a clear connection to the original theme and are understandable as adaptations of the dad jokes provided.\\n 4. Write your transformed sad jokes to a text file using the 'write_to_txt' function. Use meaningful file names that reflect the theme or the nature of the jokes within, unless a specific filename is requested.\\n\\n Your goal is not just to alter the mood of the jokes but to do so in a way that is creative, thoughtful, and respects the essence of the original humor. Remember, while the themes might be light-hearted, your transformations should offer a melancholic twist that makes them uniquely 'sad jokes'.\\n \", 'metadata': {}, 'model': 'gpt-4-1106-preview', 'name': 'the_sad_joker', 'object': 'assistant', 'tools': [ToolFunction(function=FunctionDefinition(name='write_to_txt', description='Writes a formatted string to a text file. If the file does not exist, it will be created. If the file does exist, it will be overwritten.', parameters={'type': 'object', 'properties': {'content': {'type': 'string', 'description': 'content'}, 'filename': {'type': 'string', 'default': 'dad_jokes.txt', 'description': 'filename'}}, 'required': ['content']}), type='function')]}\n" + ] + } + ], + "source": [ + "the_sad_joker = GPTAssistantAgent(\n", + " name=\"the_sad_joker\",\n", + " instructions=\"\"\"\n", + " As 'The Sad Joker', your unique role is to take dad jokes and creatively transform them into 'sad jokes'. When you receive a list of dad jokes, themed around topics like 'plants' or 'animals', you should:\n", + "\n", + " 1. Read through each dad joke carefully, understanding its theme and punchline.\n", + " 2. Creatively alter the joke to change its mood from humorous to somber or melancholic. This may involve tweaking the punchline, modifying the setup, or even completely reimagining the joke while keeping it relevant to the original theme.\n", + " 3. Ensure your transformations maintain a clear connection to the original theme and are understandable as adaptations of the dad jokes provided.\n", + " 4. Write your transformed sad jokes to a text file using the 'write_to_txt' function. Use meaningful file names that reflect the theme or the nature of the jokes within, unless a specific filename is requested.\n", + "\n", + " Your goal is not just to alter the mood of the jokes but to do so in a way that is creative, thoughtful, and respects the essence of the original humor. Remember, while the themes might be light-hearted, your transformations should offer a melancholic twist that makes them uniquely 'sad jokes'.\n", + " \"\"\",\n", + " overwrite_instructions=True, # overwrite any existing instructions with the ones provided\n", + " overwrite_tools=True, # overwrite any existing tools with the ones provided\n", + " llm_config={\n", + " \"config_list\": config_list,\n", + " \"tools\": [write_to_txt_schema],\n", + " },\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Register the `write_to_txt` function with the Sad Joker `GPTAssistantAgent`" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "# Register get_dad_jokes with the_dad GPTAssistantAgent\n", + "the_sad_joker.register_function(\n", + " function_map={\n", + " \"write_to_txt\": write_to_txt,\n", + " },\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9GBELjFBgjju" + }, + "source": [ + "## Creating the Groupchat and Starting the Conversation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9mT3c0k8SX8i" + }, + "source": [ + "Create the groupchat" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "id": "A3LG3TsNSZmO" + }, + "outputs": [], + "source": [ + "groupchat = autogen.GroupChat(agents=[user_proxy, the_dad, the_sad_joker], messages=[], max_round=15)\n", + "group_chat_manager = autogen.GroupChatManager(groupchat=groupchat, llm_config={\"config_list\": config_list})" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MT7GbnB9Spji" + }, + "source": [ + "Start the Conversation" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "id": "1m6pe5RNSmEy" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33muser_proxy\u001b[0m (to chat_manager):\n", + "\n", + "Jokes about cats\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[35m\n", + ">>>>>>>> EXECUTING FUNCTION get_dad_jokes...\u001b[0m\n", + "\u001b[33mthe_dad\u001b[0m (to chat_manager):\n", + "\n", + "Here are some cat-themed dad jokes for the sad joker to transform:\n", + "\n", + "1. Where do cats write notes? Scratch Paper!\n", + "2. It was raining cats and dogs the other day. I almost stepped in a poodle.\n", + "3. What do you call a group of disorganized cats? A cat-tastrophe.\n", + "4. I accidentally took my cat's meds last night. Don’t ask meow.\n", + "5. What do you call a pile of cats? A Meowtain.\n", + "6. Animal Fact #25: Most bobcats are not named Bob.\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[35m\n", + ">>>>>>>> EXECUTING FUNCTION write_to_txt...\u001b[0m\n", + "\u001b[33mthe_sad_joker\u001b[0m (to chat_manager):\n", + "\n", + "The cat-themed sad jokes have been transformed and saved to a text file named \"sad_cat_jokes.txt\".\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33muser_proxy\u001b[0m (to chat_manager):\n", + "\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "text/plain": [ + "ChatResult(chat_id=None, chat_history=[{'content': 'Jokes about cats', 'role': 'assistant'}, {'content': \"Here are some cat-themed dad jokes for the sad joker to transform:\\n\\n1. Where do cats write notes? Scratch Paper!\\n2. It was raining cats and dogs the other day. I almost stepped in a poodle.\\n3. What do you call a group of disorganized cats? A cat-tastrophe.\\n4. I accidentally took my cat's meds last night. Don’t ask meow.\\n5. What do you call a pile of cats? A Meowtain.\\n6. Animal Fact #25: Most bobcats are not named Bob.\\n\", 'name': 'the_dad', 'role': 'user'}, {'content': 'The cat-themed sad jokes have been transformed and saved to a text file named \"sad_cat_jokes.txt\".\\n', 'name': 'the_sad_joker', 'role': 'user'}, {'content': '', 'role': 'assistant'}], summary='', cost=({'total_cost': 0.0278, 'gpt-4-1106-preview': {'cost': 0.0278, 'prompt_tokens': 2744, 'completion_tokens': 12, 'total_tokens': 2756}}, {'total_cost': 0.02194, 'gpt-4-1106-preview': {'cost': 0.02194, 'prompt_tokens': 2167, 'completion_tokens': 9, 'total_tokens': 2176}}), human_input=[])" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "user_proxy.initiate_chat(group_chat_manager, message=\"Jokes about cats\")" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "front_matter": { + "description": "This comprehensive example demonstrates the use of tools in a GPTAssistantAgent Multi-Agent System by utilizing functions such as calling an API and writing to a file.", + "tags": [ + "open ai assistant", + "gpt assistant", + "tool use" + ] + }, + "kernelspec": { + "display_name": "Python 3", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/website/docs/Examples.md b/website/docs/Examples.md index 7c2a18a553a..45c16de4571 100644 --- a/website/docs/Examples.md +++ b/website/docs/Examples.md @@ -78,6 +78,7 @@ Links to notebook examples: - Chat with OpenAI Assistant with Code Interpreter - [View Notebook](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_oai_code_interpreter.ipynb) - Chat with OpenAI Assistant with Retrieval Augmentation - [View Notebook](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_oai_assistant_retrieval.ipynb) - OpenAI Assistant in a Group Chat - [View Notebook](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_oai_assistant_groupchat.ipynb) +- GPTAssistantAgent based Multi-Agent Tool Use - [View Notebook](https://github.com/microsoft/autogen/blob/main/notebook/gpt_assistant_agent_function_call.ipynb) ### Multimodal Agent
    # Event year Season Ceremony Flag bearer Sex State / Country Sport
    62 2018 Winter Closing Diggins , Jessica Jessica Diggins Minnesota Cross-country skiing
    61 2018 Winter Opening Hamlin , Erin Erin Hamlin New York Luge
    60 2016 Summer Closing Biles , Simone Simone Biles Texas Gymnastics
    59 2016 Summer Opening Phelps , Michael Michael Phelps Maryland Swimming
    58 2014 Winter Closing Chu , Julie Julie Chu Connecticut Hockey
    57 2014 Winter Opening Lodwick , Todd Todd Lodwick Colorado Nordic combined
    56 2012 Summer Closing Nellum , Bryshon Bryshon Nellum California Athletics
    55 2012 Summer Opening Zagunis , Mariel Mariel Zagunis Oregon Fencing
    54 Winter Closing Demong , Bill Bill Demong New York Nordic combined
    53 Winter Opening Grimmette , Mark Mark Grimmette Michigan Luge
    52 2008 Summer Closing Lorig , Khatuna Khatuna Lorig Georgia ( country ) Archery
    51 2008 Summer Opening Lomong , Lopez Lopez Lomong Sudan ( now South Sudan ) Athletics
    50 2006 Winter Closing Cheek , Joey Joey Cheek North Carolina Speed skating
    49 2006 Winter Opening Witty , Chris Chris Witty Wisconsin Speed skating
    48 Summer Closing Hamm , Mia Mia Hamm Texas Women 's soccer
    47 Summer Opening Staley , Dawn Dawn Staley Pennsylvania Basketball
    46 2002 Winter Closing Shimer , Brian Brian Shimer Florida Bobsleigh
    45 2002 Winter Opening Peterson , Amy Amy Peterson Minnesota Short track speed skating
    44 2000 Summer Closing Gardner , Rulon Rulon Gardner Wyoming Wrestling
    43 2000 Summer Opening Meidl , Cliff Cliff Meidl California Canoeing
    42 1998 Winter Closing Granato , Cammi Cammi Granato Illinois Hockey
    41 1998 Winter Opening Flaim , Eric Eric Flaim Massachusetts Speed skating
    40 Summer Closing Matz , Michael Michael Matz Pennsylvania Equestrian
    39 Summer Opening Baumgartner , Bruce Bruce Baumgartner New Jersey Wrestling
    38 1994 Winter Closing Jansen , Dan Dan Jansen Wisconsin Speed skating
    37 1994 Winter Opening Myler , Cammy Cammy Myler New York