diff --git a/apps/browser/app/page.tsx b/apps/browser/app/page.tsx
index 9b31459e..a49659d1 100644
--- a/apps/browser/app/page.tsx
+++ b/apps/browser/app/page.tsx
@@ -47,9 +47,9 @@ function Dojo({ params }: { params: { id?: string } }) {
const { mutateAsync: createChat } = useCreateChat();
const { mutateAsync: updateChatTitle } = useUpdateChatTitle();
- const { logs, isConnected, isStarting, isRunning, handleStart } = useEvoService(
+ const { logs, isConnected, isStarting, isRunning, handleStart, status } = useEvoService(
chatId,
- isAuthenticated
+ isAuthenticated,
);
const workspaceUploadUpdate = useWorkspaceUploadUpdate();
@@ -67,7 +67,7 @@ function Dojo({ params }: { params: { id?: string } }) {
// reset workspace and user files on chatId change
setWorkspace(undefined);
setWorkspaceFiles([]);
- }
+ };
const handleCreateNewChat = async () => {
const id = uuid();
@@ -110,7 +110,7 @@ function Dojo({ params }: { params: { id?: string } }) {
setNewGoalSubmitted({
goal,
- chatId: goalChatId
+ chatId: goalChatId,
});
};
@@ -139,7 +139,8 @@ function Dojo({ params }: { params: { id?: string } }) {
// Set isChatLoading to true when evoService is connected
// and the current chatId matches the current goal (if present)
useEffect(() => {
- const chatIdMatches = !newGoalSubmitted || chatId === newGoalSubmitted.chatId;
+ const chatIdMatches =
+ !newGoalSubmitted || chatId === newGoalSubmitted.chatId;
if (isChatLoading && isConnected && chatIdMatches) {
setIsChatLoading(false);
}
@@ -186,6 +187,7 @@ function Dojo({ params }: { params: { id?: string } }) {
workspaceUploadUpdate(workspace, uploads);
}
}}
+ status={status}
/>
) : (
diff --git a/apps/browser/components/Chat.tsx b/apps/browser/components/Chat.tsx
index 03130697..747465dc 100644
--- a/apps/browser/components/Chat.tsx
+++ b/apps/browser/components/Chat.tsx
@@ -26,6 +26,7 @@ export interface ChatLog {
content?: string;
user: string;
color?: string;
+ created_at?: string
}
export interface ChatProps {
@@ -34,6 +35,7 @@ export interface ChatProps {
isRunning: boolean;
onGoalSubmit: (goal: string) => Promise
;
onUpload: (upload: InMemoryFile[]) => void;
+ status: string | undefined;
}
const Chat: React.FC = ({
@@ -42,6 +44,7 @@ const Chat: React.FC = ({
isRunning,
onGoalSubmit,
onUpload,
+ status
}: ChatProps) => {
const [{ id: chatId, name: chatName }] = useAtom(chatInfoAtom);
const [showDisclaimer, setShowDisclaimer] = useAtom(showDisclaimerAtom);
@@ -87,7 +90,7 @@ const Chat: React.FC = ({
{shouldShowExamplePrompts ? (
) : (
-
+
)}
(null);
const [isAtBottom, setIsAtBottom] = useState(true);
const { data: session } = useSession();
@@ -24,6 +37,15 @@ export default function ChatLogs(props: ChatLogsProps) {
});
};
+ const sanitizedLogs = useMemo(() => {
+ return sanitizeLogs(logs);
+ }, [logs]);
+
+ const [logsDetails, setLogsDetails] = useState<{
+ open: boolean;
+ index: number;
+ }>({ open: false, index: 0 });
+
const handleScroll = useCallback(() => {
// Detect if the user is at the bottom of the list
const container = listContainerRef.current;
@@ -56,10 +78,6 @@ export default function ChatLogs(props: ChatLogsProps) {
};
}, [handleScroll]);
- if (isAtBottom) {
- scrollToBottom();
- }
-
return (
<>
@@ -70,58 +88,108 @@ export default function ChatLogs(props: ChatLogsProps) {
onScroll={handleScroll}
className="w-full flex-1 items-center space-y-6 overflow-y-auto overflow-x-clip px-2 py-3 text-left [scrollbar-gutter:stable]"
>
- {logs.map((msg, index, logs) => {
- const isEvo = msg.user === "evo";
- const previousMessage = logs[index - 1];
+ {sanitizedLogs.map((msg, index) => {
return (
-
-
- {isEvo && previousMessage?.user === "user" ? (
-
- ) : !isEvo ? (
- <>
- {session?.user.image && session?.user.email ? (
-
- ) : !session?.user.email ? (
-
- ) : (
-
- )}
- >
- ) : null}
-
-
- {index === 0 || previousMessage?.user !== msg.user ? (
-
- {session?.user.name && !msg.user.includes("evo")
- ? session?.user.name
- : msg.user.charAt(0).toUpperCase() + msg.user.slice(1)}
+
+ <>
+ {session?.user.image && session?.user.email ? (
+
+ ) : (
+
+ )}
+ >
+
+ <>
+
+
+ {session?.user.name ? session?.user.name : "User"}
+
- ) : null}
+ >
- {msg.title.toString()}
-
- {msg.content?.toString() ?? ""}
-
+ {msg.userMessage}
+
+
+
+
+
+
+
+ <>
+
+
Evo
+ {!isRunning && (
+
+ )}
+
+ >
+ {msg.evoMessage && (
+
+ {msg.evoMessage}
+
+ )}
+ {!msg.evoMessage &&
+ isRunning &&
+ sanitizedLogs.length - 1 === index && (
+ <>
+
+
+
+
+ setLogsDetails({
+ open: true,
+ index,
+ })
+ }
+ >
+ {status}
+
+
+
+ >
+ )}
+ {!msg.evoMessage && !isRunning && (
+
+ It seems I have been interrupted, please try again.
+
+ )}
- {isEvo && isRunning && index === logs.length - 1 && (
-
-
-
- )}
);
})}
+
setLogsDetails({ ...logsDetails, open: false })}
+ logs={sanitizedLogs[logsDetails.index]}
+ />
>
);
}
diff --git a/apps/browser/components/modals/ChatDetails.tsx b/apps/browser/components/modals/ChatDetails.tsx
new file mode 100644
index 00000000..6b92d649
--- /dev/null
+++ b/apps/browser/components/modals/ChatDetails.tsx
@@ -0,0 +1,100 @@
+import { useRef, useState } from "react";
+import Modal from "./ModalBase";
+import ReactMarkdown from "react-markdown";
+import { CaretUp } from "@phosphor-icons/react";
+import { MessageSet } from "@/lib/utils/sanitizeLogsDetails";
+import clsx from "clsx";
+
+interface ChatDetailsProps {
+ isOpen: boolean;
+ onClose: () => void;
+ logs: MessageSet;
+}
+
+export default function ChatDetails({
+ isOpen,
+ onClose,
+ logs,
+}: ChatDetailsProps) {
+ const [expandedStep, setExpandedStep] = useState(null);
+ const contentRefs = useRef<{ [index: number]: HTMLDivElement | null }>({});
+
+ const toggleStep = (step: string, index: number) => {
+ const currentEl = contentRefs.current[index];
+ if (!currentEl) return;
+
+ if (expandedStep !== null && expandedStep !== step) {
+ const currentIndex = Object.keys(logs.details).indexOf(expandedStep);
+ const currentExpandedEl = contentRefs.current[currentIndex];
+ if (currentExpandedEl) currentExpandedEl.style.height = "0px";
+ }
+
+ if (expandedStep === step) {
+ currentEl.style.height = "0px";
+ setExpandedStep(null);
+ } else {
+ currentEl.style.height = `${currentEl.scrollHeight}px`;
+ setExpandedStep(step);
+ }
+ };
+
+ return (
+ {
+ setExpandedStep(null);
+ onClose();
+ }}
+ title="Details"
+ >
+
+ {logs &&
+ Object.entries(logs.details).map(
+ ([stepTitle, stepDetails], index) => (
+
+
+
{
+ contentRefs.current[index] = el;
+ }}
+ className={clsx(
+ "step h-0 overflow-hidden transition-[height] duration-500 ease-in-out"
+ )}
+ >
+ {stepDetails.map((detail, detailIndex) => (
+
+ {detail}
+
+ ))}
+
+
+ )
+ )}
+
+
+ );
+}
diff --git a/apps/browser/lib/hooks/useEvoService.ts b/apps/browser/lib/hooks/useEvoService.ts
index 0aa82c0c..b002be88 100644
--- a/apps/browser/lib/hooks/useEvoService.ts
+++ b/apps/browser/lib/hooks/useEvoService.ts
@@ -30,6 +30,7 @@ export const useEvoService = (
isConnected: boolean;
isStarting: boolean;
isRunning: boolean;
+ status: string | undefined;
handleStart: (goal: string) => Promise;
} => {
const supabase = useSupabaseClient();
@@ -48,6 +49,7 @@ export const useEvoService = (
const [isStarting, setIsStarting] = useState(false);
const [isRunning, setIsRunning] = useState(false);
const [chatLog, setChatLogState] = useState([]);
+ const [status, setStatus] = useState(undefined);
// Mutations
const { mutateAsync: addChatLog } = useAddChatLog();
@@ -101,6 +103,7 @@ export const useEvoService = (
setIsRunning,
setChatLog,
setWorkspace,
+ setStatus,
onGoalCapReached: () => {
setCapReached(true);
setSettingsModalOpen(true);
@@ -226,6 +229,7 @@ export const useEvoService = (
isConnected,
isStarting,
isRunning,
- handleStart
+ handleStart,
+ status
};
};
diff --git a/apps/browser/lib/services/evo/EvoThread.ts b/apps/browser/lib/services/evo/EvoThread.ts
index a14190db..f1f19730 100644
--- a/apps/browser/lib/services/evo/EvoThread.ts
+++ b/apps/browser/lib/services/evo/EvoThread.ts
@@ -6,7 +6,7 @@ import {
ChatLogType,
ChatMessage,
Workspace,
- InMemoryWorkspace
+ InMemoryWorkspace,
} from "@evo-ninja/agents";
export interface EvoThreadConfig {
@@ -14,12 +14,16 @@ export interface EvoThreadConfig {
loadChatLog: (chatId: string) => Promise;
loadWorkspace: (chatId: string) => Promise;
onChatLogAdded: (chatLog: ChatLog) => Promise;
- onMessagesAdded: (type: ChatLogType, messages: ChatMessage[]) => Promise;
+ onMessagesAdded: (
+ type: ChatLogType,
+ messages: ChatMessage[]
+ ) => Promise;
onVariableSet: (key: string, value: string) => Promise;
}
export interface EvoThreadState {
- goal?: string;
+ goal: string | undefined;
+ status: string | undefined;
isRunning: boolean;
isLoading: boolean;
logs: ChatLog[];
@@ -27,6 +31,7 @@ export interface EvoThreadState {
}
export interface EvoThreadCallbacks {
+ setStatus: (status?: string) => void;
setIsRunning: (value: boolean) => void;
setChatLog: (chatLog: ChatLog[]) => void;
setWorkspace: (workspace: Workspace) => void;
@@ -41,6 +46,8 @@ export interface EvoThreadStartOptions {
}
const INIT_STATE: EvoThreadState = {
+ goal: undefined,
+ status: undefined,
isRunning: false,
isLoading: false,
logs: [],
@@ -72,6 +79,7 @@ export class EvoThread {
}
// Dispatch init values
+ this._callbacks.setStatus(INIT_STATE.status);
this._callbacks.setIsRunning(INIT_STATE.isRunning);
this._callbacks.setChatLog(INIT_STATE.logs);
this._callbacks.setWorkspace(INIT_STATE.workspace);
@@ -92,6 +100,7 @@ export class EvoThread {
}
// Send current state to newly connected callbacks
+ this._callbacks.setStatus(this._state.status);
this._callbacks.setIsRunning(this._state.isRunning);
this._callbacks.setChatLog(this._state.logs);
this._callbacks.setWorkspace(this._state.workspace);
@@ -139,12 +148,11 @@ export class EvoThread {
options.openAiApiKey,
this._config.onMessagesAdded,
this._config.onVariableSet,
- (chatLog) =>
- this.onChatLog(chatLog),
- () =>
- this._callbacks?.onGoalCapReached(),
- (error) =>
- this._callbacks?.onError(error)
+ (chatLog) => this.onChatLog(chatLog),
+ (status) => this.onStatusUpdate(status),
+ () => this._callbacks?.onGoalCapReached(),
+ // onError
+ (error) => this._callbacks?.onError(error)
);
if (!evo) {
@@ -202,6 +210,11 @@ export class EvoThread {
await this._config.onChatLogAdded(chatLog);
}
+ private onStatusUpdate(status: string): void {
+ this._state.status = status;
+ this._callbacks?.setStatus(status);
+ }
+
private async runEvo(evo: Evo, goal: string): Promise {
const iterator = evo.run({ goal });
@@ -213,32 +226,29 @@ export class EvoThread {
let stepCounter = 1;
while (this._state.isRunning) {
+ await this.onChatLog({
+ title: `## Step ${stepCounter}`,
+ user: "evo",
+ });
const response = await iterator.next();
this._callbacks?.setWorkspace(this._state.workspace);
if (response.done) {
- const actionTitle = response.value.value.title;
- if (
- actionTitle.includes("onGoalAchieved") ||
- actionTitle === "SUCCESS"
- ) {
- await this.onChatLog({
- title: "## Goal Achieved",
+ // If value is not present is because an unhandled error has happened in Evo
+ if ("value" in response.value) {
+ const isSuccess = response.value.value.type === "success";
+ const message = {
+ title: `## Goal has ${isSuccess ? "" : "not"} been achieved`,
user: "evo",
- });
+ };
+ await this.onChatLog(message);
}
-
this.setIsRunning(false);
evo?.reset();
break;
}
- await this.onChatLog({
- title: `## Step ${stepCounter}`,
- user: "evo",
- });
-
if (!response.done) {
const evoMessage = {
title: `### Action executed:\n${response.value.title}`,
diff --git a/apps/browser/lib/services/evo/createEvoInstance.ts b/apps/browser/lib/services/evo/createEvoInstance.ts
index c1dd2c0a..8c648a87 100644
--- a/apps/browser/lib/services/evo/createEvoInstance.ts
+++ b/apps/browser/lib/services/evo/createEvoInstance.ts
@@ -30,6 +30,7 @@ export function createEvoInstance(
onMessagesAdded: (type: ChatLogType, messages: ChatMessage[]) => Promise,
onVariableSet: (key: string, value: string) => Promise,
onChatLog: (chatLog: ChatLog) => Promise,
+ onStatusUpdate: (status: string) => void,
onGoalCapReached: () => void,
onError: (error: string) => void
): Evo | undefined {
@@ -41,6 +42,15 @@ export function createEvoInstance(
title: message,
});
},
+ onNotice: (msg: string) => {
+ onStatusUpdate(msg);
+ return Promise.resolve();
+ },
+ onSuccess: (msg: string) =>
+ onChatLog({
+ user: "evo",
+ title: msg,
+ })
});
const logger = new Logger([browserLogger, new ConsoleLogger()], {
promptUser: () => Promise.resolve("N/A"),
diff --git a/apps/browser/lib/sys/logger/BrowserLogger.ts b/apps/browser/lib/sys/logger/BrowserLogger.ts
index e78ba681..d99c35c1 100644
--- a/apps/browser/lib/sys/logger/BrowserLogger.ts
+++ b/apps/browser/lib/sys/logger/BrowserLogger.ts
@@ -1,29 +1,51 @@
import { ILogger } from "@evo-ninja/agent-utils";
+type LogLevel = "info" | "warning" | "error";
+
export interface BrowserLoggerConfig {
onLog(markdown: string): Promise;
+ onLogLevel?: Partial Promise>>;
+ onNotice: (markdown: string) => Promise;
+ onSuccess: (markdown: string) => Promise;
}
export class BrowserLogger implements ILogger {
constructor(private _config: BrowserLoggerConfig) {}
- async info(info: string): Promise {
- await this._config.onLog(info);
- }
-
async notice(msg: string): Promise {
- await this._config.onLog(msg);
+ await this._config.onNotice(msg);
}
async success(msg: string): Promise {
- await this._config.onLog(msg);
+ await this._config.onSuccess(msg);
+ }
+
+ async info(info: string): Promise {
+ await Promise.all([
+ this._config.onLog(info),
+ this.getOnLogLevel("info")(info)
+ ]);
}
async warning(msg: string): Promise {
- await this._config.onLog(msg);
+ await Promise.all([
+ this._config.onLog(msg),
+ this.getOnLogLevel("warning")(msg),
+ ]);
}
async error(msg: string): Promise {
- await this._config.onLog(msg);
+ await Promise.all([
+ this._config.onLog(msg),
+ this.getOnLogLevel("error")(msg)
+ ]);
+ }
+
+ private getOnLogLevel(level: LogLevel): (msg: string) => Promise {
+ return (
+ this._config.onLogLevel && this._config.onLogLevel[level]
+ ) || (
+ () => Promise.resolve()
+ );
}
}
diff --git a/apps/browser/lib/utils/sanitizeLogsDetails.ts b/apps/browser/lib/utils/sanitizeLogsDetails.ts
new file mode 100644
index 00000000..cf84743d
--- /dev/null
+++ b/apps/browser/lib/utils/sanitizeLogsDetails.ts
@@ -0,0 +1,63 @@
+import { ChatLog } from "@/components/Chat";
+
+export type MessageSet = {
+ userMessage: string;
+ evoMessage?: string;
+ details: Record;
+};
+
+export function sanitizeLogs(messages: ChatLog[]): MessageSet[] {
+ if (!messages || !messages.length) return [];
+
+ // First, sort the messages by their creation date
+ messages.sort(
+ (a, b) =>
+ new Date(a.created_at as string).getTime() -
+ new Date(b.created_at as string).getTime()
+ );
+
+ return messages.reduce((sanitizedLogs, currentMessage) => {
+ const sanitizedLogsLength = sanitizedLogs.length;
+ const currentSet =
+ sanitizedLogsLength > 0 ? sanitizedLogs[sanitizedLogsLength - 1] : null;
+
+ // If the message is from user, it means its the start of a new set of messages
+ if (currentMessage.user === "user") {
+ sanitizedLogs.push({
+ userMessage: currentMessage.title,
+ details: {},
+ evoMessage: undefined,
+ });
+ return sanitizedLogs;
+ }
+
+ // If there's no initialized set, don't try to fill details
+ if (!currentSet) {
+ return sanitizedLogs;
+ }
+
+ // Only user message (goal) and evo's answer does not start with #
+ // Since user message is handled above, we now for sure that its evo's answer
+ if (!currentMessage.title.startsWith("#")) {
+ currentSet.evoMessage = currentMessage.title;
+ } else {
+ // Steps or onGoal{Status} are the one that starts with two #
+ if (currentMessage.title.startsWith("## ")) {
+ currentSet.details[currentMessage.title] = [];
+ } else {
+ // Get the title and/or content and attach to section
+ const detailKeys = Object.keys(currentSet.details);
+ const currentKey = detailKeys[detailKeys.length - 1];
+ const detailContent = currentMessage.content
+ ? currentMessage.title.concat(`\n${currentMessage.content}`)
+ : currentMessage.title;
+ const currentStep = currentSet.details[currentKey];
+ // To avoid errors in runtime, we guarantee that current step indeed exists
+ if (currentStep) {
+ currentStep.push(detailContent);
+ }
+ }
+ }
+ return sanitizedLogs;
+ }, []);
+}
diff --git a/apps/browser/styles/globals.css b/apps/browser/styles/globals.css
index ab1c3454..7ceb2e68 100644
--- a/apps/browser/styles/globals.css
+++ b/apps/browser/styles/globals.css
@@ -37,6 +37,10 @@ code {
background-position: 56% 58%;
}
+.prose-condensed blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre {
+ @apply !m-0;
+}
+
.prose-file * {
@apply my-0;
}
diff --git a/packages/agent-utils/src/scripts/agent-plugin/index.ts b/packages/agent-utils/src/scripts/agent-plugin/index.ts
index 72d36f95..a220e7a8 100644
--- a/packages/agent-utils/src/scripts/agent-plugin/index.ts
+++ b/packages/agent-utils/src/scripts/agent-plugin/index.ts
@@ -33,12 +33,12 @@ export class AgentPlugin extends Module {
}
public async onGoalAchieved(args: Args_onGoalAchieved): Promise {
- await this._logger.success(`Goal has been achieved: ${args.message}`);
+ await this._logger.success(args.message);
return true;
}
public async onGoalFailed(args: Args_onGoalFailed): Promise {
- await this._logger.error(`Goal could not be achieved: ${args.message}`);
+ await this._logger.error(args.message);
return true;
}
}
diff --git a/packages/agents/src/agent-core/agent/basicFunctionCallLoop.ts b/packages/agents/src/agent-core/agent/basicFunctionCallLoop.ts
index b70f2c73..17acbdb2 100644
--- a/packages/agents/src/agent-core/agent/basicFunctionCallLoop.ts
+++ b/packages/agents/src/agent-core/agent/basicFunctionCallLoop.ts
@@ -27,6 +27,7 @@ export async function* basicFunctionCallLoop(
return ResultOk(finalOutput);
}
+ await context.logger.notice("Analyzing which function should be called...")
const response = await llm.getResponse(logs, agentFunctions);
if (!response) {
@@ -45,6 +46,7 @@ export async function* basicFunctionCallLoop(
continue;
}
+ await context.logger.notice("Executing function: " + sanitizedFunctionAndArgs.value[1].definition.name)
const { result, functionCalled } = await executeAgentFunction(sanitizedFunctionAndArgs.value, fnArgs, context)
// Save large results as variables
diff --git a/packages/agents/src/agents/Evo/index.ts b/packages/agents/src/agents/Evo/index.ts
index 553c0894..6aa689a2 100644
--- a/packages/agents/src/agents/Evo/index.ts
+++ b/packages/agents/src/agents/Evo/index.ts
@@ -76,6 +76,7 @@ export class Evo extends Agent {
const { chat } = this.context;
const { messages } = chat.chatLogs;
+ await this.context.logger.notice("Predicting best next step...")
const prediction =
!this.previousPrediction || this.loopCounter % 2 === 0
? await this.predictBestNextStep(
@@ -110,7 +111,7 @@ export class Evo extends Agent {
const predictionVector = await this.createEmbeddingVector(prediction);
await this.context.logger.info("### Prediction:\n-> " + prediction);
-
+ await this.context.logger.notice("Finding best agent to execute step...")
const [agent, agentFunctions, persona, allFunctions] = await findBestAgent(
predictionVector,
this.context
diff --git a/packages/agents/src/functions/OnGoalAchieved.ts b/packages/agents/src/functions/OnGoalAchieved.ts
index 827f5490..680e88bb 100644
--- a/packages/agents/src/functions/OnGoalAchieved.ts
+++ b/packages/agents/src/functions/OnGoalAchieved.ts
@@ -8,13 +8,13 @@ interface OnGoalAchievedFuncParameters {
export class OnGoalAchievedFunction extends ScriptFunction {
name: string = "agent_onGoalAchieved";
- description: string = "Informs the user that the goal has been achieved.";
+ description: string = "Informs the user that the goal has been achieved. Returns as message a complete and explicit answer for user's question";
parameters: any = {
type: "object",
properties: {
message: {
type: "string",
- description: "information about how the goal was achieved",
+ description: "Complete and explicit answer for user's question",
},
},
required: ["message"],
diff --git a/packages/agents/src/functions/OnGoalFailed.ts b/packages/agents/src/functions/OnGoalFailed.ts
index f9fbd0c9..af98cdb5 100644
--- a/packages/agents/src/functions/OnGoalFailed.ts
+++ b/packages/agents/src/functions/OnGoalFailed.ts
@@ -8,13 +8,13 @@ interface OnGoalFailedFuncParameters {
export class OnGoalFailedFunction extends ScriptFunction<{}> {
name: string = "agent_onGoalFailed";
- description: string = `Informs the user that the agent could not achieve the goal.`;
+ description: string = `Informs the user that the agent could not achieve the goal. Returns an explanation of why the goal could not be achieved`;
parameters: any = {
type: "object",
properties: {
message: {
type: "string",
- description: "information about how the goal was achieved",
+ description: "Explanation of why the goal could not be achieved",
},
},
required: ["message"],