Skip to content

Commit bacfa98

Browse files
DavidYu00ekzhu
andauthored
Sample for integrating Core API with chainlit (#6422)
## Why are these changes needed? This pull request adds new samples that integrates the Autogen Core API with Chainlit. It closely follows the structure of the Agentchat+Chainlit sample and provides examples for using a single agent and multiple agents in a groupchat. ## Related issue number Closes: #5345 --------- Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
1 parent 2864fbf commit bacfa98

6 files changed

Lines changed: 628 additions & 0 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
model_config.yaml
2+
.chainlit
3+
chainlit.md
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Core ChainLit Integration Sample
2+
3+
In this sample, we will demonstrate how to build simple chat interface that
4+
interacts with a [Core](https://microsoft.github.io/autogen/stable/user-guide/core-user-guide/index.html)
5+
agent or a team, using [Chainlit](https://github.com/Chainlit/chainlit),
6+
and support streaming messages.
7+
8+
## Overview
9+
10+
The `core_chainlit` sample is designed to illustrate a simple use case of ChainLit integrated with a single-threaded agent runtime. It includes the following components:
11+
12+
- **Single Agent**: A single agent that operates within the ChainLit environment.
13+
- **Group Chat**: A group chat setup featuring two agents:
14+
- **Assistant Agent**: This agent responds to user inputs.
15+
- **Critic Agent**: This agent reflects on and critiques the responses from the Assistant Agent.
16+
- **Closure Agent**: Utilizes a closure agent to aggregate output messages into an output queue.
17+
- **Token Streaming**: Demonstrates how to stream tokens to the user interface.
18+
- **Session Management**: Manages the runtime and output queue within the ChainLit user session.
19+
20+
## Requirements
21+
22+
To run this sample, you will need:
23+
- Python 3.8 or higher
24+
- Installation of necessary Python packages as listed in `requirements.txt`
25+
26+
## Installation
27+
28+
To run this sample, you will need to install the following packages:
29+
30+
```shell
31+
pip install -U chainlit autogen-core autogen-ext[openai] pyyaml
32+
```
33+
34+
To use other model providers, you will need to install a different extra
35+
for the `autogen-ext` package.
36+
See the [Models documentation](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/models.html) for more information.
37+
38+
## Model Configuration
39+
40+
Create a configuration file named `model_config.yaml` to configure the model
41+
you want to use. Use `model_config_template.yaml` as a template.
42+
43+
44+
## Running the Agent Sample
45+
46+
The first sample demonstrate how to interact with a single AssistantAgent
47+
from the chat interface.
48+
Note: cd to the sample directory.
49+
50+
```shell
51+
chainlit run app_agent.py
52+
```
53+
54+
## Running the Team Sample
55+
56+
The second sample demonstrate how to interact with a team of agents from the
57+
chat interface.
58+
59+
```shell
60+
chainlit run app_team.py -h
61+
```
62+
63+
There are two agents in the team: one is instructed to be generally helpful
64+
and the other one is instructed to be a critic and provide feedback.
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
from typing import AsyncGenerator, List, Optional
2+
import asyncio
3+
import json
4+
from dataclasses import dataclass
5+
6+
from autogen_core import (
7+
CancellationToken,
8+
DefaultTopicId,
9+
FunctionCall,
10+
message_handler,
11+
MessageContext,
12+
RoutedAgent,
13+
TopicId
14+
)
15+
from autogen_core.models import (
16+
AssistantMessage,
17+
ChatCompletionClient,
18+
CreateResult,
19+
LLMMessage,
20+
SystemMessage,
21+
UserMessage,
22+
FunctionExecutionResult,
23+
FunctionExecutionResultMessage
24+
)
25+
26+
from autogen_core.tools import Tool
27+
from pydantic import BaseModel
28+
29+
@dataclass
30+
class Message:
31+
content: str
32+
33+
class StreamResult(BaseModel):
34+
content: str | CreateResult | AssistantMessage
35+
source: str
36+
37+
class GroupChatMessage(BaseModel):
38+
body: UserMessage
39+
40+
class RequestToSpeak(BaseModel):
41+
pass
42+
43+
TASK_RESULTS_TOPIC_TYPE = "task-results"
44+
task_results_topic_id = TopicId(type=TASK_RESULTS_TOPIC_TYPE, source="default")
45+
46+
class SimpleAssistantAgent(RoutedAgent):
47+
def __init__(
48+
self,
49+
name: str,
50+
system_message: str,
51+
#context: MessageContext,
52+
model_client: ChatCompletionClient,
53+
tool_schema: List[Tool] = [],
54+
model_client_stream: bool = False,
55+
reflect_on_tool_use: bool | None = None,
56+
group_chat_topic_type: str = "Default",
57+
) -> None:
58+
super().__init__(name)
59+
self._system_message = SystemMessage(content=system_message)
60+
self._model_client = model_client
61+
self._tools = tool_schema
62+
#self._model_context = context
63+
self._model_client_stream = model_client_stream
64+
self._reflect_on_tool_use = reflect_on_tool_use
65+
self._group_chat_topic_type = group_chat_topic_type
66+
self._chat_history: List[LLMMessage] = []
67+
68+
async def _call_model_client(
69+
self, cancellation_token: CancellationToken
70+
) -> AsyncGenerator[str | CreateResult, None]:
71+
# Call the LLM model to process the message
72+
model_result = None
73+
async for chunk in self._model_client.create_stream(
74+
messages=[self._system_message] + self._chat_history,
75+
tools=self._tools,
76+
cancellation_token=cancellation_token,
77+
):
78+
if isinstance(chunk, CreateResult):
79+
model_result = chunk
80+
elif isinstance(chunk, str):
81+
yield chunk
82+
else:
83+
raise RuntimeError(f"Invalid chunk type: {type(chunk)}")
84+
85+
if model_result is None: # No final result in model client respons
86+
raise RuntimeError("No final model result in streaming mode.")
87+
88+
yield model_result
89+
return
90+
91+
async def _execute_tool_call(
92+
self, call: FunctionCall, cancellation_token: CancellationToken
93+
) -> FunctionExecutionResult:
94+
# Find the tool by name.
95+
tool = next((tool for tool in self._tools if tool.name == call.name), None)
96+
assert tool is not None
97+
98+
# Run the tool and capture the result.
99+
try:
100+
arguments = json.loads(call.arguments)
101+
result = await tool.run_json(arguments, cancellation_token)
102+
return FunctionExecutionResult(
103+
call_id=call.id, content=tool.return_value_as_string(result), is_error=False, name=tool.name
104+
)
105+
except Exception as e:
106+
return FunctionExecutionResult(call_id=call.id, content=str(e), is_error=True, name=tool.name)
107+
108+
@message_handler
109+
async def handle_user_message(self, message: UserMessage, ctx: MessageContext) -> Message:
110+
111+
# Append the message to chat history.
112+
self._chat_history.append(
113+
message
114+
)
115+
116+
# Add message to model context.
117+
# await self._model_context.add_message(UserMessage(content=message.content, source="User"))
118+
model_result: Optional[CreateResult] = None
119+
120+
async for chunk in self._call_model_client(
121+
cancellation_token=ctx.cancellation_token,
122+
):
123+
if isinstance(chunk, CreateResult):
124+
model_result = chunk
125+
elif isinstance(chunk, str):
126+
# foward the stream tokent to the Queue
127+
await self.runtime.publish_message(StreamResult(content=chunk, source=self.id.type), topic_id=task_results_topic_id)
128+
else:
129+
raise RuntimeError(f"Invalid chunk type: {type(chunk)}")
130+
131+
if model_result is None: # No final result in model client respons
132+
raise RuntimeError("No final model result in streaming mode.")
133+
134+
# Add the first model create result to the session.
135+
self._chat_history.append(AssistantMessage(content=model_result.content, source=self.id.type))
136+
137+
if isinstance(model_result.content, str): # No tools, return the result
138+
await self.runtime.publish_message(StreamResult(content=model_result, source=self.id.type), topic_id=task_results_topic_id)
139+
return (Message(content= model_result.content))
140+
141+
# Execute the tool calls.
142+
assert isinstance(model_result.content, list) and all(
143+
isinstance(call, FunctionCall) for call in model_result.content
144+
)
145+
results = await asyncio.gather(
146+
*[self._execute_tool_call(call, ctx.cancellation_token) for call in model_result.content]
147+
)
148+
149+
# Add the function execution results to the session.
150+
self._chat_history.append(FunctionExecutionResultMessage(content=results))
151+
152+
#if (not self._reflect_on_tool_use):
153+
# return Message(content=model_result.content)
154+
155+
# Run the chat completion client again to reflect on the history and function execution results.
156+
#model_result = None
157+
model_result2: Optional[CreateResult] = None
158+
async for chunk in self._call_model_client(
159+
cancellation_token=ctx.cancellation_token,
160+
):
161+
if isinstance(chunk, CreateResult):
162+
model_result2 = chunk
163+
elif isinstance(chunk, str):
164+
# foward the stream tokent to the Queue
165+
await self.runtime.publish_message(StreamResult(content=chunk, source=self.id.type), topic_id=task_results_topic_id)
166+
else:
167+
raise RuntimeError(f"Invalid chunk type: {type(chunk)}")
168+
169+
if model_result2 is None:
170+
raise RuntimeError("No final model result in streaming mode.")
171+
assert model_result2.content is not None
172+
assert isinstance(model_result2.content, str)
173+
174+
await self.runtime.publish_message(StreamResult(content=model_result2, source=self.id.type), topic_id=task_results_topic_id)
175+
176+
return Message(content=model_result2.content)
177+
178+
# Message handler for Group chat message. It just add the message to the agent message history.
179+
# The message will be processed when the agent receives the RequestToSpeak.
180+
@message_handler
181+
async def handle_message(self, message: GroupChatMessage, ctx: MessageContext) -> None:
182+
self._chat_history.extend(
183+
[
184+
UserMessage(content=f"Transferred to {message.body.source}", source="system"),
185+
message.body,
186+
]
187+
)
188+
189+
# Message handler for request to speaker message.
190+
@message_handler
191+
async def handle_request_to_speak(self, message: RequestToSpeak, ctx: MessageContext) -> None:
192+
#print(f"### {self.id.type}: ")
193+
self._chat_history.append(
194+
UserMessage(content=f"Transferred to {self.id.type}, adopt the persona immediately.", source="system")
195+
)
196+
197+
# Run the chat completion client again to reflect on the history and function execution results.
198+
model_result: Optional[CreateResult] = None
199+
async for chunk in self._call_model_client(
200+
cancellation_token=ctx.cancellation_token,
201+
):
202+
if isinstance(chunk, CreateResult):
203+
model_result = chunk
204+
await self.runtime.publish_message(StreamResult(content=model_result, source=self.id.type), topic_id=task_results_topic_id)
205+
elif isinstance(chunk, str):
206+
# foward the stream tokent to the Queue
207+
await self.runtime.publish_message(StreamResult(content=chunk, source=self.id.type), topic_id=task_results_topic_id)
208+
else:
209+
raise RuntimeError(f"Invalid chunk type: {type(chunk)}")
210+
211+
if model_result is None:
212+
raise RuntimeError("No final model result in streaming mode.")
213+
214+
assert isinstance(model_result.content, str)
215+
assert model_result.content is not None
216+
217+
self._chat_history.append(AssistantMessage(content=model_result.content, source=self.id.type))
218+
#print(model_result.content, flush=True)
219+
await self.publish_message(
220+
GroupChatMessage(body=UserMessage(content=model_result.content, source=self.id.type)),
221+
topic_id=DefaultTopicId(type=self._group_chat_topic_type),
222+
)

0 commit comments

Comments
 (0)