Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tools): add HumanTool for user interaction during agent workflows #255

Merged
merged 21 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7be475e
feat(tools): add experimental human tool
matiasmolinas Dec 13, 2024
602822e
Merge branch 'i-am-bee:main' into fresh-human-tool-new
matiasmolinas Dec 15, 2024
560c5bc
Merge branch 'main' into fresh-human-tool-new
matiasmolinas Dec 15, 2024
13c4eb0
Merge branch 'fresh-human-tool-new' of https://github.com/matiasmolin…
matiasmolinas Dec 15, 2024
6688e39
style(format): apply formatting fixes to experimental and helper files
matiasmolinas Dec 18, 2024
bb55941
Merge branch 'main' into fresh-human-tool-new
matiasmolinas Dec 18, 2024
2d9ec56
Merge branch 'main' into fresh-human-tool-new
matiasmolinas Dec 26, 2024
75255db
refactor(agents): remove unused prompt templates. The templates objec…
matiasmolinas Dec 31, 2024
cc1627e
fix(io): remove unnecessary comments from prompt method
matiasmolinas Jan 4, 2025
ae19ae2
fix(io): revert change to ensure process.exit(0) is retained
matiasmolinas Jan 4, 2025
0670e9c
fix(io): improve error handling in catch clause
matiasmolinas Jan 4, 2025
51888a9
refactor(tools): simplify type definition in human.ts
matiasmolinas Jan 4, 2025
f5e74f1
fix(tools): update inputSchema to use a named function in human.ts
matiasmolinas Jan 5, 2025
18ce28b
refactor: improve HumanTool and ConsoleReader implementation
matiasmolinas Jan 5, 2025
144b76c
refactor(tools): enhance HumanTool with JSONToolOutput and PromptTemp…
matiasmolinas Jan 6, 2025
bc124ce
refactor(tools): introduce Reader interface and HumanToolInput abstra…
matiasmolinas Jan 6, 2025
e57fee0
Merge branch 'i-am-bee:main' into fresh-human-tool-new
matiasmolinas Jan 6, 2025
87cabf4
refactor(io): simplify error handling in createConsoleReader
matiasmolinas Jan 6, 2025
b4b6b68
refactor(tools): remove unused template property from HumanTool
matiasmolinas Jan 6, 2025
fe5c723
style(tools): fix code formatting in human and IO examples
matiasmolinas Jan 7, 2025
30bc063
Merge branch 'i-am-bee:main' into fresh-human-tool-new
matiasmolinas Jan 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions examples/agents/experimental/human.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import "dotenv/config.js";
import { BeeAgent } from "bee-agent-framework/agents/bee/agent";
import { createConsoleReader } from "../../helpers/io.js"; // Use the examples console reader
import { FrameworkError } from "bee-agent-framework/errors";
import { TokenMemory } from "bee-agent-framework/memory/tokenMemory";
import { Logger } from "bee-agent-framework/logger/logger";
import { OpenMeteoTool } from "bee-agent-framework/tools/weather/openMeteo";

// Import the HumanTool from the updated file
import { HumanTool } from "../../tools/experimental/human.js";

// Set up logger
Logger.root.level = "silent"; // Disable internal logs
const logger = new Logger({ name: "app", level: "trace" });

// Initialize LLM (test against llama as requested)
import { OllamaChatLLM } from "bee-agent-framework/adapters/ollama/chat";
const llm = new OllamaChatLLM({
modelId: "llama3.1",
});

// Create the console reader once, share it with HumanTool
const reader = createConsoleReader();

// Initialize BeeAgent with shared reader for HumanTool
const agent = new BeeAgent({
llm,
memory: new TokenMemory({ llm }),
tools: [
new OpenMeteoTool(),
new HumanTool({
reader: reader,
})
],
});

// Main loop
try {
for await (const { prompt } of reader) {
// Run the agent and observe events
const response = await agent
.run(
{ prompt },
{
execution: {
maxRetriesPerStep: 3,
totalMaxRetries: 10,
maxIterations: 20,
},
},
)
.observe((emitter) => {
// Show only final answers
emitter.on("update", async ({ update }) => {
if (update.key === "final_answer") {
reader.write("Agent 🤖 : ", update.value);
}
});

// Log errors
emitter.on("error", ({ error }) => {
reader.write("Agent 🤖 : ", FrameworkError.ensure(error).dump());
});

// Retry notifications
emitter.on("retry", () => {
reader.write("Agent 🤖 : ", "Retrying the action...");
});
});

// Print the final response
if (response.result?.text) {
reader.write("Agent 🤖 : ", response.result.text);
} else {
reader.write(
"Agent 🤖 : ",
"No result was returned. Ensure your input is valid or check tool configurations.",
);
}
}
} catch (error) {
logger.error(FrameworkError.ensure(error).dump());
} finally {
// Gracefully close the reader when exiting the app
reader.close();
}
16 changes: 14 additions & 2 deletions examples/helpers/io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { stdin, stdout } from "node:process";
import picocolors from "picocolors";
import * as R from "remeda";
import stripAnsi from "strip-ansi";
import type { Abortable } from 'node:events';

interface ReadFromConsoleInput {
fallback?: string;
Expand All @@ -27,16 +28,27 @@ export function createConsoleReader({
.concat("\n"),
);
},

async prompt(): Promise<string> {
for await (const { prompt } of this) {
return prompt;
}
process.exit(0);
},

async askSingleQuestion(queryMessage: string, options?: Abortable): Promise<string> {
const answer = await rl.question(
R.piped(picocolors.cyan, picocolors.bold)(queryMessage),
options ?? { signal: undefined }
);
return stripAnsi(answer.trim());
},

close() {
stdin.pause();
rl.close();
},

async *[Symbol.asyncIterator]() {
if (!isActive) {
return;
Expand Down Expand Up @@ -64,8 +76,8 @@ export function createConsoleReader({
}
yield { prompt, iteration };
}
} catch (e) {
if (e.code === "ERR_USE_AFTER_CLOSE") {
} catch (e: unknown) {
if (e instanceof Error && 'code' in e && e.code === "ERR_USE_AFTER_CLOSE") {
matiasmolinas marked this conversation as resolved.
Show resolved Hide resolved
return;
}
} finally {
Expand Down
111 changes: 111 additions & 0 deletions examples/tools/experimental/human.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Emitter } from "bee-agent-framework/emitter/emitter";
import {
Tool,
BaseToolOptions,
BaseToolRunOptions,
JSONToolOutput,
ToolInput,
ToolEmitter,
} from "bee-agent-framework/tools/base";
import { RunContext } from "bee-agent-framework/context";
import { z } from "zod";
import { PromptTemplate } from "bee-agent-framework/template";

interface HumanToolOutput {
clarification: string;
}

export interface Reader {
write(prefix: string, message: string): void;
askSingleQuestion(prompt: string, options?: { signal?: AbortSignal }): Promise<string>;
}

export interface HumanToolInput extends BaseToolOptions {
reader: Reader;
name?: string;
description?: string;
template?: typeof HumanTool.template;
}

export class HumanTool extends Tool<JSONToolOutput<HumanToolOutput>, HumanToolInput> {
name = "HumanTool";
description = `
This tool is used whenever the user's input is unclear, ambiguous, or incomplete.
The agent MUST invoke this tool when additional clarification is required to proceed.
The output must adhere strictly to the following structure:
- Thought: A single-line description of the need for clarification.
- Function Name: HumanTool
- Function Input: { "message": "Your question to the user for clarification." }
- Function Output: The user's response in JSON format.
Examples:
- Example 1:
Input: "What is the weather?"
Thought: "The user's request lacks a location. I need to ask for clarification."
Function Name: HumanTool
Function Input: { "message": "Could you provide the location for which you would like to know the weather?" }
Function Output: { "clarification": "Santa Fe, Argentina" }
Final Answer: The current weather in Santa Fe, Argentina is 17.3°C with a relative humidity of 48% and a wind speed of 10.1 km/h.

- Example 2:
Input: "Can you help me?"
Thought: "The user's request is too vague. I need to ask for more details."
Function Name: HumanTool
Function Input: { "message": "Could you clarify what kind of help you need?" }
Function Output: { "clarification": "I need help understanding how to use the project management tool." }
Final Answer: Sure, I can help you with the project management tool. Let me know which feature you'd like to learn about or if you'd like a general overview.

- Example 3:
Input: "Translate this sentence."
Thought: "The user's request is incomplete. I need to ask for the sentence they want translated."
Function Name: HumanTool
Function Input: { "message": "Could you specify the sentence you would like me to translate?" }
Function Output: { "clarification": "Translate 'Hello, how are you?' to French." }
Final Answer: The French translation of 'Hello, how are you?' is 'Bonjour, comment vas-tu?'

Note: Do NOT attempt to guess or provide incomplete responses. Always use this tool when in doubt to ensure accurate and meaningful interactions.
`;

static readonly template = new PromptTemplate({
schema: z.object({
clarification: z.string(),
}),
template: `{
"clarification": "{clarification}"
}`,
});
matiasmolinas marked this conversation as resolved.
Show resolved Hide resolved

public readonly emitter: ToolEmitter<ToolInput<this>, JSONToolOutput<HumanToolOutput>> =
Emitter.root.child({
namespace: ["tool", "human"],
creator: this,
});

constructor(protected readonly input: HumanToolInput) {
super(input);
this.name = input?.name || this.name;
this.description = input?.description || this.description;
}

inputSchema() {
return z.object({
message: z.string().min(1, "Message cannot be empty"),
});
}

async _run(
input: ToolInput<this>,
_options: Partial<BaseToolRunOptions>,
run: RunContext<this>
): Promise<JSONToolOutput<HumanToolOutput>> {
// Use the reader from input
this.input.reader.write("HumanTool", input.message);

// Use askSingleQuestion with the signal
const userInput = await this.input.reader.askSingleQuestion("User 👤 : ", { signal: run.signal });

// Return JSONToolOutput with the clarification
return new JSONToolOutput({
clarification: userInput.trim()
});
}
}
Loading