# Solving Complex Tasks with A Sequence of Nested Chats

This notebook shows how you can leverage **nested chats** to solve complex tasks with AG2. Nested chats are a sequence of chats created by a receiver agent after receiving a message from a sender agent and finished before the receiver agent replies to this message. Nested chats allow AG2 agents to use other agents as their inner monologue to accomplish tasks. This abstraction is powerful as it enables you to compose agents in rich ways. This notebook shows how you can nest a pretty complex sequence of chats among _inner_ agents inside an _outer_ agent.

## Requirements

Install `autogen`:

```
pip install autogen
```

For more information, please refer to the [installation guide](https://docs.ag2.ai/latest/docs/user-guide/basic-concepts/installing-ag2).

```
import autogen

llm_config = autogen.LLMConfig.from_json(path="OAI_CONFIG_LIST")
```

### Example Task

Suppose we want the agents to complete the following sequence of tasks:

```
tasks = [
    "On which days in 2024 was Microsoft Stock higher than $400? Comment on the stock performance.",
    "Make a pleasant joke about it.",
]
```

Since the first task could be complex to solve, let's construct new agents that can serve as an inner monologue.

### Step 1. Define Agents

#### A Group Chat for Inner Monologue

Below, we construct a group chat manager that manages an `inner_assistant` agent and an `inner_code_interpreter` agent. Later we will use this group chat inside another agent.

```
inner_assistant = autogen.AssistantAgent(
    "Inner-assistant",
    llm_config=llm_config,
    is_termination_msg=lambda x: x.get("content", "").find("TERMINATE") >= 0,
)

inner_code_interpreter = autogen.UserProxyAgent(
    "Inner-code-interpreter",
    human_input_mode="NEVER",
    code_execution_config={
        "work_dir": "coding",
        "use_docker": False,
    },
    default_auto_reply="",
    is_termination_msg=lambda x: x.get("content", "").find("TERMINATE") >= 0,
)

groupchat = autogen.GroupChat(
    agents=[inner_assistant, inner_code_interpreter],
    messages=[],
    speaker_selection_method="round_robin",
    allow_repeat_speaker=False,
    max_round=8,
)

manager = autogen.GroupChatManager(
    groupchat=groupchat,
    is_termination_msg=lambda x: x.get("content", "").find("TERMINATE") >= 0,
    llm_config=llm_config,
    code_execution_config={
        "work_dir": "coding",
        "use_docker": False,
    },
)
```

#### Inner- and Outer-Level Individual Agents

Now we will construct several individual agents that will assume the roles of outer and inner agents.

```
assistant_1 = autogen.AssistantAgent(
    name="Assistant_1",
    llm_config=llm_config,
)

assistant_2 = autogen.AssistantAgent(
    name="Assistant_2",
    llm_config=llm_config,
)

writer = autogen.AssistantAgent(
    name="Writer",
    llm_config=llm_config,
    system_message="""
    You are a professional writer, known for
    your insightful and engaging articles.
    You transform complex concepts into compelling narratives.
    """,
)

reviewer = autogen.AssistantAgent(
    name="Reviewer",
    llm_config=llm_config,
    system_message="""
    You are a compliance reviewer, known for your thoroughness and commitment to standards.
    Your task is to scrutinize content for any harmful elements or regulatory violations, ensuring
    all materials align with required guidelines.
    You must review carefully, identify potential issues, and maintain the integrity of the organization.
    Your role demands fairness, a deep understanding of regulations, and a focus on protecting against
    harm while upholding a culture of responsibility.
    """,
)

user = autogen.UserProxyAgent(
    name="User",
    human_input_mode="NEVER",
    is_termination_msg=lambda x: x.get("content", "").find("TERMINATE") >= 0,
    code_execution_config={
        "last_n_messages": 1,
        "work_dir": "tasks",
        "use_docker": False,
    },
)  
```

### Step 2: Orchestrate Nested Chats to Solve Tasks

#### Outer Level

In the following code block, at the outer level, we have communication between:

- `user` - `assistant_1` for solving the first task, i.e., `tasks[0]`.
- `user` - `assistant_2` for solving the second task, i.e., `tasks[1]`.

#### Inner Level (Nested Chats)

Since the first task is quite complicated, we created a sequence of _nested chats_ as the inner monologue of `Assistant_1`.

1. `assistant_1` - `manager`: This chat intends to delegate the task received by `Assistant_1` to the Manager to solve.

2. `assistant_1` - `writer`: This chat takes the output from Nested Chat 1, i.e., `Assistant_1` vs.  Manager, and lets the Writer polish the content to make an engaging and nicely formatted blog post, which is realized through the `writing_message` function.

3. `assistant_1` - `reviewer`: This chat takes the output from Nested Chat 2 and intends to let the Reviewer agent review the content from Nested Chat 2.

4. `assistant_1` - `writer`: This chat takes the output from previous nested chats and intends to let the Writer agent finalize a blog post.

The sequence of nested chats can be realized with the `register_nested_chats` function, which allows one to register one or a sequence of chats to a particular agent (in this example, the `assistant_1` agent).

Information about the sequence of chats can be specified in the `chat_queue` argument of the `register_nested_chats` function. The following fields are especially useful:
- `recipient` (required) specifies the nested agent;
- `message` specifies what message to send to the nested recipient agent. In a sequence of nested chats, if the `message` field is not specified, we will use the last message the registering agent received as the initial message in the first chat and will skip any subsequent chat in the queue that does not have the `message` field. You can either provide a string or define a callable that returns a string.
- `summary_method` decides what to get out of the nested chat. You can either select from existing options including `"last_msg"` and `"reflection_with_llm"`, or define your own way on what to get from the nested chat with a Callable.
- `max_turns` determines how many turns of conversation to have between the concerned agent pairs.

```
def writing_message(recipient, messages, sender, config):
    return f"Polish the content to make an engaging and nicely formatted blog post. \n\n {recipient.chat_messages_for_summary(sender)[-1]['content']}"

nested_chat_queue = [
    {"recipient": manager, "summary_method": "reflection_with_llm"},
    {"recipient": writer, "message": writing_message, "summary_method": "last_msg", "max_turns": 1},
    {"recipient": reviewer, "message": "Review the content provided.", "summary_method": "last_msg", "max_turns": 1},
    {"recipient": writer, "message": writing_message, "summary_method": "last_msg", "max_turns": 1},
]
assistant_1.register_nested_chats(
    nested_chat_queue,
    trigger=user,
)

res = user.initiate_chats([
    {"recipient": assistant_1, "message": tasks[0], "max_turns": 1, "summary_method": "last_msg"},
    {"recipient": assistant_2, "message": tasks[1]},
])
