updates to some things

This commit is contained in:
dexhorthy
2025-05-09 09:18:55 -07:00
parent 05291f3e42
commit bca8ce1aa4
9 changed files with 177 additions and 157 deletions

View File

@@ -10,6 +10,7 @@
"dependencies": {
"baml": "^0.0.0",
"express": "^5.1.0",
"humanlayer": "^0.7.7",
"tsx": "^4.15.0",
"typescript": "^5.0.0"
},
@@ -2224,6 +2225,12 @@
"node": ">= 0.8"
}
},
"node_modules/humanlayer": {
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/humanlayer/-/humanlayer-0.7.7.tgz",
"integrity": "sha512-6pk+EvyHDPUJwDex5tGwv/11tn1ZeanRUr2hrDIF3F7zRqB6fstRilpRN5Glc4s+WdnzqCvaBsAQOHk9DyLDww==",
"license": "Apache-2.0"
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",

View File

@@ -9,6 +9,7 @@
"dependencies": {
"baml": "^0.0.0",
"express": "^5.1.0",
"humanlayer": "^0.7.7",
"tsx": "^4.15.0",
"typescript": "^5.0.0"
},

View File

@@ -1,9 +1,8 @@
// cli.ts lets you invoke the agent loop from the command line
import { humanlayer } from "humanlayer";
import { agentLoop, Thread, Event } from "../src/agent";
export async function cli() {
// Get command line arguments, skipping the first two (node and script name)
const args = process.argv.slice(2);
@@ -20,23 +19,31 @@ export async function cli() {
const thread = new Thread([{ type: "user_input", data: message }]);
// Run the agent loop with the thread
const result = await agentLoop(thread);
let lastEvent = result.events.slice(-1)[0];
let newThread = await agentLoop(thread);
let lastEvent = newThread.events.slice(-1)[0];
while (lastEvent.data.intent === "request_more_information") {
const message = await askHuman(lastEvent.data.message);
thread.events.push({ type: "human_response", data: message });
const result = await agentLoop(thread);
lastEvent = result.events.slice(-1)[0];
while (lastEvent.data.intent !== "done_for_now") {
const responseEvent = await askHuman(lastEvent);
thread.events.push(responseEvent);
newThread = await agentLoop(thread);
lastEvent = newThread.events.slice(-1)[0];
}
// print the final result
// optional - you could loop here too
// optional - you could loop here too
console.log(lastEvent.data.message);
process.exit(0);
}
async function askHuman(message: string) {
async function askHuman(lastEvent: Event): Promise<Event> {
if (process.env.HUMANLAYER_API_KEY) {
return await askHumanEmail(lastEvent);
} else {
return await askHumanCLI(lastEvent.data.message);
}
}
async function askHumanCLI(message: string): Promise<Event> {
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
@@ -44,7 +51,66 @@ async function askHuman(message: string) {
return new Promise((resolve) => {
readline.question(`${message}\n> `, (answer: string) => {
resolve(answer);
resolve({ type: "human_response", data: answer });
});
});
}
export async function askHumanEmail(lastEvent: Event): Promise<Event> {
if (!process.env.HUMANLAYER_EMAIL) {
throw new Error("missing or invalid parameters: HUMANLAYER_EMAIL");
}
const hl = humanlayer({ //reads apiKey from env
// name of this agent
runId: "12fa-cli-agent",
verbose: true,
contactChannel: {
// agent should request permission via email
email: {
address: process.env.HUMANLAYER_EMAIL,
}
}
})
if (lastEvent.data.intent === "request_more_information") {
// fetch response synchronously - this will block until reply
const response = await hl.fetchHumanResponse({
spec: {
msg: lastEvent.data.message
}
})
return {
"type": "tool_response",
"data": response
}
}
if (lastEvent.data.intent === "divide") {
// fetch approval synchronously - this will block until reply
const response = await hl.fetchHumanApproval({
spec: {
fn: "divide",
kwargs: {
a: lastEvent.data.a,
b: lastEvent.data.b
}
}
})
if (response.approved) {
const result = lastEvent.data.a / lastEvent.data.b;
console.log("tool_response", result);
return {
"type": "tool_response",
"data": result
};
} else {
return {
"type": "tool_response",
"data": `user denied operation ${lastEvent.data.intent}
with feedback: ${response.comment}`
};
}
}
throw new Error(`unknown tool: ${lastEvent.data.intent}`)
}

View File

@@ -29,6 +29,24 @@ sections:
title: "Chapter 0 - Hello World"
text: "Let's start with a basic TypeScript setup and a hello world program."
steps:
- text: |
This guide is written in TypeScript (yes, a python version is coming soon)
There are many checkpoints between the every file edit in theworkshop steps,
so even if you aren't super familiar with typescript,
you should be able to keep up and run each example.
To run this guide, you'll need a relatively recent version of nodejs and npm installed
You can use whatever nodejs version manager you want, [homebrew](https://formulae.brew.sh/formula/node) is fine
command:
brew install node@20
results:
- text: "You should see the node version"
code: |
node --version
- text: "Copy initial package.json"
file: {src: ./walkthrough/00-package.json, dest: package.json}
- text: "Install dependencies"
@@ -55,7 +73,9 @@ sections:
title: "Chapter 1 - CLI and Agent Loop"
text: "Now let's add BAML and create our first agent with a CLI interface."
steps:
- text: "Install BAML"
- text: |
First, we'll need to install [BAML](https://github.com/boundaryml/baml)
which is a tool for prompting and structured outputs.
command: |
npm i baml
incremental: true
@@ -67,7 +87,7 @@ sections:
command: |
rm baml_src/resume.baml
incremental: true
- text: "Add our starter agent"
- text: "Add our starter agent, a single baml prompt that we'll build on"
file: {src: ./walkthrough/01-agent.baml, dest: baml_src/agent.baml}
- text: "Generate BAML client code"
command: |
@@ -82,9 +102,33 @@ sections:
file: {src: ./walkthrough/01-index.ts, dest: src/index.ts}
- text: "Add the agent implementation"
file: {src: ./walkthrough/01-agent.ts, dest: src/agent.ts}
- text: |
The the BAML code is configured to use OPENAI_API_KEY by default
As you're testing, you can change the model / provider to something else
as you please
client "openai/gpt-4o"
[Docs on baml clients can be found here](https://docs.boundaryml.com/guide/baml-basics/switching-llms)
For example, you can configure [gemini](https://docs.boundaryml.com/ref/llm-client-providers/google-ai-gemini)
or [anthropic](https://docs.boundaryml.com/ref/llm-client-providers/anthropic) as your model provider.
If you want to run the example with no changes, you can set the OPENAI_API_KEY env var to any valid openai key.
command: |
export OPENAI_API_KEY=...
- text: "Try it out"
command: |
npx tsx src/index.ts hello
results:
- text: you should see a familiar response from the model
code: |
{
intent: 'done_for_now',
message: 'Hello! How can I assist you today?'
}
- name: calculator-tools
title: "Chapter 2 - Add Calculator Tools"
@@ -346,13 +390,35 @@ sections:
command: |
npm install humanlayer
incremental: true
- text: "Update agent with HumanLayer integration"
file: {src: ./walkthrough/11-agent.ts, dest: src/agent.ts}
- text: "Update CLI with HumanLayer support"
file: {src: ./walkthrough/11-cli.ts, dest: src/cli.ts}
- text: "Run the CLI"
command: |
npx tsx src/index.ts 'can divide 4 by 5'
npx tsx src/index.ts 'can you divide 4 by 5'
results:
- text: "The last line of your program should mention human review step"
code: |
nextStep { intent: 'divide', a: 4, b: 5 }
HumanLayer: Requested human approval from HumanLayer cloud
- text: |
go ahead and respond to the email with some feedback:
![reject-email](https://github.com/humanlayer/12-factor-agents/blob/main/workshops/2025-05/walkthrough/11-email-reject.png?raw=true)
- text: |
you should get another email with an updated attempt based on your feedback!
You can go ahead and approve this one:
![appove-email](https://github.com/humanlayer/12-factor-agents/blob/main/workshops/2025-05/walkthrough/11-email-approve.png?raw=true)
results:
- text: and your final output will look like
code: |
nextStep {
intent: 'done_for_now',
message: 'The division of 4 by 5 is 0.8. If you have any other calculations or questions, feel free to ask!'
}
The division of 4 by 5 is 0.8. If you have any other calculations or questions, feel free to ask!
- name: humanlayer-webhook
title: "Chapter 12 - HumanLayer Webhook Integration"

View File

@@ -1,111 +0,0 @@
import { AddTool, SubtractTool, DivideTool, MultiplyTool, b } from "../baml_client";
export interface Event {
type: string
data: any;
}
export class Thread {
events: Event[] = [];
constructor(events: Event[]) {
this.events = events;
}
serializeForLLM() {
return this.events.map(e => this.serializeOneEvent(e)).join("\n");
}
trimLeadingWhitespace(s: string) {
return s.replace(/^[ \t]+/gm, '');
}
serializeOneEvent(e: Event) {
return this.trimLeadingWhitespace(`
<${e.data?.intent || e.type}>
${
typeof e.data !== 'object' ? e.data :
Object.keys(e.data).filter(k => k !== 'intent').map(k => `${k}: ${e.data[k]}`).join("\n")}
</${e.data?.intent || e.type}>
`)
}
awaitingHumanResponse(): boolean {
const lastEvent = this.events[this.events.length - 1];
return ['request_more_information', 'done_for_now'].includes(lastEvent.data.intent);
}
awaitingHumanApproval(): boolean {
const lastEvent = this.events[this.events.length - 1];
return lastEvent.data.intent === 'divide';
}
}
export type CalculatorTool = AddTool | SubtractTool | MultiplyTool | DivideTool;
export async function handleNextStep(nextStep: CalculatorTool, thread: Thread): Promise<Thread> {
let result: number;
switch (nextStep.intent) {
case "add":
result = nextStep.a + nextStep.b;
console.log("tool_response", result);
thread.events.push({
"type": "tool_response",
"data": result
});
return thread;
case "subtract":
result = nextStep.a - nextStep.b;
console.log("tool_response", result);
thread.events.push({
"type": "tool_response",
"data": result
});
return thread;
case "multiply":
result = nextStep.a * nextStep.b;
console.log("tool_response", result);
thread.events.push({
"type": "tool_response",
"data": result
});
return thread;
case "divide":
result = nextStep.a / nextStep.b;
console.log("tool_response", result);
thread.events.push({
"type": "tool_response",
"data": result
});
return thread;
}
}
export async function agentLoop(thread: Thread): Promise<Thread> {
while (true) {
const nextStep = await b.DetermineNextStep(thread.serializeForLLM());
console.log("nextStep", nextStep);
thread.events.push({
"type": "tool_call",
"data": nextStep
});
switch (nextStep.intent) {
case "done_for_now":
case "request_more_information":
// response to human, return the next step object
return thread;
case "divide":
// divide is scary, return it for human approval
return thread;
case "add":
case "subtract":
case "multiply":
thread = await handleNextStep(nextStep, thread);
}
}
}

View File

@@ -22,18 +22,11 @@ export async function cli() {
let newThread = await agentLoop(thread);
let lastEvent = newThread.events.slice(-1)[0];
let needsResponse =
newThread.awaitingHumanResponse() ||
newThread.awaitingHumanApproval();
while (needsResponse) {
lastEvent = newThread.events.slice(-1)[0];
while (lastEvent.data.intent !== "done_for_now") {
const responseEvent = await askHuman(lastEvent);
thread.events.push(responseEvent);
newThread = await agentLoop(thread);
// determine if we should loop or if we're done
needsResponse = newThread.awaitingHumanResponse()
|| newThread.awaitingHumanApproval();
lastEvent = newThread.events.slice(-1)[0];
}
// print the final result
@@ -69,7 +62,8 @@ export async function askHumanEmail(lastEvent: Event): Promise<Event> {
}
const hl = humanlayer({ //reads apiKey from env
// name of this agent
runId: "cli-agent",
runId: "12fa-cli-agent",
verbose: true,
contactChannel: {
// agent should request permission via email
email: {
@@ -79,6 +73,7 @@ export async function askHumanEmail(lastEvent: Event): Promise<Event> {
})
if (lastEvent.data.intent === "request_more_information") {
// fetch response synchronously - this will block until reply
const response = await hl.fetchHumanResponse({
spec: {
msg: lastEvent.data.message
@@ -91,7 +86,7 @@ export async function askHumanEmail(lastEvent: Event): Promise<Event> {
}
if (lastEvent.data.intent === "divide") {
// fetch approval synchronously
// fetch approval synchronously - this will block until reply
const response = await hl.fetchHumanApproval({
spec: {
fn: "divide",
@@ -112,7 +107,8 @@ export async function askHumanEmail(lastEvent: Event): Promise<Event> {
} else {
return {
"type": "tool_response",
"data": `user denied operation ${lastEvent.data.intent}`
"data": `user denied operation ${lastEvent.data.intent}
with feedback: ${response.comment}`
};
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 182 KiB

View File

@@ -22,22 +22,15 @@ export async function cli() {
let newThread = await agentLoop(thread);
let lastEvent = newThread.events.slice(-1)[0];
let needsResponse =
newThread.awaitingHumanResponse() ||
newThread.awaitingHumanApproval();
while (needsResponse) {
lastEvent = newThread.events.slice(-1)[0];
while (lastEvent.data.intent !== "done_for_now") {
const responseEvent = await askHuman(lastEvent);
thread.events.push(responseEvent);
newThread = await agentLoop(thread);
// determine if we should loop or if we're done
needsResponse = newThread.awaitingHumanResponse()
|| newThread.awaitingHumanApproval();
lastEvent = newThread.events.slice(-1)[0];
}
// print the final result
// optional - you could loop here too
// optional - you could loop here too
console.log(lastEvent.data.message);
process.exit(0);
}
@@ -63,29 +56,31 @@ async function askHumanCLI(message: string): Promise<Event> {
});
}
async function askHumanEmail(lastEvent: Event): Promise<Event> {
export async function askHumanEmail(lastEvent: Event): Promise<Event> {
if (!process.env.HUMANLAYER_EMAIL) {
throw new Error("missing or invalid parameters: HUMANLAYER_EMAIL");
}
const hl = humanlayer({ //reads apiKey from env
// name of this agent
runId: "cli-agent",
runId: "12fa-cli-agent",
verbose: true,
contactChannel: {
// agent should request permission via email
email: {
address: process.env.HUMANLAYER_EMAIL,
// custom email body - jinja
template: `
agent {{ event.run_id }} is requesting approval for {{event.spec.fn}}
with args: {{event.spec.kwargs}}
<br><br>
reply to this email to approve
agent {{ event.run_id }} is requesting approval for {{event.spec.fn}}
with args: {{event.spec.kwargs}}
<br><br>
reply to this email to approve
`
}
}
})
if (lastEvent.data.intent === "request_more_information") {
// fetch response synchronously - this will block until reply
const response = await hl.fetchHumanResponse({
spec: {
msg: lastEvent.data.message
@@ -98,7 +93,7 @@ async function askHumanEmail(lastEvent: Event): Promise<Event> {
}
if (lastEvent.data.intent === "divide") {
// fetch approval synchronously
// fetch approval synchronously - this will block until reply
const response = await hl.fetchHumanApproval({
spec: {
fn: "divide",