117 |
118 |
119 |
Overview
120 |
121 | Session {currentSession}
122 |
123 |
124 |
125 |
126 | Color:
127 |
144 |
145 |
146 |
147 |
148 |
155 |
156 | {/* legend */}
157 |
158 | {uniqueValues.map((value) => (
159 |
184 | ))}
185 |
186 |
187 |
188 | );
189 | };
190 |
191 | export default ConversationOverview;
192 |
--------------------------------------------------------------------------------
/src/agdebugger/serialization.py:
--------------------------------------------------------------------------------
1 | import json
2 | from dataclasses import dataclass
3 | from typing import Dict, List
4 |
5 | from autogen_agentchat.messages import (
6 | AgentEvent,
7 | ChatMessage,
8 | HandoffMessage,
9 | MemoryQueryEvent,
10 | MultiModalMessage,
11 | StopMessage,
12 | TextMessage,
13 | ToolCallExecutionEvent,
14 | ToolCallRequestEvent,
15 | ToolCallSummaryMessage,
16 | UserInputRequestedEvent,
17 | )
18 | from autogen_agentchat.teams._group_chat._events import (
19 | GroupChatAgentResponse,
20 | GroupChatMessage,
21 | GroupChatRequestPublish,
22 | GroupChatReset,
23 | GroupChatStart,
24 | GroupChatTermination,
25 | )
26 | from autogen_core.models import (
27 | AssistantMessage,
28 | FunctionExecutionResult,
29 | FunctionExecutionResultMessage,
30 | LLMMessage,
31 | SystemMessage,
32 | UserMessage,
33 | )
34 |
35 |
36 | @dataclass
37 | class FieldInfo:
38 | name: str
39 | type: str
40 | required: bool
41 |
42 |
43 | @dataclass
44 | class MessageTypeDescription:
45 | name: str
46 | fields: List[FieldInfo] | None = None
47 |
48 |
49 | def get_message_type_descriptions() -> Dict[str, MessageTypeDescription]:
50 | """
51 | Gets the message type descriptions for user-sendable messages for agentchat:
52 | - TextMessage, MultiModalMessage, StopMessage, HandoffMessage
53 | """
54 |
55 | return {
56 | # "TextMessage": MessageTypeDescription(
57 | # name="TextMessage",
58 | # fields=[
59 | # FieldInfo(name="source", type="str", required=True),
60 | # FieldInfo(name="content", type="str", required=True),
61 | # FieldInfo(name="type", type="str", required=True),
62 | # ],
63 | # ),
64 | # "MultiModalMessage": MessageTypeDescription(
65 | # name="MultiModalMessage",
66 | # fields=[
67 | # FieldInfo(name="source", type="str", required=True),
68 | # FieldInfo(name="content", type="List[str]", required=True),
69 | # FieldInfo(name="type", type="str", required=True),
70 | # ],
71 | # ),
72 | # "StopMessage": MessageTypeDescription(
73 | # name="StopMessage",
74 | # fields=[
75 | # FieldInfo(name="source", type="str", required=True),
76 | # FieldInfo(name="content", type="str", required=True),
77 | # FieldInfo(name="type", type="str", required=True),
78 | # ],
79 | # ),
80 | # "HandoffMessage": MessageTypeDescription(
81 | # name="HandoffMessage",
82 | # fields=[
83 | # FieldInfo(name="source", type="str", required=True),
84 | # FieldInfo(name="content", type="str", required=True),
85 | # FieldInfo(name="target", type="str", required=True),
86 | # FieldInfo(name="context", type="List[LLMMessage]", required=False),
87 | # FieldInfo(name="type", type="str", required=True),
88 | # ],
89 | # ),
90 | "GroupChatStart": MessageTypeDescription(
91 | name="GroupChatStart",
92 | fields=[
93 | FieldInfo(name="messages", type="List[ChatMessage]", required=False),
94 | ],
95 | ),
96 | "GroupChatAgentResponse": MessageTypeDescription(
97 | name="GroupChatAgentResponse",
98 | fields=[
99 | FieldInfo(name="agent_response", type="Response", required=True),
100 | ],
101 | ),
102 | "GroupChatRequestPublish": MessageTypeDescription(
103 | name="GroupChatRequestPublish",
104 | fields=None,
105 | ),
106 | "GroupChatMessage": MessageTypeDescription(
107 | name="GroupChatMessage",
108 | fields=[
109 | FieldInfo(name="message", type="ChatMessage", required=True),
110 | ],
111 | ),
112 | "GroupChatTermination": MessageTypeDescription(
113 | name="GroupChatTermination",
114 | fields=[
115 | FieldInfo(name="message", type="StopMessage", required=True),
116 | ],
117 | ),
118 | "GroupChatReset": MessageTypeDescription(
119 | name="GroupChatReset",
120 | fields=None,
121 | ),
122 | }
123 |
124 |
125 | # ### Serialization ### -- maybe should be a class?
126 |
127 | __message_map = {
128 | # agentchat messages
129 | "TextMessage": TextMessage,
130 | "MultiModalMessage": MultiModalMessage,
131 | "StopMessage": StopMessage,
132 | "HandoffMessage": HandoffMessage,
133 | # agentchat events
134 | "ToolCallRequestEvent": ToolCallRequestEvent,
135 | "ToolCallExecutionEvent": ToolCallExecutionEvent,
136 | "ToolCallSummaryMessage": ToolCallSummaryMessage,
137 | "UserInputRequestedEvent": UserInputRequestedEvent,
138 | "MemoryQueryEvent": MemoryQueryEvent,
139 | # group chat messages
140 | "GroupChatAgentResponse": GroupChatAgentResponse,
141 | "GroupChatMessage": GroupChatMessage,
142 | "GroupChatRequestPublish": GroupChatRequestPublish,
143 | "GroupChatReset": GroupChatReset,
144 | "GroupChatStart": GroupChatStart,
145 | "GroupChatTermination": GroupChatTermination,
146 | # core messages
147 | "AssistantMessage": AssistantMessage,
148 | "FunctionExecutionResult": FunctionExecutionResult,
149 | "FunctionExecutionResultMessage": FunctionExecutionResultMessage,
150 | "SystemMessage": SystemMessage,
151 | "UserMessage": UserMessage,
152 | }
153 |
154 |
155 | def serialize(message: ChatMessage | AgentEvent | LLMMessage | None) -> dict:
156 | try:
157 | if message is None:
158 | return {"type": "None"}
159 |
160 | serialized_message = message.model_dump()
161 |
162 | # get name in case doesnt exist
163 | type_name = type(message).__name__
164 | serialized_message["type"] = type_name
165 | return serialized_message
166 | except Exception:
167 | print("[WARN] Unable to serialize message: ", message)
168 | return {}
169 |
170 |
171 | def deserialize(
172 | message_dict: Dict | str,
173 | ) -> ChatMessage | AgentEvent | LLMMessage | None:
174 | try:
175 | if isinstance(message_dict, str):
176 | message_dict = json.loads(message_dict)
177 |
178 | message_type = message_dict["type"] # type: ignore
179 |
180 | if message_type == "None":
181 | return None
182 |
183 | new_message_class = __message_map[message_type]
184 | new_message = new_message_class(**message_dict)
185 | return new_message
186 | except Exception as e:
187 | print(
188 | f"[WARN] Unable to deserialize message dict into Pydantic class. Error: {str(e)}.\nMessage dict: ",
189 | message_dict,
190 | )
191 | return None
192 |
--------------------------------------------------------------------------------
/frontend/src/components/MessageCard.tsx:
--------------------------------------------------------------------------------
1 | import { Close } from "flowbite-react-icons/outline";
2 | import { Reply } from "flowbite-react-icons/solid";
3 | import _ from "lodash";
4 | import React, { useState, useEffect, memo } from "react";
5 |
6 | import { useAllowActions } from "../context/AllowActionsContext";
7 | import { useHoveredMessage } from "../context/HoveredMessageContext";
8 | import type { Message, GenericMessage } from "../shared-types";
9 | import { messageTypeFormat } from "../utils/colours";
10 | import DisplayMessage from "./MessageDisplays/DisplayMessage";
11 |
12 | interface MessageProps {
13 | message: Message;
14 | editId: number;
15 | timestamp?: number;
16 | writeEditAndRevertMessage: (
17 | id: number,
18 | message: GenericMessage | undefined,
19 | ) => void;
20 | writeMessageTag?: string;
21 | allowRevert?: boolean;
22 | shouldBold?: boolean;
23 | }
24 |
25 | type UnknownDict = { [key: string]: unknown };
26 |
27 | interface MessageInfo {
28 | outerMessageType: "Publish" | "Send" | "Response";
29 | innerMessageType: string;
30 | sender: string | undefined;
31 | recipient: string | undefined;
32 | innerMessage: UnknownDict;
33 | canEditContent: boolean;
34 | }
35 |
36 | function parseMessage(message: Message): MessageInfo {
37 | const innerMessage = message.message as GenericMessage;
38 |
39 | const outerKind = messageTypeFormat(message.type) as
40 | | "Publish"
41 | | "Send"
42 | | "Response";
43 | let sender: string | undefined = undefined;
44 | let recipient: string | undefined = undefined;
45 |
46 | if (message.type === "PublishMessageEnvelope") {
47 | sender = message.sender ?? "User";
48 | recipient = "Group";
49 | } else if (message.type === "SendMessageEnvelope") {
50 | sender = message.sender ?? "User";
51 | recipient = (message.recipient as string) ?? "Unknown";
52 | } else if (message.type === "ResponseMessageEnvelope") {
53 | sender = message.sender as string;
54 | recipient = message.recipient ?? "User";
55 | } else if (message.type === "ThoughtMessage") {
56 | sender = message.sender + " 🧠";
57 | } else {
58 | console.warn(
59 | "Invalid Outer Message type: " +
60 | message.type +
61 | "\nEntire message: " +
62 | message,
63 | );
64 | }
65 |
66 | // const contentStr: string =
67 | // innerMessage.type == undefined
68 | // ? String(innerMessage)
69 | // : JSON.stringify(innerMessage, null, 2);
70 | const canEditContent: boolean =
71 | outerKind === "Send" || outerKind === "Publish";
72 |
73 | return {
74 | outerMessageType: outerKind,
75 | // @ts-expect-error ignore bro it has type
76 | innerMessageType: innerMessage.type,
77 | sender,
78 | recipient,
79 | innerMessage,
80 | canEditContent,
81 | };
82 | }
83 |
84 | function getMessageTypeDisplay(inner?: string, outer?: string) {
85 | if (inner != undefined && outer != undefined)
86 | return `${outer ?? ""} - ${inner ?? ""}`;
87 | if (inner != undefined) return inner;
88 | return outer;
89 | }
90 |
91 | const MessageCard: React.FC
= memo(
92 | ({
93 | message,
94 | editId,
95 | timestamp,
96 | writeEditAndRevertMessage,
97 | writeMessageTag = "Save edit",
98 | allowRevert = true,
99 | shouldBold,
100 | }) => {
101 | const [outerMessage, setOuterMessage] = useState();
102 | const [innerMessage, setInnerMessage] = useState();
103 | const [originalInnerMessage, setOriginalInnerMessage] = useState();
104 |
105 | // hooks
106 | const { hoveredMessageId, setHoveredMessageId } = useHoveredMessage();
107 | const { allowActions } = useAllowActions();
108 |
109 | useEffect(() => {
110 | const parsed = parseMessage(message);
111 |
112 | setOuterMessage(parsed);
113 | setOriginalInnerMessage(parsed.innerMessage);
114 | setInnerMessage(parsed.innerMessage);
115 | }, [message]);
116 |
117 | useEffect(() => {
118 | setOuterMessage((prev) => {
119 | if (prev != undefined) {
120 | const updatedMessage = structuredClone(prev);
121 | // @ts-expect-error will be fine
122 | updatedMessage.innerMessage = innerMessage;
123 | return updatedMessage;
124 | }
125 | });
126 | }, [innerMessage, setOuterMessage]);
127 |
128 | // edit and reset message
129 | const saveAndRevertMessage = () => {
130 | if (outerMessage === undefined) return;
131 |
132 | writeEditAndRevertMessage(editId, outerMessage.innerMessage);
133 | };
134 |
135 | // reset message no edit
136 | const revertToMessage = () => {
137 | writeEditAndRevertMessage(editId, undefined);
138 | };
139 |
140 | return (
141 | outerMessage != undefined && (
142 | {
146 | timestamp != undefined && setHoveredMessageId(timestamp);
147 | }}
148 | onPointerLeave={() => setHoveredMessageId(undefined)}
149 | >
150 |
151 |
152 | {outerMessage.sender}{" "}
153 | {outerMessage.recipient != undefined &&
154 | "→ " + outerMessage.recipient}
155 |
156 |
157 |
158 |
159 | {getMessageTypeDisplay(
160 | outerMessage.innerMessageType,
161 | outerMessage.outerMessageType,
162 | )}
163 |
164 |
167 | {timestamp}
168 |
169 |
170 | {allowRevert &&
171 | (outerMessage.outerMessageType === "Send" ||
172 | outerMessage.outerMessageType === "Publish") && (
173 |
181 | )}
182 |
183 |
184 |
185 |
186 |
192 |
193 | {outerMessage.canEditContent &&
194 | !_.isEqual(originalInnerMessage, outerMessage.innerMessage) && (
195 |
196 |
203 |
210 |
211 | )}
212 |
213 | )
214 | );
215 | },
216 | );
217 |
218 | export default MessageCard;
219 |
--------------------------------------------------------------------------------
/frontend/src/components/SendMessage.tsx:
--------------------------------------------------------------------------------
1 | import { Select, Button } from "flowbite-react";
2 | import { PaperPlane } from "flowbite-react-icons/outline";
3 | import React, { useEffect, useState } from "react";
4 | import { memo } from "react";
5 |
6 | import { api } from "../api";
7 | import { useAllowActions } from "../context/AllowActionsContext";
8 | import type { AgentName, MessageTypeDescription } from "../shared-types";
9 | import { DEFAULT_MESSAGES } from "../utils/default-messages";
10 | import DisplayMessage from "./MessageDisplays/DisplayMessage";
11 |
12 | interface SendMessageProps {
13 | agents: AgentName[];
14 | onSend: () => void;
15 | topics: string[];
16 | }
17 |
18 | type SendOption = "dm" | "publish";
19 |
20 | function makeDefaultMessage(metadata: MessageTypeDescription) {
21 | if (metadata.name in DEFAULT_MESSAGES) {
22 | return DEFAULT_MESSAGES[metadata.name];
23 | } else {
24 | const message = {};
25 | metadata.fields?.forEach((field) => {
26 | message[field.name] = "";
27 | });
28 | message["type"] = metadata.name;
29 | return message;
30 | }
31 | }
32 |
33 | const SendMessage: React.FC = memo((props) => {
34 | const [sendType, setSendType] = useState("dm");
35 | const [newMessage, setNewMessage] = useState({});
36 | const [errorMessage, setErrorMessage] = useState("");
37 | const [messageInfoDict, setMessageInfoDict] = useState<{
38 | [key: string]: MessageTypeDescription;
39 | }>({});
40 | const [selectedMessageType, setSelectedMessageType] = useState(
41 | Object.keys(messageInfoDict)[0],
42 | );
43 |
44 | const [selectedAgent, setSelectedAgent] = useState(
45 | props.agents.length > 0 ? props.agents[0] : undefined,
46 | );
47 | const [selectedTopic, setSelectedTopic] = useState(
48 | props.topics.length > 0 ? props.topics[0] : undefined,
49 | );
50 |
51 | // Ensure selectedAgent is only set once
52 | useEffect(() => {
53 | if (!selectedAgent && props.agents.length > 0) {
54 | setSelectedAgent(props.agents[0]);
55 | }
56 | }, [props.agents, selectedAgent]);
57 |
58 | // Ensure selectedTopic is only set once
59 | useEffect(() => {
60 | if (!selectedTopic && props.topics.length > 0) {
61 | setSelectedTopic(props.topics[0]);
62 | }
63 | }, [props.topics, selectedTopic]);
64 |
65 | useEffect(() => {
66 | api
67 | .get<{ [key: string]: MessageTypeDescription }>("/message_types")
68 | .then((response) => {
69 | const infoDict = response.data;
70 | const selectedM = Object.keys(infoDict)[0];
71 | setMessageInfoDict(infoDict);
72 | setSelectedMessageType(selectedM);
73 | setNewMessage(makeDefaultMessage(infoDict[selectedM]));
74 | })
75 | .catch((error) => console.error("Error fetching message types:", error));
76 | }, []);
77 |
78 | const handleMessageTypeChange = (e: React.ChangeEvent) => {
79 | const newMessageType = e.target.value;
80 |
81 | setNewMessage(makeDefaultMessage(messageInfoDict[newMessageType]));
82 |
83 | setSelectedMessageType(newMessageType);
84 | };
85 |
86 | const handleSubmit = (event: React.FormEvent) => {
87 | event.preventDefault();
88 |
89 | if (selectedAgent === null) {
90 | setErrorMessage("Please select an agent or Publish");
91 | return;
92 | }
93 | if (selectedMessageType === null) {
94 | setErrorMessage("Please select a message type");
95 | return;
96 | }
97 | if (newMessage == undefined || newMessage == "") {
98 | setErrorMessage("Please enter a message");
99 | return;
100 | }
101 |
102 | if (sendType === "publish") {
103 | api
104 | .post("/publish", {
105 | body: newMessage,
106 | type: selectedMessageType,
107 | topic: selectedTopic,
108 | })
109 | .then(() => {
110 | setErrorMessage("");
111 | props.onSend();
112 | setNewMessage(
113 | makeDefaultMessage(messageInfoDict[selectedMessageType]),
114 | );
115 | })
116 | .catch((error) => {
117 | console.error("Error sending message:", error);
118 | setErrorMessage(error.message);
119 | });
120 | } else {
121 | api
122 | .post("/send", {
123 | recipient: selectedAgent,
124 | body: newMessage,
125 | type: selectedMessageType,
126 | })
127 | .then(() => {
128 | setErrorMessage("");
129 | props.onSend();
130 | setNewMessage(
131 | makeDefaultMessage(messageInfoDict[selectedMessageType]),
132 | );
133 | })
134 | .catch((error) => console.error("Error sending message:", error));
135 | }
136 | };
137 |
138 | const { allowActions } = useAllowActions();
139 |
140 | return (
141 |
142 |
143 |
New Message
144 |
145 |
234 |
235 |
236 | );
237 | });
238 |
239 | export default SendMessage;
240 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | // src/App.tsx
2 | import { Container, Section, Bar } from "@column-resizer/react";
3 | import _ from "lodash";
4 | import React, { useEffect, useState, useMemo, useCallback } from "react";
5 |
6 | import { api, step } from "./api.ts";
7 | import AgentList from "./components/AgentList.tsx";
8 | import ConversationOverview from "./components/ConversationOverview.tsx";
9 | import LogList from "./components/LogList.tsx";
10 | import MessageList from "./components/MessageList.tsx";
11 | import MessageQueue from "./components/MessageQueue.tsx";
12 | import RunControls from "./components/RunControls.tsx";
13 | import SendMessage from "./components/SendMessage.tsx";
14 | import type {
15 | AgentName,
16 | Message,
17 | LogMessage,
18 | MessageHistoryMap,
19 | MessageHistoryState,
20 | } from "./shared-types";
21 |
22 | const App: React.FC = () => {
23 | const [agents, setAgents] = useState([]);
24 | const [timeStep, setTimeStep] = useState(0);
25 | const [logs, setLogs] = useState([]);
26 | const [numTasks, setNumTasks] = useState(0);
27 | const [loopRunning, setLoopRunning] = useState(false);
28 | const [messageQueue, setMessageQueue] = useState([]);
29 | const [sessionHistory, setSessionHistory] = useState<
30 | MessageHistoryMap | undefined
31 | >(undefined);
32 | const [currentSession, setCurrentSession] = useState(
33 | undefined,
34 | );
35 | const [allTopics, setAllTopics] = useState([]);
36 |
37 | // timer to poll backend
38 | useEffect(() => {
39 | const interval = setInterval(() => {
40 | setTimeStep(timeStep + 1);
41 | }, 1000);
42 | return () => clearInterval(interval);
43 | }, [timeStep]);
44 |
45 | useEffect(() => {
46 | api
47 | .get("/agents")
48 | .then((response) => {
49 | setAgents((prev) =>
50 | _.isEqual(prev, response.data) ? prev : response.data,
51 | );
52 | })
53 | .catch((error) => console.error("Error fetching agents:", error));
54 |
55 | api
56 | .get("/getMessageQueue")
57 | .then((response) => {
58 | setMessageQueue((prev) =>
59 | _.isEqual(prev, response.data) ? prev : response.data,
60 | );
61 | })
62 | .catch((error) => console.error("Error fetching messages:", error));
63 |
64 | api
65 | .get("/logs")
66 | .then((response) =>
67 | setLogs((prev) =>
68 | _.isEqual(prev, response.data) ? prev : response.data,
69 | ),
70 | )
71 | .catch((error) => console.error("Error fetching logs:", error));
72 |
73 | api
74 | .get("/num_tasks")
75 | .then((response) =>
76 | setNumTasks((prev) =>
77 | _.isEqual(prev, response.data) ? prev : response.data,
78 | ),
79 | )
80 | .catch((error) => console.error("Error fetching tasks:", error));
81 |
82 | api
83 | .get("/getSessionHistory")
84 | .then((response) => {
85 | const historyState = response.data;
86 |
87 | setSessionHistory((prev) =>
88 | _.isEqual(prev, historyState.message_history)
89 | ? prev
90 | : historyState.message_history,
91 | );
92 | setCurrentSession((prev) =>
93 | _.isEqual(prev, historyState.current_session)
94 | ? prev
95 | : historyState.current_session,
96 | );
97 | })
98 | .catch((error) => console.error("Error fetching history:", error));
99 |
100 | api
101 | .get("/loop_status")
102 | .then((response) => {
103 | setLoopRunning((prev) =>
104 | _.isEqual(prev, response.data) ? prev : response.data,
105 | );
106 | })
107 | .catch((error) => console.error("Error fetching loop_status:", error));
108 |
109 | api
110 | .get("/topics")
111 | .then((response) => {
112 | setAllTopics((prev) =>
113 | _.isEqual(prev, response.data) ? prev : response.data,
114 | );
115 | })
116 | .catch((error) => console.error("Error fetching topics:", error));
117 | }, [timeStep]);
118 |
119 | const onProcessNext = useCallback(() => {
120 | step(() => setTimeStep((prev) => prev + 1));
121 | }, []);
122 |
123 | const onDropNext = useCallback(() => {
124 | api
125 | .post("/drop")
126 | .then((response) => {
127 | console.log("Message dropped:", response.data);
128 | setTimeStep((prev) => prev + 1);
129 | })
130 | .catch((error) => console.error("Error dropping next:", error));
131 | }, []);
132 |
133 | const onSend = useCallback(() => {
134 | setTimeStep((prev) => prev + 1);
135 | }, []);
136 |
137 | const setLoop = useCallback((state: "start" | "stop") => {
138 | if (state === "start") {
139 | api
140 | .post("/start_loop")
141 | .then(() => {
142 | setLoopRunning(true);
143 | })
144 | .catch((error) => console.error("Error starting loop:", error));
145 | } else {
146 | api
147 | .post("/stop_loop")
148 | .then(() => {
149 | setLoopRunning(false);
150 | })
151 | .catch((error) => console.error("Error stopping loop:", error));
152 | }
153 | }, []);
154 |
155 | const memoizedAgents = useMemo(() => agents, [agents]);
156 | const memoizedTopics = useMemo(() => allTopics, [allTopics]);
157 | const memoizedLogs = useMemo(() => logs, [logs]);
158 | const memoizedMessageQueue = useMemo(() => messageQueue, [messageQueue]);
159 | const memoizedSessionHistory = useMemo(
160 | () => sessionHistory,
161 | [sessionHistory],
162 | );
163 |
164 | const memoizedRunControls = useMemo(
165 | () => (
166 | 0}
172 | />
173 | ),
174 | [
175 | onProcessNext,
176 | onDropNext,
177 | loopRunning,
178 | setLoop,
179 | memoizedMessageQueue.length,
180 | ],
181 | );
182 |
183 | return (
184 |
185 |
188 |
189 | {/* body */}
190 |
191 |
192 |
195 |
196 |
212 |
213 |
217 |
218 |
219 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 | {memoizedSessionHistory != undefined && currentSession != undefined && (
236 |
240 | )}
241 |
242 |
243 | {/*
*/}
246 |
247 | );
248 | };
249 |
250 | export default App;
251 |
--------------------------------------------------------------------------------
/src/agdebugger/backend.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 | from typing import Any, Dict, List
4 |
5 | from autogen_agentchat.teams import BaseGroupChat
6 | from autogen_core import AgentId, DefaultTopicId, SingleThreadedAgentRuntime, TopicId
7 | from autogen_core._queue import Queue
8 | from autogen_core._single_threaded_agent_runtime import (
9 | PublishMessageEnvelope,
10 | ResponseMessageEnvelope,
11 | RunContext,
12 | SendMessageEnvelope,
13 | )
14 |
15 | from .intervention import AgDebuggerInterventionHandler
16 | from .log import ListHandler # , LogToHistoryHandler
17 | from .serialization import get_message_type_descriptions
18 | from .types import (
19 | AgentInfo,
20 | AGEPublishMessage,
21 | AGESendMessage,
22 | MessageHistorySession,
23 | ScoreResult,
24 | )
25 | from .utils import message_to_json
26 |
27 |
28 | async def wait_for_future(fut): # type: ignore
29 | await fut
30 |
31 |
32 | class BackendRuntimeManager:
33 | def __init__(
34 | self,
35 | groupchat: BaseGroupChat,
36 | logger: logging.Logger,
37 | message_history=None,
38 | state_cache=None,
39 | ):
40 | self._groupchat = groupchat
41 | self.message_info = get_message_type_descriptions()
42 | self.prior_histories: Dict[int, MessageHistorySession] = {}
43 | self.session_counter = 0
44 | self.current_session_reset_from: int | None = None
45 | self.agent_checkpoints = {} if state_cache is None else state_cache
46 | self.run_context: RunContext | None = None
47 | self.intervention_handler = AgDebuggerInterventionHandler(self.checkpoint_agents, message_history)
48 | self.all_topics: List[str] = []
49 | self.log_handler = ListHandler()
50 | logger.addHandler(self.log_handler)
51 | self.ready = False
52 |
53 | print("Initial Backend loaded.")
54 |
55 | async def async_initialize(self) -> None:
56 | if not self.groupchat._initialized:
57 | await self.groupchat._init(self.runtime)
58 |
59 | # manually add all topics from the chat
60 | self.all_topics = [
61 | self.groupchat._group_topic_type,
62 | self.groupchat._output_topic_type,
63 | self.groupchat._group_chat_manager_topic_type,
64 | *self.groupchat._participant_topic_types,
65 | ]
66 |
67 | # add intervention handler since runtime already initialized
68 | if self.runtime._intervention_handlers is None:
69 | self.runtime._intervention_handlers = []
70 | self.runtime._intervention_handlers.append(self.intervention_handler)
71 |
72 | # load the last checkpoint - N.B. might be earlier than last message so we get the max key
73 | if len(self.intervention_handler.history) > 0:
74 | last_checkpoint_time = max(self.agent_checkpoints.keys())
75 | print("resetting to checkpoint: ", last_checkpoint_time)
76 | checkpoint = self.agent_checkpoints.get(last_checkpoint_time)
77 | if checkpoint is not None:
78 | await self.runtime.load_state(checkpoint)
79 |
80 | self.ready = True
81 | print("Finished backend async load")
82 |
83 | @property
84 | def groupchat(self) -> BaseGroupChat:
85 | return self._groupchat
86 |
87 | @property
88 | def runtime(self) -> SingleThreadedAgentRuntime:
89 | return self.groupchat._runtime
90 |
91 | @property
92 | def agent_key(self) -> str:
93 | return self.groupchat._team_id
94 |
95 | @property
96 | def current_score(self) -> ScoreResult | None:
97 | return self.intervention_handler._current_score
98 |
99 | @property
100 | def agent_names(self) -> List[str]:
101 | return list(self.runtime._known_agent_names)
102 |
103 | @property
104 | def message_queue_list(self) -> List[PublishMessageEnvelope | SendMessageEnvelope | ResponseMessageEnvelope]:
105 | # read and serialize without having to reconstruct a new Queue each time
106 | return list(self.runtime._message_queue._queue) # type: ignore
107 |
108 | @property
109 | def unprocessed_messages_count(self):
110 | return self.runtime.unprocessed_messages_count
111 |
112 | @property
113 | def is_processing(self) -> bool:
114 | return self.runtime._run_context is not None
115 |
116 | def start_processing(self) -> None:
117 | self.runtime.start()
118 |
119 | async def process_next(self):
120 | await self.runtime.process_next()
121 |
122 | async def stop_processing(self) -> None:
123 | await self.runtime.stop_when_idle()
124 | # OR maybe below to stop immediatley
125 | # await self.runtime.stop()
126 |
127 | async def checkpoint_agents(self, timestamp: int) -> None:
128 | checkpoint = await self.runtime.save_state()
129 | self.agent_checkpoints[timestamp] = checkpoint
130 |
131 | def get_current_history(self):
132 | return [message_to_json(m.message, m.timestamp) for m in self.intervention_handler.history]
133 |
134 | def save_history_session_from_reset(self, new_reset_from: int) -> None:
135 | self.prior_histories[self.session_counter] = MessageHistorySession(
136 | messages=self.get_current_history(),
137 | current_session_reset_from=self.current_session_reset_from,
138 | next_session_starts_at=None,
139 | current_session_score=self.current_score,
140 | )
141 |
142 | self.session_counter += 1
143 | self.current_session_reset_from = new_reset_from
144 |
145 | def read_current_session_history(self):
146 | saved_sessions = self.prior_histories.copy()
147 |
148 | # save current messages
149 | saved_sessions[self.session_counter] = MessageHistorySession(
150 | messages=self.get_current_history(),
151 | current_session_reset_from=self.current_session_reset_from,
152 | next_session_starts_at=None,
153 | current_session_score=self.current_score,
154 | )
155 | return saved_sessions
156 |
157 | async def get_agent_config(self, agent_name) -> AgentInfo:
158 | agent_id = await self.runtime.get(agent_name, key=self.agent_key)
159 |
160 | if agent_id in self.runtime._instantiated_agents:
161 | agent_state = await self.runtime.agent_save_state(agent_id)
162 | else:
163 | agent_state = "Agent not instantiated yet!"
164 |
165 | return AgentInfo(config={}, state=agent_state)
166 |
167 | def publish_message(self, new_message: Any, topic: str | TopicId):
168 | """
169 | PUBLISH new message to the runtime.
170 | """
171 | if isinstance(topic, str):
172 | topic = DefaultTopicId(topic)
173 |
174 | asyncio.create_task(wait_for_future(self.runtime.publish_message(new_message, topic)))
175 |
176 | async def send_message(self, new_message: Any, recipient: str | AgentId, sender=None):
177 | """
178 | SEND new message to the runtime.
179 | """
180 | agent_id = await self.runtime.get(recipient, key=self.agent_key)
181 | return asyncio.create_task(wait_for_future(self.runtime.send_message(new_message, agent_id, sender=sender)))
182 |
183 | async def edit_message_queue(self, new_message: Any, edit_idx: int):
184 | """
185 | Edit existing message in the runtime queue.
186 | """
187 | if edit_idx >= self.runtime._message_queue.qsize():
188 | raise IndexError(f"Index out of range in queue {edit_idx}")
189 |
190 | # #1 simple way -- directly edit queue array
191 | # backend.runtime._message_queue._queue[editMessage.idx].message = newMessage
192 |
193 | # #2 more robust -- make new queue
194 | current_queue = []
195 | while not self.runtime._message_queue.empty():
196 | current_queue.append(self.runtime._message_queue.get_nowait())
197 |
198 | current_queue[edit_idx].message = new_message
199 |
200 | newQueue = Queue()
201 | for item in current_queue:
202 | await newQueue.put(item)
203 | self.runtime._message_queue = newQueue
204 |
205 | async def edit_and_revert_message(self, new_message: Any | None, cutoff_timestamp: int):
206 | # immediately stop and clear queue
207 | if self.is_processing:
208 | await self.stop_processing()
209 |
210 | current_message = self.intervention_handler.get_message_at_timestamp(cutoff_timestamp)
211 | if current_message is None:
212 | raise ValueError(f"Unable to find message in history with timestamp {cutoff_timestamp}")
213 |
214 | self.save_history_session_from_reset(cutoff_timestamp)
215 | self.intervention_handler.purge_history_after_cutoff(cutoff_timestamp)
216 |
217 | # edit actual message and add to queue
218 | if new_message is None:
219 | new_message = current_message.message.message
220 |
221 | # publish or send as new message
222 | if isinstance(current_message.message, AGEPublishMessage):
223 | self.publish_message(new_message, current_message.message.topic_id)
224 | elif isinstance(current_message.message, AGESendMessage):
225 | await self.send_message(
226 | new_message, current_message.message.recipient, sender=current_message.message.sender
227 | )
228 | else:
229 | raise ValueError(
230 | f"Failed to re-send message after history reset. Unsure how to handle message of type: {current_message.message}"
231 | )
232 |
233 | # NOTE: reset can be slow if heavy state so performing after message is sent.
234 | checkpoint = self.agent_checkpoints.get(cutoff_timestamp, None)
235 | if checkpoint is not None:
236 | await self.runtime.load_state(checkpoint)
237 | else:
238 | print("[WARN] Was unable to find agent state checkpoint for time ", cutoff_timestamp)
239 |
--------------------------------------------------------------------------------
/frontend/src/components/viz/MessageHistoryChart.tsx:
--------------------------------------------------------------------------------
1 | import Tippy from "@tippyjs/react";
2 | import { scaleLinear, scaleBand } from "d3";
3 |
4 | import { useHoveredMessage } from "../../context/HoveredMessageContext";
5 | import type {
6 | MessageHistoryMap,
7 | MessageHistory,
8 | Message,
9 | colorOption,
10 | ResetMap,
11 | } from "../../shared-types";
12 | import MessageTooltip from "./MessageTooltip";
13 | import ScoreTooltip from "./ScoreTooltip";
14 | import { getMessageField } from "./viz-utils";
15 |
16 | interface MessageHistoryChartProps {
17 | messageHistoryData: MessageHistoryMap;
18 | colorField: colorOption;
19 | currentSession?: number;
20 | getColor: (value: string | undefined | null) => string;
21 | sessionResetTimestamps: ResetMap;
22 | }
23 |
24 | // COLORS
25 | const HIGHLIGHT_COLOR = "#f59e0b"; // amber-500
26 | const PRIMARY_COLOR = "#0e7490"; // primary-700
27 |
28 | const MessageHistoryChart: React.FC = ({
29 | messageHistoryData,
30 | colorField,
31 | currentSession,
32 | getColor,
33 | sessionResetTimestamps,
34 | }) => {
35 | const padding = 10;
36 | const rectWidth = 20;
37 | const rectHeight = 10;
38 | const xSpace = 15;
39 | const ySpace = 6;
40 | const axisSpace = 25;
41 |
42 | const longestArrayLength = Math.max(
43 | ...Object.values(messageHistoryData).map((h) => h.messages.length),
44 | );
45 |
46 | const scoreSpace = 25;
47 |
48 | const maxYExtent = (longestArrayLength - 1) * (rectHeight + ySpace);
49 | const maxXExtent =
50 | Object.keys(messageHistoryData).length * (rectWidth + xSpace);
51 | const chartHeight =
52 | padding + axisSpace + maxYExtent + rectHeight + ySpace + scoreSpace;
53 | const chartWidth = padding + maxXExtent;
54 |
55 | const xScale = scaleBand()
56 | .domain(Object.keys(messageHistoryData))
57 | .range([padding, maxXExtent]);
58 |
59 | const yScale = scaleLinear()
60 | .domain([0, longestArrayLength - 1])
61 | .range([
62 | padding + axisSpace + scoreSpace,
63 | padding + axisSpace + scoreSpace + maxYExtent,
64 | ]);
65 |
66 | const isNewMessage = (
67 | timestamp: number,
68 | current_session_reset_from: number | undefined,
69 | ): boolean => {
70 | if (current_session_reset_from === undefined) {
71 | return true;
72 | }
73 | return timestamp >= current_session_reset_from;
74 | };
75 |
76 | const getMessageColor = (
77 | message: Message,
78 | colorField: colorOption,
79 | ): string => {
80 | if (colorField === "none") {
81 | return PRIMARY_COLOR;
82 | }
83 |
84 | const fieldValue = getMessageField(message, colorField);
85 | return getColor(fieldValue);
86 | };
87 |
88 | const getFill = (
89 | message: Message,
90 | colorField: colorOption,
91 | shouldHighlight: boolean,
92 | ): string => {
93 | if (shouldHighlight) {
94 | if (colorField === "none") return HIGHLIGHT_COLOR;
95 | else return "#000000";
96 | }
97 |
98 | return getMessageColor(message, colorField);
99 | };
100 |
101 | const resetMessages = Object.keys(messageHistoryData).reduce(
102 | (acc: ResetMap, sessionKey: string) => {
103 | const session = messageHistoryData[Number(sessionKey)];
104 |
105 | const lookingFor: number | undefined =
106 | sessionResetTimestamps[Number(sessionKey)];
107 |
108 | if (lookingFor === undefined) {
109 | return acc;
110 | }
111 | const firstMessageIndex = session.messages.findIndex(
112 | (message: Message) => message.timestamp === lookingFor,
113 | );
114 | if (firstMessageIndex !== -1) {
115 | acc[Number(sessionKey)] = firstMessageIndex;
116 | }
117 | return acc;
118 | },
119 | {},
120 | );
121 |
122 | const { hoveredMessageId, setHoveredMessageId } = useHoveredMessage();
123 |
124 | // interaction handles
125 | const handleMessageHover = (message: Message) => {
126 | setHoveredMessageId(message.timestamp);
127 | };
128 |
129 | const handleMessageUnhover = () => {
130 | setHoveredMessageId(undefined);
131 | };
132 |
133 | const handleRectClick = (timestamp: number) => {
134 | const target = document.getElementById(`message-timestamp-${timestamp}`);
135 | if (target) {
136 | const offset = 120; // Adjust this value as needed
137 | const bodyRect = document.body.getBoundingClientRect().top;
138 | const elementRect = target.getBoundingClientRect().top;
139 | const elementPosition = elementRect - bodyRect;
140 | const offsetPosition = elementPosition - offset;
141 |
142 | window.scrollTo({
143 | top: offsetPosition,
144 | behavior: "smooth",
145 | });
146 | }
147 | };
148 |
149 | return (
150 |
313 | );
314 | };
315 |
316 | export default MessageHistoryChart;
317 |
--------------------------------------------------------------------------------