Skip to content

Commit

Permalink
Human-in-the-Loop (HITL) (#52)
Browse files Browse the repository at this point in the history
# Human-in-the-Loop (HITL) Implementation for Enhanced Task Oversight

This PR implements Human-in-the-Loop (HITL) support to allow for manual
intervention in tasks that require human oversight, addressing [issue
#35](#35). The changes
enhance AgenticJS's ability to handle complex tasks that benefit from
human judgment while maintaining automated efficiency.

## Key Changes:

1. **AgentsBoardDebugger Component**: Added functionality for providing
feedback and validating tasks, including UI elements for task selection,
feedback entry, and submission.

2. **React Playground**: Added a new story for ResumeCreationTeam with
HITL integration using OpenAI models.

3. **Agent and Task Classes**: Updated with new methods for workflow
management, feedback provision, and task validation.

4. **Task and Team Stores**: Implemented new enums and logic for
handling task completion, errors, validation, and feedback processing.

5. **Workflow Controller**: Added helper function to check agent
availability and manage task status transitions.

6. **Subscribers**: Updated taskSubscriber and teamSubscriber to support
new task statuses and provide more specific status messages.

7. **Enums**: Added new enums for feedback and task statuses
(AWAITING_VALIDATION, VALIDATED, PENDING, PROCESSED).

8. **Test Updates**: Modified various test files to reflect new HITL
functionality, including updates to snapshots and the addition of new
test cases for OpenAI agents with HITL features.

9. **API Mocks**: Updated request/response mocks for OpenAI and Gemini
to support HITL testing scenarios.

10. **Utility Updates**: Enhanced moscaFetch function to improve request
body comparison accuracy in tests.

These changes collectively implement a robust HITL system within
AgenticJS, allowing for seamless integration of human oversight in
AI-driven workflows. This enhancement improves the accuracy,
reliability, and ethical considerations of complex or sensitive
operations managed by the library.
  • Loading branch information
darielnoel authored Aug 30, 2024
2 parents 61b7914 + 3a2e726 commit 7a2b411
Show file tree
Hide file tree
Showing 31 changed files with 58,603 additions and 12,949 deletions.
4 changes: 2 additions & 2 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-syntax-import-meta"]
// "presets": ["@babel/preset-env"],
// "plugins": ["@babel/plugin-syntax-import-meta"]
}
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { Agent, Task, Team } from 'agenticjs';
const { Agent, Task, Team } = require("agenticjs");
```

> Note: AgenticJS is typescript supported. To learn more, check out the [NodeJS TypeScript example](https://github.com/AI-Champions/AgenticJS/blob/main/playground/nodejs-ts/README.md).
> Note: AgenticJS is TypeScript-supported. To learn more, check out the [NodeJS TypeScript example](https://github.com/AI-Champions/AgenticJS/blob/main/playground/nodejs-ts/README.md).
## Example Usage

Expand Down Expand Up @@ -97,9 +97,19 @@ const team = new Team({
env: {OPENAI_API_KEY: 'your-open-ai-api-key'} // Environment variables for the team
});

team.start().then((result) => {
console.log("Final Output:", result);
});
// Listen to the workflow status changes
// team.onWorkflowStatusChange((status) => {
// console.log("Workflow status:", status);
// });

team.start()
.then((output) => {
console.log("Workflow status:", output.status);
console.log("Result:", output.result);
})
.catch((error) => {
console.error("Workflow encountered an error:", error);
});
```

## Basic Concepts
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"dev": "NODE_ENV=development npx rollup -c -w",
"test": "npm run build:test && npm run test:integration",
"test:watch": "TEST_ENV=mocked-llm-apis jest --testPathPattern='tests/e2e' --watch",
"test:debug": "TEST_ENV=real-llm-apis node --inspect-brk node_modules/.bin/jest --runInBand --verbose --testPathPattern='tests/e2e'",
"test:debug": "TEST_ENV=mocked-llm-apis node --inspect-brk node_modules/.bin/jest --runInBand --verbose --testPathPattern='tests/e2e'",
"test:prod": "npm run build && jest --testPathPattern='tests/e2e'",
"test:integration": "TEST_ENV=mocked-llm-apis jest --testPathPattern='tests/e2e'",
"test:e2e": "TEST_ENV=real-llm-apis jest --testPathPattern='tests/e2e'",
Expand Down
81 changes: 78 additions & 3 deletions playground/react/src/AgentsBoardDebugger.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,42 @@ import React, { useState, useEffect } from 'react';
const AgentsBoardDebugger = ({team, title=null}) => {

const useTeamStore = team.useStore();
const { agents, tasks, workflowLogs, teamWorkflowStatus, workflowResult, inputs, setInputs, workflowContext } = useTeamStore(state => ({
const { agents, tasks, workflowLogs, teamWorkflowStatus, workflowResult, inputs, setInputs, workflowContext, provideFeedback, validateTask } = useTeamStore(state => ({
agents: state.agents,
tasks: state.tasks,
workflowLogs: state.workflowLogs,
teamWorkflowStatus: state.teamWorkflowStatus,
workflowResult: state.workflowResult,
inputs: state.inputs,
setInputs: state.setInputs,
workflowContext: state.workflowContext
workflowContext: state.workflowContext,
provideFeedback: state.provideFeedback,
validateTask: state.validateTask
}));

const [openSystemMessage, setOpenSystemMessage] = useState({});
const [openWorkflowContext, setOpenWorkflowContext] = useState(false);

const [feedbackContent, setFeedbackContent] = useState('');
const [selectedTaskId, setSelectedTaskId] = useState('');


const handleFeedbackSubmit = () => {
if (selectedTaskId && feedbackContent) {
provideFeedback(selectedTaskId, feedbackContent);
setFeedbackContent('');
setSelectedTaskId('');
}
};

const handleTaskValidation = (taskId, isApproved) => {
if (isApproved) {
validateTask(taskId);
} else {
provideFeedback(taskId, "Task needs revision");
}
};

const toggleSystemMessage = (id) => {
setOpenSystemMessage(prev => ({
...prev,
Expand Down Expand Up @@ -112,7 +134,60 @@ return (
<div style={{ margin: '20px 0' }}>
<h2 style={{ color: '#666', fontSize: '30px' }}>Workflow Result</h2>
<div>{workflowResult ? workflowResult : 'Not yet available'}</div>
</div>
</div>
<div style={{ margin: '20px 0' }}>
<h2 style={{ color: '#666', fontSize: '30px' }}>👥 Human in the Loop (HITL)</h2>
<select
value={selectedTaskId}
onChange={(e) => setSelectedTaskId(e.target.value)}
style={{ marginRight: '10px', padding: '5px' }}
>
<option value="">Select a task</option>
{tasks.map(task => (
<option key={task.id} value={task.id}>{task.description}</option>
))}
</select>
<input
type="text"
value={feedbackContent}
onChange={(e) => setFeedbackContent(e.target.value)}
placeholder="Enter feedback"
style={{ marginRight: '10px', padding: '5px' }}
/>
<button
onClick={handleFeedbackSubmit}
style={{ padding: '5px 10px', backgroundColor: '#4CAF50', color: 'white', border: 'none', cursor: 'pointer' }}
>
Submit Feedback
</button>
</div>

<div style={{ margin: '20px 0' }}>
<h2 style={{ color: '#666', fontSize: '30px' }}>📝 Tasks</h2>
{tasks.map(task => (
<div key={task.id} style={{ color: '#999', marginBottom: '10px' }}>
<p>🔘 {task.description} - {task.status}</p>
{task.status === 'AWAITING_VALIDATION' && (
<div>
<button onClick={() => handleTaskValidation(task.id, true)} style={{ marginRight: '10px' }}>Approve</button>
<button onClick={() => handleTaskValidation(task.id, false)}>Request Revision</button>
</div>
)}
{task.feedbackHistory && task.feedbackHistory.length > 0 && (
<div style={{ marginLeft: '20px' }}>
<strong>Feedback History:</strong>
{task.feedbackHistory.map((feedback, index) => (
<p key={index} style={{ margin: '5px 0' }}>
- {feedback.content} (Status: {feedback.status})
</p>
))}
</div>
)}
</div>
))}
</div>


<div style={{ margin: '20px 0' }}>
<h2 style={{ color: '#666', fontSize: '30px' }}>📋 Workflow Logs</h2>
{workflowLogs.map((log, index) => (
Expand Down
8 changes: 8 additions & 0 deletions playground/react/src/stories/ResumeCreationTeam.stories.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AgentsBoardDebugger from '../AgentsBoardDebugger';
import teamOpenAI from '../teams/resume_creation/openai';
import teamOpenAIHITL from '../teams/resume_creation/openai_with_hitl';


// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
Expand All @@ -15,3 +16,10 @@ export const withOpenAI = {
title: 'With OpenAI Model'
},
};

export const withHITLOpenAI = {
args: {
team: teamOpenAIHITL,
title: 'With HITL and OpenAI Model'
},
};
60 changes: 60 additions & 0 deletions playground/react/src/teams/resume_creation/openai_with_hitl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Agent, Task, Team } from 'agenticjs';

// Define agents
const profileAnalyst = new Agent({
name: 'Mary',
role: 'Profile Analyst',
goal: 'Extract structured information from conversational user input.',
background: 'Data Processor',
tools: [] // Tools are omitted for now
});

const resumeWriter = new Agent({
name: 'Alex Mercer',
role: 'Resume Writer',
goal: `Craft compelling, well-structured resumes
that effectively showcase job seekers qualifications and achievements.`,
background: `Extensive experience in recruiting,
copywriting, and human resources, enabling
effective resume design that stands out to employers.`,
tools: []
});

// Define tasks
const processingTask = new Task({
description: `Extract relevant details such as name,
experience, skills, and job history from the user's 'aboutMe' input.
aboutMe: {aboutMe}`,
expectedOutput: 'Structured data ready to be used for a resume creation.',
agent: profileAnalyst,
externalValidationRequired: true
});

const resumeCreationTask = new Task({
description: `Utilize the structured data to create
a detailed and attractive resume.
Enrich the resume content by inferring additional details from the provided information.
Include sections such as a personal summary, detailed work experience, skills, and educational background.`,
expectedOutput: `A professionally formatted resume in markdown format,
ready for submission to potential employers.`,
agent: resumeWriter
});

// Create a team
const team = new Team({
name: 'Resume Creation Team',
agents: [profileAnalyst, resumeWriter],
tasks: [processingTask, resumeCreationTask],
inputs: { aboutMe: `My name is David Llaca.
JavaScript Developer for 5 years.
I worked for three years at Disney,
where I developed user interfaces for their primary landing pages
using React, NextJS, and Redux. Before Disney,
I was a Junior Front-End Developer at American Airlines,
where I worked with Vue and Tailwind.
I earned a Bachelor of Science in Computer Science from FIU in 2018,
and I completed a JavaScript bootcamp that same year.` }, // Initial input for the first task
env: {OPENAI_API_KEY: import.meta.env.VITE_OPENAI_API_KEY, ANTHROPIC_API_KEY: import.meta.env.VITE_ANTHROPIC_API_KEY}
});

export default team;
4 changes: 2 additions & 2 deletions src/agents/baseAgent.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ class BaseAgent {
this.env = env;
}

executeTask(task) {
throw new Error("executeTask must be implemented by subclasses.");
workOnTask(task) {
throw new Error("workOnTask must be implemented by subclasses.");
}
}

Expand Down
Loading

0 comments on commit 7a2b411

Please sign in to comment.