Closed monarchwadia closed 4 months ago
Hi Monarch,
I've been reviewing the challenges associated with separating concerns within Ragged's tool handling and I'd like to suggest an approach that might simplify implementation while enhancing modularity and maintainability.
I propose using the Event Emitter Pattern to effectively decouple the handling of tool outputs from their operational logic. This pattern facilitates a clear separation between the generation of data (through tool operations) and subsequent actions taken on this data (such as logging or further processing).
Here's how you could integrate this pattern into Ragged:
class EventEmitter {
constructor() {
this.events = {};
}
on(eventName, fn) {
this.events[eventName] = this.events[eventName] || [];
this.events[eventName].push(fn);
}
emit(eventName, data) {
const event = this.events[eventName];
if (event) {
event.forEach(fn => {
fn(data);
});
}
}
}
// Usage within Ragged
const emitter = new EventEmitter();
const tool = new RaggedTool()
.name("theTool")
.handler((payload) => {
emitter.emit("toolUse", payload);
});
// Listening for tool use
emitter.on("toolUse", (payload) => {
console.log(payload); // Handle payload separately from the tool logic
});
This event-driven model provides a robust framework for achieving the flexibility and simplicity needed in Ragged. It supports scalability and clean separation of concerns—key for maintaining and extending your library.
I hope this proposal assists in your development process, and I'm eager to see how it evolves!
Matthew
@CoderDill thanks a lot. This is very interesting, you seem to be onto something... your chain of thought sparked something... I wonder what you think of the following code which follows your model:
const theTool = new RaggedTool()
.name("theTool");
// no more handler
and then accessed as below
const stream = r.predictStream("Call theTool", {tools: [theTool]});
stream
.onToolUse("theTool", (payload, emitter) => {
console.log(payload);
return null; // gets sent to the LLM as a response
});
One downside of this approach is that the toolcall should only be responded to exactly once... so if multiple responses are sent because of multiple handlers, then an error will be thrown... this is the true advantage of the tool.handler
Hey Monarch,
Really appreciate your development on this! Stripping the handler from RaggedTool
and using predictStream
for dynamic handling sounds promising. It definitely opens up flexibility and makes the tool’s use more fluid.
Regarding the concern about multiple responses, a possible solution is to use a flag within the tool instance to ensure each tool responds just once per stream. Something like this might work:
stream.onToolUse("theTool", (payload, emitter) => {
if (!emitter.hasResponded) {
console.log(payload);
emitter.hasResponded = true; // Prevent further responses
return null; // Send this back to the LLM
}
});
This would prevent multiple responses from cluttering up the process, keeping the operation clean and error-free.
Looking forward to your thoughts on this.
Matthew
@CoderDill what do you think of the current API in 0.2.0
? (i just pushed this)
const answer: string = await r
.chat("Say 'Hello World!'")
.firstText();
console.log(answer); // Hello World!
const adder = t
.tool()
.title("adder")
.description("Add two numbers together")
.inputs({
a: t.number().description("first number").isRequired(),
b: t.number().description("second number").isRequired(),
})
.handler((input: { a: number; b: number }) => input.a + input.b);
const answer: string = await r
.chat("Add 1 + 2", { tools: [adder] })
.firstToolResult();
console.log(`answer: ${answer.result}`) // answer: 3
Hey Monarch,
I've reviewed the new API in v0.2.0, and I must say, I'm impressed with the updates. The clear distinction between chat and tool-use functionalities not only simplifies interaction but also enhances the system’s manageability—excellent work there!
Quick Thoughts:
Suggestion:
emitter.hasResponded
to manage response control is clever. It might be beneficial to automate this within the tool management system to streamline code and avoid redundancy.Thank you for pushing the envelope on this project. I'm excited about its direction and am eager to contribute further to its success.
Best regards, Matthew
Thanks @CoderDill for the feedback & suggestions! After working on it for a bit, I think I'm happy with where this interface is -- it's not perfect, and won't ever be. But it's a good enough starting point. Closing this issue.
Does anybody have any suggestions for how to improve this?
Right now, the only way to capture tool use inputs and outputs is in the
handler
.This is problematic because it discourages separation of concerns. Ideally, the return value of the
handler
would hand off the value toragged
which would then emit it as an event.Maybe something like this....? Although I feel like this needs to be improved too.
Or maybe something more fluid. I want to remove rxjs as a dependency anyway. Maybe the returnValue could be something like this...