mirror of
https://github.com/humanlayer/12-factor-agents.git
synced 2025-08-20 18:59:53 +03:00
new stuff
This commit is contained in:
2
workshops/2025-05/final/.gitignore
vendored
Normal file
2
workshops/2025-05/final/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
baml_client/
|
||||
node_modules/
|
||||
159
workshops/2025-05/final/README.md
Normal file
159
workshops/2025-05/final/README.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Chapter 11 - Human Approval with HumanLayer
|
||||
|
||||
Integrate with HumanLayer for approvals.
|
||||
|
||||
Install HumanLayer
|
||||
|
||||
npm install humanlayer
|
||||
|
||||
Update agent with HumanLayer integration
|
||||
|
||||
```diff
|
||||
src/agent.ts
|
||||
case "done_for_now":
|
||||
case "request_more_information":
|
||||
- // response to human, return the thread
|
||||
+ // response to human, return the next step object
|
||||
return thread;
|
||||
case "divide":
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>skip this step</summary>
|
||||
|
||||
cp ./walkthrough/11-agent.ts src/agent.ts
|
||||
|
||||
</details>
|
||||
|
||||
Update CLI with HumanLayer support
|
||||
|
||||
```diff
|
||||
src/cli.ts
|
||||
// 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)
|
||||
|
||||
// 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];
|
||||
+ let needsResponse =
|
||||
+ newThread.awaitingHumanResponse() ||
|
||||
+ newThread.awaitingHumanApproval();
|
||||
+
|
||||
+ while (needsResponse) {
|
||||
+ lastEvent = newThread.events.slice(-1)[0];
|
||||
+ 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();
|
||||
}
|
||||
|
||||
// print the final result
|
||||
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,
|
||||
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: "cli-agent",
|
||||
+ contactChannel: {
|
||||
+ // agent should request permission via email
|
||||
+ email: {
|
||||
+ address: process.env.HUMANLAYER_EMAIL,
|
||||
+ }
|
||||
+ }
|
||||
+ })
|
||||
+
|
||||
+ if (lastEvent.data.intent === "request_more_information") {
|
||||
+ const response = await hl.fetchHumanResponse({
|
||||
+ spec: {
|
||||
+ msg: lastEvent.data.message
|
||||
+ }
|
||||
+ })
|
||||
+ return {
|
||||
+ "type": "tool_response",
|
||||
+ "data": response
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if (lastEvent.data.intent === "divide") {
|
||||
+ // fetch approval synchronously
|
||||
+ 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}`
|
||||
+ };
|
||||
+ }
|
||||
+ }
|
||||
+ throw new Error(`unknown tool: ${lastEvent.data.intent}`)
|
||||
+}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>skip this step</summary>
|
||||
|
||||
cp ./walkthrough/11-cli.ts src/cli.ts
|
||||
|
||||
</details>
|
||||
|
||||
Run the CLI
|
||||
|
||||
npx tsx src/index.ts 'can divide 4 by 5'
|
||||
|
||||
153
workshops/2025-05/final/baml_src/agent.baml
Normal file
153
workshops/2025-05/final/baml_src/agent.baml
Normal file
@@ -0,0 +1,153 @@
|
||||
// human tools are async requests to a human
|
||||
type HumanTools = ClarificationRequest | DoneForNow
|
||||
|
||||
class ClarificationRequest {
|
||||
intent "request_more_information" @description("you can request more information from me")
|
||||
message string
|
||||
}
|
||||
|
||||
class DoneForNow {
|
||||
intent "done_for_now"
|
||||
|
||||
message string @description(#"
|
||||
message to send to the user about the work that was done.
|
||||
"#)
|
||||
}
|
||||
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
) -> HumanTools | CalculatorTools {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
prompt #"
|
||||
{{ _.role("system") }}
|
||||
|
||||
You are a helpful assistant that can help with tasks.
|
||||
|
||||
{{ _.role("user") }}
|
||||
|
||||
You are working on the following thread:
|
||||
|
||||
{{ thread }}
|
||||
|
||||
What should the next step be?
|
||||
|
||||
{{ ctx.output_format }}
|
||||
|
||||
Always think about what to do next first, like:
|
||||
|
||||
- ...
|
||||
- ...
|
||||
- ...
|
||||
|
||||
{...} // schema
|
||||
"#
|
||||
}
|
||||
|
||||
test HelloWorld {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
<user_input>
|
||||
hello!
|
||||
</user_input>
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "request_more_information"}})
|
||||
}
|
||||
|
||||
test MathOperation {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
<user_input>
|
||||
can you multiply 3 and 4?
|
||||
</user_input>
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "multiply"}})
|
||||
}
|
||||
|
||||
test LongMath {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
<user_input>
|
||||
can you multiply 3 and 4, then divide the result by 2 and then add 12 to that result?
|
||||
</user_input>
|
||||
|
||||
|
||||
<multiply>
|
||||
a: 3
|
||||
b: 4
|
||||
</multiply>
|
||||
|
||||
|
||||
<tool_response>
|
||||
12
|
||||
</tool_response>
|
||||
|
||||
|
||||
<divide>
|
||||
a: 12
|
||||
b: 2
|
||||
</divide>
|
||||
|
||||
|
||||
<tool_response>
|
||||
6
|
||||
</tool_response>
|
||||
|
||||
|
||||
<add>
|
||||
a: 6
|
||||
b: 12
|
||||
</add>
|
||||
|
||||
|
||||
<tool_response>
|
||||
18
|
||||
</tool_response>
|
||||
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "done_for_now"}})
|
||||
@@assert(answer, {{"18" in this.message}})
|
||||
}
|
||||
|
||||
|
||||
|
||||
test MathOperationWithClarification {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
<user_input>
|
||||
can you multiply 3 and fe1iiaff10
|
||||
</user_input>
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "request_more_information"}})
|
||||
}
|
||||
|
||||
test MathOperationPostClarification {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
<user_input>
|
||||
can you multiply 3 and FD*(#F&& ?
|
||||
</user_input>
|
||||
|
||||
<request_more_information>
|
||||
message: It seems like there was a typo or mistake in your request. Could you please clarify or provide the correct numbers you would like to multiply?
|
||||
</request_more_information>
|
||||
|
||||
<human_response>
|
||||
lets try 12 instead
|
||||
</human_response>
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "multiply"}})
|
||||
@@assert(b, {{this.a == 3}})
|
||||
@@assert(a, {{this.b == 12}})
|
||||
}
|
||||
|
||||
75
workshops/2025-05/final/baml_src/clients.baml
Normal file
75
workshops/2025-05/final/baml_src/clients.baml
Normal file
@@ -0,0 +1,75 @@
|
||||
// Learn more about clients at https://docs.boundaryml.com/docs/snippets/clients/overview
|
||||
|
||||
client<llm> CustomGPT4o {
|
||||
provider openai
|
||||
options {
|
||||
model "gpt-4o"
|
||||
api_key env.OPENAI_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
client<llm> CustomGPT4oMini {
|
||||
provider openai
|
||||
retry_policy Exponential
|
||||
options {
|
||||
model "gpt-4o-mini"
|
||||
api_key env.OPENAI_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
client<llm> CustomSonnet {
|
||||
provider anthropic
|
||||
options {
|
||||
model "claude-3-5-sonnet-20241022"
|
||||
api_key env.ANTHROPIC_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
client<llm> CustomHaiku {
|
||||
provider anthropic
|
||||
retry_policy Constant
|
||||
options {
|
||||
model "claude-3-haiku-20240307"
|
||||
api_key env.ANTHROPIC_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/round-robin
|
||||
client<llm> CustomFast {
|
||||
provider round-robin
|
||||
options {
|
||||
// This will alternate between the two clients
|
||||
strategy [CustomGPT4oMini, CustomHaiku]
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/fallback
|
||||
client<llm> OpenaiFallback {
|
||||
provider fallback
|
||||
options {
|
||||
// This will try the clients in order until one succeeds
|
||||
strategy [CustomGPT4oMini, CustomGPT4oMini]
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/retry
|
||||
retry_policy Constant {
|
||||
max_retries 3
|
||||
// Strategy is optional
|
||||
strategy {
|
||||
type constant_delay
|
||||
delay_ms 200
|
||||
}
|
||||
}
|
||||
|
||||
retry_policy Exponential {
|
||||
max_retries 2
|
||||
// Strategy is optional
|
||||
strategy {
|
||||
type exponential_backoff
|
||||
delay_ms 300
|
||||
multiplier 1.5
|
||||
max_delay_ms 10000
|
||||
}
|
||||
}
|
||||
18
workshops/2025-05/final/baml_src/generators.baml
Normal file
18
workshops/2025-05/final/baml_src/generators.baml
Normal file
@@ -0,0 +1,18 @@
|
||||
// This helps use auto generate libraries you can use in the language of
|
||||
// your choice. You can have multiple generators if you use multiple languages.
|
||||
// Just ensure that the output_dir is different for each generator.
|
||||
generator target {
|
||||
// Valid values: "python/pydantic", "typescript", "ruby/sorbet", "rest/openapi"
|
||||
output_type "typescript"
|
||||
|
||||
// Where the generated code will be saved (relative to baml_src/)
|
||||
output_dir "../"
|
||||
|
||||
// The version of the BAML package you have installed (e.g. same version as your baml-py or @boundaryml/baml).
|
||||
// The BAML VSCode extension version should also match this version.
|
||||
version "0.85.0"
|
||||
|
||||
// Valid values: "sync", "async"
|
||||
// This controls what `b.FunctionName()` will be (sync or async).
|
||||
default_client_mode async
|
||||
}
|
||||
27
workshops/2025-05/final/baml_src/tool_calculator.baml
Normal file
27
workshops/2025-05/final/baml_src/tool_calculator.baml
Normal file
@@ -0,0 +1,27 @@
|
||||
type CalculatorTools = AddTool | SubtractTool | MultiplyTool | DivideTool
|
||||
|
||||
|
||||
class AddTool {
|
||||
intent "add"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class SubtractTool {
|
||||
intent "subtract"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class MultiplyTool {
|
||||
intent "multiply"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class DivideTool {
|
||||
intent "divide"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
3388
workshops/2025-05/final/package-lock.json
generated
Normal file
3388
workshops/2025-05/final/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
workshops/2025-05/final/package.json
Normal file
23
workshops/2025-05/final/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "my-agent",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "tsx src/index.ts",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"baml": "^0.0.0",
|
||||
"express": "^5.1.0",
|
||||
"tsx": "^4.15.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/node": "^20.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"eslint": "^8.0.0",
|
||||
"supertest": "^7.1.0"
|
||||
}
|
||||
}
|
||||
111
workshops/2025-05/final/src/agent.ts
Normal file
111
workshops/2025-05/final/src/agent.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
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 thread
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
50
workshops/2025-05/final/src/cli.ts
Normal file
50
workshops/2025-05/final/src/cli.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// cli.ts lets you invoke the agent loop from the command line
|
||||
|
||||
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);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error("Error: Please provide a message as a command line argument");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Join all arguments into a single message
|
||||
const message = args.join(" ");
|
||||
|
||||
// Create a new thread with the user's message as the initial event
|
||||
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];
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
// print the final result
|
||||
// optional - you could loop here too
|
||||
console.log(lastEvent.data.message);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function askHuman(message: string) {
|
||||
const readline = require('readline').createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
readline.question(`${message}\n> `, (answer: string) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
}
|
||||
11
workshops/2025-05/final/src/index.ts
Normal file
11
workshops/2025-05/final/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { cli } from "./cli"
|
||||
|
||||
async function hello(): Promise<void> {
|
||||
console.log('hello, world!')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await cli()
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
111
workshops/2025-05/final/src/server.ts
Normal file
111
workshops/2025-05/final/src/server.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import express from 'express';
|
||||
import { Thread, agentLoop, handleNextStep } from '../src/agent';
|
||||
import { ThreadStore } from '../src/state';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
const store = new ThreadStore();
|
||||
|
||||
// POST /thread - Start new thread
|
||||
app.post('/thread', async (req, res) => {
|
||||
const thread = new Thread([{
|
||||
type: "user_input",
|
||||
data: req.body.message
|
||||
}]);
|
||||
|
||||
const threadId = store.create(thread);
|
||||
const newThread = await agentLoop(thread);
|
||||
|
||||
store.update(threadId, newThread);
|
||||
|
||||
const lastEvent = newThread.events[newThread.events.length - 1];
|
||||
// If we exited the loop, include the response URL so the client can
|
||||
// push a new message onto the thread
|
||||
lastEvent.data.response_url = `/thread/${threadId}/response`;
|
||||
|
||||
console.log("returning last event from endpoint", lastEvent);
|
||||
|
||||
res.json({
|
||||
thread_id: threadId,
|
||||
...newThread
|
||||
});
|
||||
});
|
||||
|
||||
// GET /thread/:id - Get thread status
|
||||
app.get('/thread/:id', (req, res) => {
|
||||
const thread = store.get(req.params.id);
|
||||
if (!thread) {
|
||||
return res.status(404).json({ error: "Thread not found" });
|
||||
}
|
||||
res.json(thread);
|
||||
});
|
||||
|
||||
|
||||
type ApprovalPayload = {
|
||||
type: "approval";
|
||||
approved: boolean;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
type ResponsePayload = {
|
||||
type: "response";
|
||||
response: string;
|
||||
}
|
||||
|
||||
type Payload = ApprovalPayload | ResponsePayload;
|
||||
|
||||
// POST /thread/:id/response - Handle clarification response
|
||||
app.post('/thread/:id/response', async (req, res) => {
|
||||
let thread = store.get(req.params.id);
|
||||
if (!thread) {
|
||||
return res.status(404).json({ error: "Thread not found" });
|
||||
}
|
||||
|
||||
const body: Payload = req.body;
|
||||
|
||||
let lastEvent = thread.events[thread.events.length - 1];
|
||||
|
||||
if (thread.awaitingHumanResponse() && body.type === 'response') {
|
||||
thread.events.push({
|
||||
type: "human_response",
|
||||
data: body.response
|
||||
});
|
||||
} else if (thread.awaitingHumanApproval() && body.type === 'approval' && !body.approved) {
|
||||
// push feedback onto the thread
|
||||
thread.events.push({
|
||||
type: "tool_response",
|
||||
data: `user denied the operation with feedback: "${body.comment}"`
|
||||
});
|
||||
} else if (thread.awaitingHumanApproval() && body.type === 'approval' && body.approved) {
|
||||
// approved, run the tool, pushing results onto the thread
|
||||
await handleNextStep(lastEvent.data, thread);
|
||||
} else {
|
||||
res.status(400).json({
|
||||
error: "Invalid request: " + body.type,
|
||||
awaitingHumanResponse: thread.awaitingHumanResponse(),
|
||||
awaitingHumanApproval: thread.awaitingHumanApproval()
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// loop until stop event
|
||||
const result = await agentLoop(thread);
|
||||
|
||||
store.update(req.params.id, result);
|
||||
|
||||
lastEvent = result.events[result.events.length - 1];
|
||||
lastEvent.data.response_url = `/thread/${req.params.id}/response`;
|
||||
|
||||
console.log("returning last event from endpoint", lastEvent);
|
||||
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
app.listen(port, () => {
|
||||
console.log(`Server running on port ${port}`);
|
||||
});
|
||||
|
||||
export { app };
|
||||
23
workshops/2025-05/final/src/state.ts
Normal file
23
workshops/2025-05/final/src/state.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import crypto from 'crypto';
|
||||
import { Thread } from '../src/agent';
|
||||
|
||||
|
||||
// you can replace this with any simple state management,
|
||||
// e.g. redis, sqlite, postgres, etc
|
||||
export class ThreadStore {
|
||||
private threads: Map<string, Thread> = new Map();
|
||||
|
||||
create(thread: Thread): string {
|
||||
const id = crypto.randomUUID();
|
||||
this.threads.set(id, thread);
|
||||
return id;
|
||||
}
|
||||
|
||||
get(id: string): Thread | undefined {
|
||||
return this.threads.get(id);
|
||||
}
|
||||
|
||||
update(id: string, thread: Thread): void {
|
||||
this.threads.set(id, thread);
|
||||
}
|
||||
}
|
||||
24
workshops/2025-05/final/tsconfig.json
Normal file
24
workshops/2025-05/final/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", "walkthrough"]
|
||||
}
|
||||
|
||||
111
workshops/2025-05/final/walkthrough/11-agent.ts
Normal file
111
workshops/2025-05/final/walkthrough/11-agent.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
120
workshops/2025-05/final/walkthrough/11-cli.ts
Normal file
120
workshops/2025-05/final/walkthrough/11-cli.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
// 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);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error("Error: Please provide a message as a command line argument");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Join all arguments into a single message
|
||||
const message = args.join(" ");
|
||||
|
||||
// Create a new thread with the user's message as the initial event
|
||||
const thread = new Thread([{ type: "user_input", data: message }]);
|
||||
|
||||
// Run the agent loop with the thread
|
||||
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];
|
||||
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();
|
||||
}
|
||||
|
||||
// print the final result
|
||||
// optional - you could loop here too
|
||||
console.log(lastEvent.data.message);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
readline.question(`${message}\n> `, (answer: string) => {
|
||||
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: "cli-agent",
|
||||
contactChannel: {
|
||||
// agent should request permission via email
|
||||
email: {
|
||||
address: process.env.HUMANLAYER_EMAIL,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (lastEvent.data.intent === "request_more_information") {
|
||||
const response = await hl.fetchHumanResponse({
|
||||
spec: {
|
||||
msg: lastEvent.data.message
|
||||
}
|
||||
})
|
||||
return {
|
||||
"type": "tool_response",
|
||||
"data": response
|
||||
}
|
||||
}
|
||||
|
||||
if (lastEvent.data.intent === "divide") {
|
||||
// fetch approval synchronously
|
||||
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}`
|
||||
};
|
||||
}
|
||||
}
|
||||
throw new Error(`unknown tool: ${lastEvent.data.intent}`)
|
||||
}
|
||||
125
workshops/2025-05/sections/00-hello-world/README.md
Normal file
125
workshops/2025-05/sections/00-hello-world/README.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Chapter 0 - Hello World
|
||||
|
||||
Let's start with a basic TypeScript setup and a hello world program.
|
||||
|
||||
Copy initial package.json
|
||||
|
||||
cp ./walkthrough/00-package.json package.json
|
||||
|
||||
<details>
|
||||
<summary>show file</summary>
|
||||
|
||||
```json
|
||||
// ./walkthrough/00-package.json
|
||||
{
|
||||
"name": "my-agent",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "tsx src/index.ts",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"tsx": "^4.15.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"eslint": "^8.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Install dependencies
|
||||
|
||||
npm install
|
||||
|
||||
Copy tsconfig.json
|
||||
|
||||
cp ./walkthrough/00-tsconfig.json tsconfig.json
|
||||
|
||||
<details>
|
||||
<summary>show file</summary>
|
||||
|
||||
```json
|
||||
// ./walkthrough/00-tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", "walkthrough"]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
add .gitignore
|
||||
|
||||
cp ./walkthrough/00-.gitignore .gitignore
|
||||
|
||||
<details>
|
||||
<summary>show file</summary>
|
||||
|
||||
```gitignore
|
||||
// ./walkthrough/00-.gitignore
|
||||
baml_client/
|
||||
node_modules/
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Create src folder
|
||||
|
||||
mkdir -p src
|
||||
|
||||
Add a simple hello world index.ts
|
||||
|
||||
cp ./walkthrough/00-index.ts src/index.ts
|
||||
|
||||
<details>
|
||||
<summary>show file</summary>
|
||||
|
||||
```ts
|
||||
// ./walkthrough/00-index.ts
|
||||
async function hello(): Promise<void> {
|
||||
console.log('hello, world!')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await hello()
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Run it to verify
|
||||
|
||||
npx tsx src/index.ts
|
||||
|
||||
You should see:
|
||||
|
||||
hello, world!
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
baml_client/
|
||||
node_modules/
|
||||
@@ -0,0 +1,9 @@
|
||||
async function hello(): Promise<void> {
|
||||
console.log('hello, world!')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await hello()
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "my-agent",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "tsx src/index.ts",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"tsx": "^4.15.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"eslint": "^8.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", "walkthrough"]
|
||||
}
|
||||
|
||||
2
workshops/2025-05/sections/01-cli-and-agent/.gitignore
vendored
Normal file
2
workshops/2025-05/sections/01-cli-and-agent/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
baml_client/
|
||||
node_modules/
|
||||
181
workshops/2025-05/sections/01-cli-and-agent/README.md
Normal file
181
workshops/2025-05/sections/01-cli-and-agent/README.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# Chapter 1 - CLI and Agent Loop
|
||||
|
||||
Now let's add BAML and create our first agent with a CLI interface.
|
||||
|
||||
Install BAML
|
||||
|
||||
npm i baml
|
||||
|
||||
Initialize BAML
|
||||
|
||||
npx baml-cli init
|
||||
|
||||
Remove default resume.baml
|
||||
|
||||
rm baml_src/resume.baml
|
||||
|
||||
Add our starter agent
|
||||
|
||||
cp ./walkthrough/01-agent.baml baml_src/agent.baml
|
||||
|
||||
<details>
|
||||
<summary>show file</summary>
|
||||
|
||||
```rust
|
||||
// ./walkthrough/01-agent.baml
|
||||
class DoneForNow {
|
||||
intent "done_for_now"
|
||||
message string
|
||||
}
|
||||
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
) -> DoneForNow {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
prompt #"
|
||||
{{ _.role("system") }}
|
||||
|
||||
You are a helpful assistant that can help with tasks.
|
||||
|
||||
{{ _.role("user") }}
|
||||
|
||||
You are working on the following thread:
|
||||
|
||||
{{ thread }}
|
||||
|
||||
What should the next step be?
|
||||
|
||||
{{ ctx.output_format }}
|
||||
"#
|
||||
}
|
||||
|
||||
test HelloWorld {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "hello!"
|
||||
}
|
||||
"#
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Generate BAML client code
|
||||
|
||||
npx baml-cli generate
|
||||
|
||||
Enable BAML logging for development
|
||||
|
||||
export BAML_LOG=debug
|
||||
|
||||
Add the CLI interface
|
||||
|
||||
cp ./walkthrough/01-cli.ts src/cli.ts
|
||||
|
||||
<details>
|
||||
<summary>show file</summary>
|
||||
|
||||
```ts
|
||||
// ./walkthrough/01-cli.ts
|
||||
// cli.ts lets you invoke the agent loop from the command line
|
||||
|
||||
import { agentLoop, Thread, Event } from "./agent";
|
||||
|
||||
export async function cli() {
|
||||
// Get command line arguments, skipping the first two (node and script name)
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error("Error: Please provide a message as a command line argument");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Join all arguments into a single message
|
||||
const message = args.join(" ");
|
||||
|
||||
// Create a new thread with the user's message as the initial event
|
||||
const thread = new Thread([{ type: "user_input", data: message }]);
|
||||
|
||||
// Run the agent loop with the thread
|
||||
const result = await agentLoop(thread);
|
||||
console.log(result);
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Update index.ts to use the CLI
|
||||
|
||||
```diff
|
||||
src/index.ts
|
||||
+import { cli } from "./cli"
|
||||
+
|
||||
async function hello(): Promise<void> {
|
||||
console.log('hello, world!')
|
||||
|
||||
async function main() {
|
||||
- await hello()
|
||||
+ await cli()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>skip this step</summary>
|
||||
|
||||
cp ./walkthrough/01-index.ts src/index.ts
|
||||
|
||||
</details>
|
||||
|
||||
Add the agent implementation
|
||||
|
||||
cp ./walkthrough/01-agent.ts src/agent.ts
|
||||
|
||||
<details>
|
||||
<summary>show file</summary>
|
||||
|
||||
```ts
|
||||
// ./walkthrough/01-agent.ts
|
||||
import { b } from "../baml_client";
|
||||
|
||||
// tool call or a respond to human tool
|
||||
type AgentResponse = Awaited<ReturnType<typeof b.DetermineNextStep>>;
|
||||
|
||||
export interface Event {
|
||||
type: string
|
||||
data: any;
|
||||
}
|
||||
|
||||
export class Thread {
|
||||
events: Event[] = [];
|
||||
|
||||
constructor(events: Event[]) {
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
serializeForLLM() {
|
||||
// can change this to whatever custom serialization you want to do, XML, etc
|
||||
// e.g. https://github.com/got-agents/agents/blob/59ebbfa236fc376618f16ee08eb0f3bf7b698892/linear-assistant-ts/src/agent.ts#L66-L105
|
||||
return JSON.stringify(this.events);
|
||||
}
|
||||
}
|
||||
|
||||
// right now this just runs one turn with the LLM, but
|
||||
// we'll update this function to handle all the agent logic
|
||||
export async function agentLoop(thread: Thread): Promise<AgentResponse> {
|
||||
const nextStep = await b.DetermineNextStep(thread.serializeForLLM());
|
||||
return nextStep;
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Try it out
|
||||
|
||||
npx tsx src/index.ts hello
|
||||
|
||||
2289
workshops/2025-05/sections/01-cli-and-agent/package-lock.json
generated
Normal file
2289
workshops/2025-05/sections/01-cli-and-agent/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
workshops/2025-05/sections/01-cli-and-agent/package.json
Normal file
20
workshops/2025-05/sections/01-cli-and-agent/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "my-agent",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "tsx src/index.ts",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"tsx": "^4.15.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"eslint": "^8.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
9
workshops/2025-05/sections/01-cli-and-agent/src/index.ts
Normal file
9
workshops/2025-05/sections/01-cli-and-agent/src/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
async function hello(): Promise<void> {
|
||||
console.log('hello, world!')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await hello()
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
24
workshops/2025-05/sections/01-cli-and-agent/tsconfig.json
Normal file
24
workshops/2025-05/sections/01-cli-and-agent/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", "walkthrough"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
class DoneForNow {
|
||||
intent "done_for_now"
|
||||
message string
|
||||
}
|
||||
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
) -> DoneForNow {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
prompt #"
|
||||
{{ _.role("system") }}
|
||||
|
||||
You are a helpful assistant that can help with tasks.
|
||||
|
||||
{{ _.role("user") }}
|
||||
|
||||
You are working on the following thread:
|
||||
|
||||
{{ thread }}
|
||||
|
||||
What should the next step be?
|
||||
|
||||
{{ ctx.output_format }}
|
||||
"#
|
||||
}
|
||||
|
||||
test HelloWorld {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "hello!"
|
||||
}
|
||||
"#
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { b } from "../baml_client";
|
||||
|
||||
// tool call or a respond to human tool
|
||||
type AgentResponse = Awaited<ReturnType<typeof b.DetermineNextStep>>;
|
||||
|
||||
export interface Event {
|
||||
type: string
|
||||
data: any;
|
||||
}
|
||||
|
||||
export class Thread {
|
||||
events: Event[] = [];
|
||||
|
||||
constructor(events: Event[]) {
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
serializeForLLM() {
|
||||
// can change this to whatever custom serialization you want to do, XML, etc
|
||||
// e.g. https://github.com/got-agents/agents/blob/59ebbfa236fc376618f16ee08eb0f3bf7b698892/linear-assistant-ts/src/agent.ts#L66-L105
|
||||
return JSON.stringify(this.events);
|
||||
}
|
||||
}
|
||||
|
||||
// right now this just runs one turn with the LLM, but
|
||||
// we'll update this function to handle all the agent logic
|
||||
export async function agentLoop(thread: Thread): Promise<AgentResponse> {
|
||||
const nextStep = await b.DetermineNextStep(thread.serializeForLLM());
|
||||
return nextStep;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// cli.ts lets you invoke the agent loop from the command line
|
||||
|
||||
import { agentLoop, Thread, Event } from "./agent";
|
||||
|
||||
export async function cli() {
|
||||
// Get command line arguments, skipping the first two (node and script name)
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error("Error: Please provide a message as a command line argument");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Join all arguments into a single message
|
||||
const message = args.join(" ");
|
||||
|
||||
// Create a new thread with the user's message as the initial event
|
||||
const thread = new Thread([{ type: "user_input", data: message }]);
|
||||
|
||||
// Run the agent loop with the thread
|
||||
const result = await agentLoop(thread);
|
||||
console.log(result);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { cli } from "./cli"
|
||||
|
||||
async function hello(): Promise<void> {
|
||||
console.log('hello, world!')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await cli()
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
2
workshops/2025-05/sections/02-calculator-tools/.gitignore
vendored
Normal file
2
workshops/2025-05/sections/02-calculator-tools/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
baml_client/
|
||||
node_modules/
|
||||
70
workshops/2025-05/sections/02-calculator-tools/README.md
Normal file
70
workshops/2025-05/sections/02-calculator-tools/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Chapter 2 - Add Calculator Tools
|
||||
|
||||
Let's add some calculator tools to our agent.
|
||||
|
||||
Add calculator tools definition
|
||||
|
||||
cp ./walkthrough/02-tool_calculator.baml baml_src/tool_calculator.baml
|
||||
|
||||
<details>
|
||||
<summary>show file</summary>
|
||||
|
||||
```rust
|
||||
// ./walkthrough/02-tool_calculator.baml
|
||||
type CalculatorTools = AddTool | SubtractTool | MultiplyTool | DivideTool
|
||||
|
||||
|
||||
class AddTool {
|
||||
intent "add"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class SubtractTool {
|
||||
intent "subtract"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class MultiplyTool {
|
||||
intent "multiply"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class DivideTool {
|
||||
intent "divide"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Update agent to use calculator tools
|
||||
|
||||
```diff
|
||||
baml_src/agent.baml
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
-) -> DoneForNow {
|
||||
+) -> CalculatorTools | DoneForNow {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>skip this step</summary>
|
||||
|
||||
cp ./walkthrough/02-agent.baml baml_src/agent.baml
|
||||
|
||||
</details>
|
||||
|
||||
Generate updated BAML client
|
||||
|
||||
npx baml-cli generate
|
||||
|
||||
Try out the calculator
|
||||
|
||||
npx tsx src/index.ts 'can you add 3 and 4'
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
class DoneForNow {
|
||||
intent "done_for_now"
|
||||
message string
|
||||
}
|
||||
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
) -> DoneForNow {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
prompt #"
|
||||
{{ _.role("system") }}
|
||||
|
||||
You are a helpful assistant that can help with tasks.
|
||||
|
||||
{{ _.role("user") }}
|
||||
|
||||
You are working on the following thread:
|
||||
|
||||
{{ thread }}
|
||||
|
||||
What should the next step be?
|
||||
|
||||
{{ ctx.output_format }}
|
||||
"#
|
||||
}
|
||||
|
||||
test HelloWorld {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "hello!"
|
||||
}
|
||||
"#
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Learn more about clients at https://docs.boundaryml.com/docs/snippets/clients/overview
|
||||
|
||||
client<llm> CustomGPT4o {
|
||||
provider openai
|
||||
options {
|
||||
model "gpt-4o"
|
||||
api_key env.OPENAI_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
client<llm> CustomGPT4oMini {
|
||||
provider openai
|
||||
retry_policy Exponential
|
||||
options {
|
||||
model "gpt-4o-mini"
|
||||
api_key env.OPENAI_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
client<llm> CustomSonnet {
|
||||
provider anthropic
|
||||
options {
|
||||
model "claude-3-5-sonnet-20241022"
|
||||
api_key env.ANTHROPIC_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
client<llm> CustomHaiku {
|
||||
provider anthropic
|
||||
retry_policy Constant
|
||||
options {
|
||||
model "claude-3-haiku-20240307"
|
||||
api_key env.ANTHROPIC_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/round-robin
|
||||
client<llm> CustomFast {
|
||||
provider round-robin
|
||||
options {
|
||||
// This will alternate between the two clients
|
||||
strategy [CustomGPT4oMini, CustomHaiku]
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/fallback
|
||||
client<llm> OpenaiFallback {
|
||||
provider fallback
|
||||
options {
|
||||
// This will try the clients in order until one succeeds
|
||||
strategy [CustomGPT4oMini, CustomGPT4oMini]
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/retry
|
||||
retry_policy Constant {
|
||||
max_retries 3
|
||||
// Strategy is optional
|
||||
strategy {
|
||||
type constant_delay
|
||||
delay_ms 200
|
||||
}
|
||||
}
|
||||
|
||||
retry_policy Exponential {
|
||||
max_retries 2
|
||||
// Strategy is optional
|
||||
strategy {
|
||||
type exponential_backoff
|
||||
delay_ms 300
|
||||
multiplier 1.5
|
||||
max_delay_ms 10000
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// This helps use auto generate libraries you can use in the language of
|
||||
// your choice. You can have multiple generators if you use multiple languages.
|
||||
// Just ensure that the output_dir is different for each generator.
|
||||
generator target {
|
||||
// Valid values: "python/pydantic", "typescript", "ruby/sorbet", "rest/openapi"
|
||||
output_type "typescript"
|
||||
|
||||
// Where the generated code will be saved (relative to baml_src/)
|
||||
output_dir "../"
|
||||
|
||||
// The version of the BAML package you have installed (e.g. same version as your baml-py or @boundaryml/baml).
|
||||
// The BAML VSCode extension version should also match this version.
|
||||
version "0.85.0"
|
||||
|
||||
// Valid values: "sync", "async"
|
||||
// This controls what `b.FunctionName()` will be (sync or async).
|
||||
default_client_mode async
|
||||
}
|
||||
2295
workshops/2025-05/sections/02-calculator-tools/package-lock.json
generated
Normal file
2295
workshops/2025-05/sections/02-calculator-tools/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
workshops/2025-05/sections/02-calculator-tools/package.json
Normal file
20
workshops/2025-05/sections/02-calculator-tools/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "my-agent",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "tsx src/index.ts",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"baml": "^0.0.0",
|
||||
"tsx": "^4.15.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"eslint": "^8.0.0"
|
||||
}
|
||||
}
|
||||
32
workshops/2025-05/sections/02-calculator-tools/src/agent.ts
Normal file
32
workshops/2025-05/sections/02-calculator-tools/src/agent.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { b } from "../baml_client";
|
||||
|
||||
// tool call or a respond to human tool
|
||||
type AgentResponse = Awaited<ReturnType<typeof b.DetermineNextStep>>;
|
||||
|
||||
export interface Event {
|
||||
type: string
|
||||
data: any;
|
||||
}
|
||||
|
||||
export class Thread {
|
||||
events: Event[] = [];
|
||||
|
||||
constructor(events: Event[]) {
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
serializeForLLM() {
|
||||
// can change this to whatever custom serialization you want to do, XML, etc
|
||||
// e.g. https://github.com/got-agents/agents/blob/59ebbfa236fc376618f16ee08eb0f3bf7b698892/linear-assistant-ts/src/agent.ts#L66-L105
|
||||
return JSON.stringify(this.events);
|
||||
}
|
||||
}
|
||||
|
||||
// right now this just runs one turn with the LLM, but
|
||||
// we'll update this function to handle all the agent logic
|
||||
export async function agentLoop(thread: Thread): Promise<AgentResponse> {
|
||||
const nextStep = await b.DetermineNextStep(thread.serializeForLLM());
|
||||
return nextStep;
|
||||
}
|
||||
|
||||
|
||||
23
workshops/2025-05/sections/02-calculator-tools/src/cli.ts
Normal file
23
workshops/2025-05/sections/02-calculator-tools/src/cli.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// cli.ts lets you invoke the agent loop from the command line
|
||||
|
||||
import { agentLoop, Thread, Event } from "./agent";
|
||||
|
||||
export async function cli() {
|
||||
// Get command line arguments, skipping the first two (node and script name)
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error("Error: Please provide a message as a command line argument");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Join all arguments into a single message
|
||||
const message = args.join(" ");
|
||||
|
||||
// Create a new thread with the user's message as the initial event
|
||||
const thread = new Thread([{ type: "user_input", data: message }]);
|
||||
|
||||
// Run the agent loop with the thread
|
||||
const result = await agentLoop(thread);
|
||||
console.log(result);
|
||||
}
|
||||
11
workshops/2025-05/sections/02-calculator-tools/src/index.ts
Normal file
11
workshops/2025-05/sections/02-calculator-tools/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { cli } from "./cli"
|
||||
|
||||
async function hello(): Promise<void> {
|
||||
console.log('hello, world!')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await cli()
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
24
workshops/2025-05/sections/02-calculator-tools/tsconfig.json
Normal file
24
workshops/2025-05/sections/02-calculator-tools/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", "walkthrough"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
class DoneForNow {
|
||||
intent "done_for_now"
|
||||
message string
|
||||
}
|
||||
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
) -> CalculatorTools | DoneForNow {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
prompt #"
|
||||
{{ _.role("system") }}
|
||||
|
||||
You are a helpful assistant that can help with tasks.
|
||||
|
||||
{{ _.role("user") }}
|
||||
|
||||
You are working on the following thread:
|
||||
|
||||
{{ thread }}
|
||||
|
||||
What should the next step be?
|
||||
|
||||
{{ ctx.output_format }}
|
||||
"#
|
||||
}
|
||||
|
||||
test HelloWorld {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "hello!"
|
||||
}
|
||||
"#
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
type CalculatorTools = AddTool | SubtractTool | MultiplyTool | DivideTool
|
||||
|
||||
|
||||
class AddTool {
|
||||
intent "add"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class SubtractTool {
|
||||
intent "subtract"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class MultiplyTool {
|
||||
intent "multiply"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class DivideTool {
|
||||
intent "divide"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
2
workshops/2025-05/sections/03-tool-loop/.gitignore
vendored
Normal file
2
workshops/2025-05/sections/03-tool-loop/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
baml_client/
|
||||
node_modules/
|
||||
172
workshops/2025-05/sections/03-tool-loop/README.md
Normal file
172
workshops/2025-05/sections/03-tool-loop/README.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# Chapter 3 - Process Tool Calls in a Loop
|
||||
|
||||
Now let's add a real agentic loop that can run the tools and get a final answer from the LLM.
|
||||
|
||||
Update agent with tool handling
|
||||
|
||||
```diff
|
||||
src/agent.ts
|
||||
}
|
||||
|
||||
-// right now this just runs one turn with the LLM, but
|
||||
-// we'll update this function to handle all the agent logic
|
||||
-export async function agentLoop(thread: Thread): Promise<AgentResponse> {
|
||||
- const nextStep = await b.DetermineNextStep(thread.serializeForLLM());
|
||||
- return nextStep;
|
||||
+
|
||||
+
|
||||
+export async function agentLoop(thread: Thread): Promise<string> {
|
||||
+
|
||||
+ while (true) {
|
||||
+ const nextStep = await b.DetermineNextStep(thread.serializeForLLM());
|
||||
+ console.log("nextStep", nextStep);
|
||||
+
|
||||
+ switch (nextStep.intent) {
|
||||
+ case "done_for_now":
|
||||
+ // response to human, return the next step object
|
||||
+ return nextStep.message;
|
||||
+ case "add":
|
||||
+ thread.events.push({
|
||||
+ "type": "tool_call",
|
||||
+ "data": nextStep
|
||||
+ });
|
||||
+ const result = nextStep.a + nextStep.b;
|
||||
+ console.log("tool_response", result);
|
||||
+ thread.events.push({
|
||||
+ "type": "tool_response",
|
||||
+ "data": result
|
||||
+ });
|
||||
+ continue;
|
||||
+ default:
|
||||
+ throw new Error(`Unknown intent: ${nextStep.intent}`);
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>skip this step</summary>
|
||||
|
||||
cp ./walkthrough/03-agent.ts src/agent.ts
|
||||
|
||||
</details>
|
||||
|
||||
Try a simple calculation
|
||||
|
||||
npx tsx src/index.ts 'can you add 3 and 4'
|
||||
|
||||
Turn off BAML logs for cleaner output
|
||||
|
||||
export BAML_LOG=off
|
||||
|
||||
Try a multi-step calculation
|
||||
|
||||
npx tsx src/index.ts 'can you add 3 and 4, then add 6 to that result'
|
||||
|
||||
Add handlers for all calculator tools
|
||||
|
||||
```diff
|
||||
src/agent.ts
|
||||
-import { b } from "../baml_client";
|
||||
+import { AddTool, SubtractTool, DivideTool, MultiplyTool, b } from "../baml_client";
|
||||
|
||||
-// tool call or a respond to human tool
|
||||
-type AgentResponse = Awaited<ReturnType<typeof b.DetermineNextStep>>;
|
||||
-
|
||||
export interface Event {
|
||||
type: string
|
||||
}
|
||||
|
||||
+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<string> {
|
||||
console.log("nextStep", nextStep);
|
||||
|
||||
+ thread.events.push({
|
||||
+ "type": "tool_call",
|
||||
+ "data": nextStep
|
||||
+ });
|
||||
+
|
||||
switch (nextStep.intent) {
|
||||
case "done_for_now":
|
||||
return nextStep.message;
|
||||
case "add":
|
||||
- thread.events.push({
|
||||
- "type": "tool_call",
|
||||
- "data": nextStep
|
||||
- });
|
||||
- const result = nextStep.a + nextStep.b;
|
||||
- console.log("tool_response", result);
|
||||
- thread.events.push({
|
||||
- "type": "tool_response",
|
||||
- "data": result
|
||||
- });
|
||||
- continue;
|
||||
- default:
|
||||
- throw new Error(`Unknown intent: ${nextStep.intent}`);
|
||||
+ case "subtract":
|
||||
+ case "multiply":
|
||||
+ case "divide":
|
||||
+ thread = await handleNextStep(nextStep, thread);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>skip this step</summary>
|
||||
|
||||
cp ./walkthrough/03b-agent.ts src/agent.ts
|
||||
|
||||
</details>
|
||||
|
||||
Test subtraction
|
||||
|
||||
npx tsx src/index.ts 'can you subtract 3 from 4'
|
||||
|
||||
Test multiplication
|
||||
|
||||
npx tsx src/index.ts 'can you multiply 3 and 4'
|
||||
|
||||
Test a complex calculation
|
||||
|
||||
npx tsx src/index.ts 'can you multiply 3 and 4, then divide the result by 2 and then add 12 to that result'
|
||||
|
||||
38
workshops/2025-05/sections/03-tool-loop/baml_src/agent.baml
Normal file
38
workshops/2025-05/sections/03-tool-loop/baml_src/agent.baml
Normal file
@@ -0,0 +1,38 @@
|
||||
class DoneForNow {
|
||||
intent "done_for_now"
|
||||
message string
|
||||
}
|
||||
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
) -> CalculatorTools | DoneForNow {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
prompt #"
|
||||
{{ _.role("system") }}
|
||||
|
||||
You are a helpful assistant that can help with tasks.
|
||||
|
||||
{{ _.role("user") }}
|
||||
|
||||
You are working on the following thread:
|
||||
|
||||
{{ thread }}
|
||||
|
||||
What should the next step be?
|
||||
|
||||
{{ ctx.output_format }}
|
||||
"#
|
||||
}
|
||||
|
||||
test HelloWorld {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "hello!"
|
||||
}
|
||||
"#
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Learn more about clients at https://docs.boundaryml.com/docs/snippets/clients/overview
|
||||
|
||||
client<llm> CustomGPT4o {
|
||||
provider openai
|
||||
options {
|
||||
model "gpt-4o"
|
||||
api_key env.OPENAI_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
client<llm> CustomGPT4oMini {
|
||||
provider openai
|
||||
retry_policy Exponential
|
||||
options {
|
||||
model "gpt-4o-mini"
|
||||
api_key env.OPENAI_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
client<llm> CustomSonnet {
|
||||
provider anthropic
|
||||
options {
|
||||
model "claude-3-5-sonnet-20241022"
|
||||
api_key env.ANTHROPIC_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
client<llm> CustomHaiku {
|
||||
provider anthropic
|
||||
retry_policy Constant
|
||||
options {
|
||||
model "claude-3-haiku-20240307"
|
||||
api_key env.ANTHROPIC_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/round-robin
|
||||
client<llm> CustomFast {
|
||||
provider round-robin
|
||||
options {
|
||||
// This will alternate between the two clients
|
||||
strategy [CustomGPT4oMini, CustomHaiku]
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/fallback
|
||||
client<llm> OpenaiFallback {
|
||||
provider fallback
|
||||
options {
|
||||
// This will try the clients in order until one succeeds
|
||||
strategy [CustomGPT4oMini, CustomGPT4oMini]
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/retry
|
||||
retry_policy Constant {
|
||||
max_retries 3
|
||||
// Strategy is optional
|
||||
strategy {
|
||||
type constant_delay
|
||||
delay_ms 200
|
||||
}
|
||||
}
|
||||
|
||||
retry_policy Exponential {
|
||||
max_retries 2
|
||||
// Strategy is optional
|
||||
strategy {
|
||||
type exponential_backoff
|
||||
delay_ms 300
|
||||
multiplier 1.5
|
||||
max_delay_ms 10000
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// This helps use auto generate libraries you can use in the language of
|
||||
// your choice. You can have multiple generators if you use multiple languages.
|
||||
// Just ensure that the output_dir is different for each generator.
|
||||
generator target {
|
||||
// Valid values: "python/pydantic", "typescript", "ruby/sorbet", "rest/openapi"
|
||||
output_type "typescript"
|
||||
|
||||
// Where the generated code will be saved (relative to baml_src/)
|
||||
output_dir "../"
|
||||
|
||||
// The version of the BAML package you have installed (e.g. same version as your baml-py or @boundaryml/baml).
|
||||
// The BAML VSCode extension version should also match this version.
|
||||
version "0.85.0"
|
||||
|
||||
// Valid values: "sync", "async"
|
||||
// This controls what `b.FunctionName()` will be (sync or async).
|
||||
default_client_mode async
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
type CalculatorTools = AddTool | SubtractTool | MultiplyTool | DivideTool
|
||||
|
||||
|
||||
class AddTool {
|
||||
intent "add"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class SubtractTool {
|
||||
intent "subtract"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class MultiplyTool {
|
||||
intent "multiply"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class DivideTool {
|
||||
intent "divide"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
2295
workshops/2025-05/sections/03-tool-loop/package-lock.json
generated
Normal file
2295
workshops/2025-05/sections/03-tool-loop/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
workshops/2025-05/sections/03-tool-loop/package.json
Normal file
20
workshops/2025-05/sections/03-tool-loop/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "my-agent",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "tsx src/index.ts",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"baml": "^0.0.0",
|
||||
"tsx": "^4.15.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"eslint": "^8.0.0"
|
||||
}
|
||||
}
|
||||
32
workshops/2025-05/sections/03-tool-loop/src/agent.ts
Normal file
32
workshops/2025-05/sections/03-tool-loop/src/agent.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { b } from "../baml_client";
|
||||
|
||||
// tool call or a respond to human tool
|
||||
type AgentResponse = Awaited<ReturnType<typeof b.DetermineNextStep>>;
|
||||
|
||||
export interface Event {
|
||||
type: string
|
||||
data: any;
|
||||
}
|
||||
|
||||
export class Thread {
|
||||
events: Event[] = [];
|
||||
|
||||
constructor(events: Event[]) {
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
serializeForLLM() {
|
||||
// can change this to whatever custom serialization you want to do, XML, etc
|
||||
// e.g. https://github.com/got-agents/agents/blob/59ebbfa236fc376618f16ee08eb0f3bf7b698892/linear-assistant-ts/src/agent.ts#L66-L105
|
||||
return JSON.stringify(this.events);
|
||||
}
|
||||
}
|
||||
|
||||
// right now this just runs one turn with the LLM, but
|
||||
// we'll update this function to handle all the agent logic
|
||||
export async function agentLoop(thread: Thread): Promise<AgentResponse> {
|
||||
const nextStep = await b.DetermineNextStep(thread.serializeForLLM());
|
||||
return nextStep;
|
||||
}
|
||||
|
||||
|
||||
23
workshops/2025-05/sections/03-tool-loop/src/cli.ts
Normal file
23
workshops/2025-05/sections/03-tool-loop/src/cli.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// cli.ts lets you invoke the agent loop from the command line
|
||||
|
||||
import { agentLoop, Thread, Event } from "./agent";
|
||||
|
||||
export async function cli() {
|
||||
// Get command line arguments, skipping the first two (node and script name)
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error("Error: Please provide a message as a command line argument");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Join all arguments into a single message
|
||||
const message = args.join(" ");
|
||||
|
||||
// Create a new thread with the user's message as the initial event
|
||||
const thread = new Thread([{ type: "user_input", data: message }]);
|
||||
|
||||
// Run the agent loop with the thread
|
||||
const result = await agentLoop(thread);
|
||||
console.log(result);
|
||||
}
|
||||
11
workshops/2025-05/sections/03-tool-loop/src/index.ts
Normal file
11
workshops/2025-05/sections/03-tool-loop/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { cli } from "./cli"
|
||||
|
||||
async function hello(): Promise<void> {
|
||||
console.log('hello, world!')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await cli()
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
24
workshops/2025-05/sections/03-tool-loop/tsconfig.json
Normal file
24
workshops/2025-05/sections/03-tool-loop/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", "walkthrough"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { b } from "../baml_client";
|
||||
|
||||
// tool call or a respond to human tool
|
||||
type AgentResponse = Awaited<ReturnType<typeof b.DetermineNextStep>>;
|
||||
|
||||
export interface Event {
|
||||
type: string
|
||||
data: any;
|
||||
}
|
||||
|
||||
export class Thread {
|
||||
events: Event[] = [];
|
||||
|
||||
constructor(events: Event[]) {
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
serializeForLLM() {
|
||||
// can change this to whatever custom serialization you want to do, XML, etc
|
||||
// e.g. https://github.com/got-agents/agents/blob/59ebbfa236fc376618f16ee08eb0f3bf7b698892/linear-assistant-ts/src/agent.ts#L66-L105
|
||||
return JSON.stringify(this.events);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function agentLoop(thread: Thread): Promise<string> {
|
||||
|
||||
while (true) {
|
||||
const nextStep = await b.DetermineNextStep(thread.serializeForLLM());
|
||||
console.log("nextStep", nextStep);
|
||||
|
||||
switch (nextStep.intent) {
|
||||
case "done_for_now":
|
||||
// response to human, return the next step object
|
||||
return nextStep.message;
|
||||
case "add":
|
||||
thread.events.push({
|
||||
"type": "tool_call",
|
||||
"data": nextStep
|
||||
});
|
||||
const result = nextStep.a + nextStep.b;
|
||||
console.log("tool_response", result);
|
||||
thread.events.push({
|
||||
"type": "tool_response",
|
||||
"data": result
|
||||
});
|
||||
continue;
|
||||
default:
|
||||
throw new Error(`Unknown intent: ${nextStep.intent}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
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() {
|
||||
// can change this to whatever custom serialization you want to do, XML, etc
|
||||
// e.g. https://github.com/got-agents/agents/blob/59ebbfa236fc376618f16ee08eb0f3bf7b698892/linear-assistant-ts/src/agent.ts#L66-L105
|
||||
return JSON.stringify(this.events);
|
||||
}
|
||||
}
|
||||
|
||||
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<string> {
|
||||
|
||||
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":
|
||||
// response to human, return the next step object
|
||||
return nextStep.message;
|
||||
case "add":
|
||||
case "subtract":
|
||||
case "multiply":
|
||||
case "divide":
|
||||
thread = await handleNextStep(nextStep, thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
workshops/2025-05/sections/04-baml-tests/.gitignore
vendored
Normal file
2
workshops/2025-05/sections/04-baml-tests/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
baml_client/
|
||||
node_modules/
|
||||
144
workshops/2025-05/sections/04-baml-tests/README.md
Normal file
144
workshops/2025-05/sections/04-baml-tests/README.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Chapter 4 - Add Tests to agent.baml
|
||||
|
||||
Let's add some tests to our BAML agent.
|
||||
|
||||
Update agent with tests
|
||||
|
||||
```diff
|
||||
baml_src/agent.baml
|
||||
"#
|
||||
}
|
||||
+
|
||||
+test MathOperation {
|
||||
+ functions [DetermineNextStep]
|
||||
+ args {
|
||||
+ thread #"
|
||||
+ {
|
||||
+ "type": "user_input",
|
||||
+ "data": "can you multiply 3 and 4?"
|
||||
+ }
|
||||
+ "#
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>skip this step</summary>
|
||||
|
||||
cp ./walkthrough/04-agent.baml baml_src/agent.baml
|
||||
|
||||
</details>
|
||||
|
||||
Run the tests
|
||||
|
||||
npx baml-cli test
|
||||
|
||||
Add more complex test cases
|
||||
|
||||
```diff
|
||||
baml_src/agent.baml
|
||||
"#
|
||||
}
|
||||
+ @@assert(hello, {{this.intent == "done_for_now"}})
|
||||
}
|
||||
|
||||
"#
|
||||
}
|
||||
+ @@assert(math_operation, {{this.intent == "multiply"}})
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>skip this step</summary>
|
||||
|
||||
cp ./walkthrough/04b-agent.baml baml_src/agent.baml
|
||||
|
||||
</details>
|
||||
|
||||
Run the tests
|
||||
|
||||
npx baml-cli test
|
||||
|
||||
Add more complex test cases
|
||||
|
||||
```diff
|
||||
baml_src/agent.baml
|
||||
"#
|
||||
}
|
||||
- @@assert(hello, {{this.intent == "done_for_now"}})
|
||||
+ @@assert(intent, {{this.intent == "done_for_now"}})
|
||||
}
|
||||
|
||||
"#
|
||||
}
|
||||
- @@assert(math_operation, {{this.intent == "multiply"}})
|
||||
+ @@assert(intent, {{this.intent == "multiply"}})
|
||||
}
|
||||
|
||||
+test LongMath {
|
||||
+ functions [DetermineNextStep]
|
||||
+ args {
|
||||
+ thread #"
|
||||
+ [
|
||||
+ {
|
||||
+ "type": "user_input",
|
||||
+ "data": "can you multiply 3 and 4, then divide the result by 2 and then add 12 to that result?"
|
||||
+ },
|
||||
+ {
|
||||
+ "type": "tool_call",
|
||||
+ "data": {
|
||||
+ "intent": "multiply",
|
||||
+ "a": 3,
|
||||
+ "b": 4
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "type": "tool_response",
|
||||
+ "data": 12
|
||||
+ },
|
||||
+ {
|
||||
+ "type": "tool_call",
|
||||
+ "data": {
|
||||
+ "intent": "divide",
|
||||
+ "a": 12,
|
||||
+ "b": 2
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "type": "tool_response",
|
||||
+ "data": 6
|
||||
+ },
|
||||
+ {
|
||||
+ "type": "tool_call",
|
||||
+ "data": {
|
||||
+ "intent": "add",
|
||||
+ "a": 6,
|
||||
+ "b": 12
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "type": "tool_response",
|
||||
+ "data": 18
|
||||
+ }
|
||||
+ ]
|
||||
+ "#
|
||||
+ }
|
||||
+ @@assert(intent, {{this.intent == "done_for_now"}})
|
||||
+ @@assert(answer, {{"18" in this.message}})
|
||||
+}
|
||||
+
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>skip this step</summary>
|
||||
|
||||
cp ./walkthrough/04c-agent.baml baml_src/agent.baml
|
||||
|
||||
</details>
|
||||
|
||||
Run the expanded test suite
|
||||
|
||||
npx baml-cli test
|
||||
|
||||
38
workshops/2025-05/sections/04-baml-tests/baml_src/agent.baml
Normal file
38
workshops/2025-05/sections/04-baml-tests/baml_src/agent.baml
Normal file
@@ -0,0 +1,38 @@
|
||||
class DoneForNow {
|
||||
intent "done_for_now"
|
||||
message string
|
||||
}
|
||||
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
) -> CalculatorTools | DoneForNow {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
prompt #"
|
||||
{{ _.role("system") }}
|
||||
|
||||
You are a helpful assistant that can help with tasks.
|
||||
|
||||
{{ _.role("user") }}
|
||||
|
||||
You are working on the following thread:
|
||||
|
||||
{{ thread }}
|
||||
|
||||
What should the next step be?
|
||||
|
||||
{{ ctx.output_format }}
|
||||
"#
|
||||
}
|
||||
|
||||
test HelloWorld {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "hello!"
|
||||
}
|
||||
"#
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Learn more about clients at https://docs.boundaryml.com/docs/snippets/clients/overview
|
||||
|
||||
client<llm> CustomGPT4o {
|
||||
provider openai
|
||||
options {
|
||||
model "gpt-4o"
|
||||
api_key env.OPENAI_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
client<llm> CustomGPT4oMini {
|
||||
provider openai
|
||||
retry_policy Exponential
|
||||
options {
|
||||
model "gpt-4o-mini"
|
||||
api_key env.OPENAI_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
client<llm> CustomSonnet {
|
||||
provider anthropic
|
||||
options {
|
||||
model "claude-3-5-sonnet-20241022"
|
||||
api_key env.ANTHROPIC_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
client<llm> CustomHaiku {
|
||||
provider anthropic
|
||||
retry_policy Constant
|
||||
options {
|
||||
model "claude-3-haiku-20240307"
|
||||
api_key env.ANTHROPIC_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/round-robin
|
||||
client<llm> CustomFast {
|
||||
provider round-robin
|
||||
options {
|
||||
// This will alternate between the two clients
|
||||
strategy [CustomGPT4oMini, CustomHaiku]
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/fallback
|
||||
client<llm> OpenaiFallback {
|
||||
provider fallback
|
||||
options {
|
||||
// This will try the clients in order until one succeeds
|
||||
strategy [CustomGPT4oMini, CustomGPT4oMini]
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/retry
|
||||
retry_policy Constant {
|
||||
max_retries 3
|
||||
// Strategy is optional
|
||||
strategy {
|
||||
type constant_delay
|
||||
delay_ms 200
|
||||
}
|
||||
}
|
||||
|
||||
retry_policy Exponential {
|
||||
max_retries 2
|
||||
// Strategy is optional
|
||||
strategy {
|
||||
type exponential_backoff
|
||||
delay_ms 300
|
||||
multiplier 1.5
|
||||
max_delay_ms 10000
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// This helps use auto generate libraries you can use in the language of
|
||||
// your choice. You can have multiple generators if you use multiple languages.
|
||||
// Just ensure that the output_dir is different for each generator.
|
||||
generator target {
|
||||
// Valid values: "python/pydantic", "typescript", "ruby/sorbet", "rest/openapi"
|
||||
output_type "typescript"
|
||||
|
||||
// Where the generated code will be saved (relative to baml_src/)
|
||||
output_dir "../"
|
||||
|
||||
// The version of the BAML package you have installed (e.g. same version as your baml-py or @boundaryml/baml).
|
||||
// The BAML VSCode extension version should also match this version.
|
||||
version "0.85.0"
|
||||
|
||||
// Valid values: "sync", "async"
|
||||
// This controls what `b.FunctionName()` will be (sync or async).
|
||||
default_client_mode async
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
type CalculatorTools = AddTool | SubtractTool | MultiplyTool | DivideTool
|
||||
|
||||
|
||||
class AddTool {
|
||||
intent "add"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class SubtractTool {
|
||||
intent "subtract"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class MultiplyTool {
|
||||
intent "multiply"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class DivideTool {
|
||||
intent "divide"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
2295
workshops/2025-05/sections/04-baml-tests/package-lock.json
generated
Normal file
2295
workshops/2025-05/sections/04-baml-tests/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
workshops/2025-05/sections/04-baml-tests/package.json
Normal file
20
workshops/2025-05/sections/04-baml-tests/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "my-agent",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "tsx src/index.ts",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"baml": "^0.0.0",
|
||||
"tsx": "^4.15.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"eslint": "^8.0.0"
|
||||
}
|
||||
}
|
||||
86
workshops/2025-05/sections/04-baml-tests/src/agent.ts
Normal file
86
workshops/2025-05/sections/04-baml-tests/src/agent.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
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() {
|
||||
// can change this to whatever custom serialization you want to do, XML, etc
|
||||
// e.g. https://github.com/got-agents/agents/blob/59ebbfa236fc376618f16ee08eb0f3bf7b698892/linear-assistant-ts/src/agent.ts#L66-L105
|
||||
return JSON.stringify(this.events);
|
||||
}
|
||||
}
|
||||
|
||||
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<string> {
|
||||
|
||||
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":
|
||||
// response to human, return the next step object
|
||||
return nextStep.message;
|
||||
case "add":
|
||||
case "subtract":
|
||||
case "multiply":
|
||||
case "divide":
|
||||
thread = await handleNextStep(nextStep, thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
23
workshops/2025-05/sections/04-baml-tests/src/cli.ts
Normal file
23
workshops/2025-05/sections/04-baml-tests/src/cli.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// cli.ts lets you invoke the agent loop from the command line
|
||||
|
||||
import { agentLoop, Thread, Event } from "./agent";
|
||||
|
||||
export async function cli() {
|
||||
// Get command line arguments, skipping the first two (node and script name)
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error("Error: Please provide a message as a command line argument");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Join all arguments into a single message
|
||||
const message = args.join(" ");
|
||||
|
||||
// Create a new thread with the user's message as the initial event
|
||||
const thread = new Thread([{ type: "user_input", data: message }]);
|
||||
|
||||
// Run the agent loop with the thread
|
||||
const result = await agentLoop(thread);
|
||||
console.log(result);
|
||||
}
|
||||
11
workshops/2025-05/sections/04-baml-tests/src/index.ts
Normal file
11
workshops/2025-05/sections/04-baml-tests/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { cli } from "./cli"
|
||||
|
||||
async function hello(): Promise<void> {
|
||||
console.log('hello, world!')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await cli()
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
24
workshops/2025-05/sections/04-baml-tests/tsconfig.json
Normal file
24
workshops/2025-05/sections/04-baml-tests/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", "walkthrough"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
class DoneForNow {
|
||||
intent "done_for_now"
|
||||
message string
|
||||
}
|
||||
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
) -> CalculatorTools | DoneForNow {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
prompt #"
|
||||
{{ _.role("system") }}
|
||||
|
||||
You are a helpful assistant that can help with tasks.
|
||||
|
||||
{{ _.role("user") }}
|
||||
|
||||
You are working on the following thread:
|
||||
|
||||
{{ thread }}
|
||||
|
||||
What should the next step be?
|
||||
|
||||
{{ ctx.output_format }}
|
||||
"#
|
||||
}
|
||||
|
||||
test HelloWorld {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "hello!"
|
||||
}
|
||||
"#
|
||||
}
|
||||
}
|
||||
|
||||
test MathOperation {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "can you multiply 3 and 4?"
|
||||
}
|
||||
"#
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
class DoneForNow {
|
||||
intent "done_for_now"
|
||||
message string
|
||||
}
|
||||
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
) -> CalculatorTools | DoneForNow {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
prompt #"
|
||||
{{ _.role("system") }}
|
||||
|
||||
You are a helpful assistant that can help with tasks.
|
||||
|
||||
{{ _.role("user") }}
|
||||
|
||||
You are working on the following thread:
|
||||
|
||||
{{ thread }}
|
||||
|
||||
What should the next step be?
|
||||
|
||||
{{ ctx.output_format }}
|
||||
"#
|
||||
}
|
||||
|
||||
test HelloWorld {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "hello!"
|
||||
}
|
||||
"#
|
||||
}
|
||||
@@assert(hello, {{this.intent == "done_for_now"}})
|
||||
}
|
||||
|
||||
test MathOperation {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "can you multiply 3 and 4?"
|
||||
}
|
||||
"#
|
||||
}
|
||||
@@assert(math_operation, {{this.intent == "multiply"}})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
class DoneForNow {
|
||||
intent "done_for_now"
|
||||
message string
|
||||
}
|
||||
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
) -> CalculatorTools | DoneForNow {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
prompt #"
|
||||
{{ _.role("system") }}
|
||||
|
||||
You are a helpful assistant that can help with tasks.
|
||||
|
||||
{{ _.role("user") }}
|
||||
|
||||
You are working on the following thread:
|
||||
|
||||
{{ thread }}
|
||||
|
||||
What should the next step be?
|
||||
|
||||
{{ ctx.output_format }}
|
||||
"#
|
||||
}
|
||||
|
||||
test HelloWorld {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "hello!"
|
||||
}
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "done_for_now"}})
|
||||
}
|
||||
|
||||
test MathOperation {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "can you multiply 3 and 4?"
|
||||
}
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "multiply"}})
|
||||
}
|
||||
|
||||
test LongMath {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
[
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "can you multiply 3 and 4, then divide the result by 2 and then add 12 to that result?"
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "multiply",
|
||||
"a": 3,
|
||||
"b": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 12
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "divide",
|
||||
"a": 12,
|
||||
"b": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 6
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "add",
|
||||
"a": 6,
|
||||
"b": 12
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 18
|
||||
}
|
||||
]
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "done_for_now"}})
|
||||
@@assert(answer, {{"18" in this.message}})
|
||||
}
|
||||
|
||||
2
workshops/2025-05/sections/05-human-tools/.gitignore
vendored
Normal file
2
workshops/2025-05/sections/05-human-tools/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
baml_client/
|
||||
node_modules/
|
||||
202
workshops/2025-05/sections/05-human-tools/README.md
Normal file
202
workshops/2025-05/sections/05-human-tools/README.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# Chapter 5 - Multiple Human Tools
|
||||
|
||||
Add support for requesting clarification from humans.
|
||||
|
||||
Update agent with clarification support
|
||||
|
||||
```diff
|
||||
baml_src/agent.baml
|
||||
+// human tools are async requests to a human
|
||||
+type HumanTools = ClarificationRequest | DoneForNow
|
||||
+
|
||||
+class ClarificationRequest {
|
||||
+ intent "request_more_information" @description("you can request more information from me")
|
||||
+ message string
|
||||
+}
|
||||
+
|
||||
class DoneForNow {
|
||||
intent "done_for_now"
|
||||
- message string
|
||||
+
|
||||
+ message string @description(#"
|
||||
+ message to send to the user about the work that was done.
|
||||
+ "#)
|
||||
}
|
||||
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
-) -> CalculatorTools | DoneForNow {
|
||||
+) -> HumanTools | CalculatorTools {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
}
|
||||
|
||||
+
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>skip this step</summary>
|
||||
|
||||
cp ./walkthrough/05-agent.baml baml_src/agent.baml
|
||||
|
||||
</details>
|
||||
|
||||
Generate updated client
|
||||
|
||||
npx baml-cli generate
|
||||
|
||||
Update agent implementation
|
||||
|
||||
```diff
|
||||
src/agent.ts
|
||||
}
|
||||
|
||||
-export async function agentLoop(thread: Thread): Promise<string> {
|
||||
+export async function agentLoop(thread: Thread): Promise<Thread> {
|
||||
|
||||
while (true) {
|
||||
switch (nextStep.intent) {
|
||||
case "done_for_now":
|
||||
- // response to human, return the next step object
|
||||
- return nextStep.message;
|
||||
+ case "request_more_information":
|
||||
+ // response to human, return the thread
|
||||
+ return thread;
|
||||
case "add":
|
||||
case "subtract":
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>skip this step</summary>
|
||||
|
||||
cp ./walkthrough/05-agent.ts src/agent.ts
|
||||
|
||||
</details>
|
||||
|
||||
Update CLI to handle clarification requests
|
||||
|
||||
```diff
|
||||
src/cli.ts
|
||||
// cli.ts lets you invoke the agent loop from the command line
|
||||
|
||||
-import { agentLoop, Thread, Event } from "./agent";
|
||||
+import { agentLoop, Thread, Event } from "../src/agent";
|
||||
|
||||
+
|
||||
+
|
||||
export async function cli() {
|
||||
// Get command line arguments, skipping the first two (node and script name)
|
||||
// Run the agent loop with the thread
|
||||
const result = await agentLoop(thread);
|
||||
- console.log(result);
|
||||
+ let lastEvent = result.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];
|
||||
+ }
|
||||
+
|
||||
+ // print the final result
|
||||
+ // optional - you could loop here too
|
||||
+ console.log(lastEvent.data.message);
|
||||
+ process.exit(0);
|
||||
}
|
||||
+
|
||||
+async function askHuman(message: string) {
|
||||
+ const readline = require('readline').createInterface({
|
||||
+ input: process.stdin,
|
||||
+ output: process.stdout
|
||||
+ });
|
||||
+
|
||||
+ return new Promise((resolve) => {
|
||||
+ readline.question(`${message}\n> `, (answer: string) => {
|
||||
+ resolve(answer);
|
||||
+ });
|
||||
+ });
|
||||
+}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>skip this step</summary>
|
||||
|
||||
cp ./walkthrough/05-cli.ts src/cli.ts
|
||||
|
||||
</details>
|
||||
|
||||
Test clarification flow
|
||||
|
||||
npx tsx src/index.ts 'can you multiply 3 and FD*(#F&& '
|
||||
|
||||
Add tests for clarification
|
||||
|
||||
```diff
|
||||
baml_src/agent.baml
|
||||
|
||||
|
||||
+
|
||||
+test MathOperationWithClarification {
|
||||
+ functions [DetermineNextStep]
|
||||
+ args {
|
||||
+ thread #"
|
||||
+ [{"type":"user_input","data":"can you multiply 3 and feee9ff10"}]
|
||||
+ "#
|
||||
+ }
|
||||
+ @@assert(intent, {{this.intent == "request_more_information"}})
|
||||
+}
|
||||
+
|
||||
+test MathOperationPostClarification {
|
||||
+ functions [DetermineNextStep]
|
||||
+ args {
|
||||
+ thread #"
|
||||
+ [
|
||||
+ {"type":"user_input","data":"can you multiply 3 and FD*(#F&& ?"},
|
||||
+ {"type":"tool_call","data":{"intent":"request_more_information","message":"It seems like there was a typo or mistake in your request. Could you please clarify or provide the correct numbers you would like to multiply?"}},
|
||||
+ {"type":"human_response","data":"lets try 12 instead"},
|
||||
+ ]
|
||||
+ "#
|
||||
+ }
|
||||
+ @@assert(intent, {{this.intent == "multiply"}})
|
||||
+ @@assert(a, {{this.b == 12}})
|
||||
+ @@assert(b, {{this.a == 3}})
|
||||
+}
|
||||
+
|
||||
+
|
||||
+
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>skip this step</summary>
|
||||
|
||||
cp ./walkthrough/05b-agent.baml baml_src/agent.baml
|
||||
|
||||
</details>
|
||||
|
||||
Run the tests
|
||||
|
||||
npx baml-cli test
|
||||
|
||||
Fix hello world test
|
||||
|
||||
```diff
|
||||
baml_src/agent.baml
|
||||
"#
|
||||
}
|
||||
- @@assert(intent, {{this.intent == "done_for_now"}})
|
||||
+ @@assert(intent, {{this.intent == "request_more_information"}})
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>skip this step</summary>
|
||||
|
||||
cp ./walkthrough/05c-agent.baml baml_src/agent.baml
|
||||
|
||||
</details>
|
||||
|
||||
Verify tests pass
|
||||
|
||||
npx baml-cli test
|
||||
|
||||
105
workshops/2025-05/sections/05-human-tools/baml_src/agent.baml
Normal file
105
workshops/2025-05/sections/05-human-tools/baml_src/agent.baml
Normal file
@@ -0,0 +1,105 @@
|
||||
class DoneForNow {
|
||||
intent "done_for_now"
|
||||
message string
|
||||
}
|
||||
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
) -> CalculatorTools | DoneForNow {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
prompt #"
|
||||
{{ _.role("system") }}
|
||||
|
||||
You are a helpful assistant that can help with tasks.
|
||||
|
||||
{{ _.role("user") }}
|
||||
|
||||
You are working on the following thread:
|
||||
|
||||
{{ thread }}
|
||||
|
||||
What should the next step be?
|
||||
|
||||
{{ ctx.output_format }}
|
||||
"#
|
||||
}
|
||||
|
||||
test HelloWorld {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "hello!"
|
||||
}
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "done_for_now"}})
|
||||
}
|
||||
|
||||
test MathOperation {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "can you multiply 3 and 4?"
|
||||
}
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "multiply"}})
|
||||
}
|
||||
|
||||
test LongMath {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
[
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "can you multiply 3 and 4, then divide the result by 2 and then add 12 to that result?"
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "multiply",
|
||||
"a": 3,
|
||||
"b": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 12
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "divide",
|
||||
"a": 12,
|
||||
"b": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 6
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "add",
|
||||
"a": 6,
|
||||
"b": 12
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 18
|
||||
}
|
||||
]
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "done_for_now"}})
|
||||
@@assert(answer, {{"18" in this.message}})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Learn more about clients at https://docs.boundaryml.com/docs/snippets/clients/overview
|
||||
|
||||
client<llm> CustomGPT4o {
|
||||
provider openai
|
||||
options {
|
||||
model "gpt-4o"
|
||||
api_key env.OPENAI_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
client<llm> CustomGPT4oMini {
|
||||
provider openai
|
||||
retry_policy Exponential
|
||||
options {
|
||||
model "gpt-4o-mini"
|
||||
api_key env.OPENAI_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
client<llm> CustomSonnet {
|
||||
provider anthropic
|
||||
options {
|
||||
model "claude-3-5-sonnet-20241022"
|
||||
api_key env.ANTHROPIC_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
client<llm> CustomHaiku {
|
||||
provider anthropic
|
||||
retry_policy Constant
|
||||
options {
|
||||
model "claude-3-haiku-20240307"
|
||||
api_key env.ANTHROPIC_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/round-robin
|
||||
client<llm> CustomFast {
|
||||
provider round-robin
|
||||
options {
|
||||
// This will alternate between the two clients
|
||||
strategy [CustomGPT4oMini, CustomHaiku]
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/fallback
|
||||
client<llm> OpenaiFallback {
|
||||
provider fallback
|
||||
options {
|
||||
// This will try the clients in order until one succeeds
|
||||
strategy [CustomGPT4oMini, CustomGPT4oMini]
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/retry
|
||||
retry_policy Constant {
|
||||
max_retries 3
|
||||
// Strategy is optional
|
||||
strategy {
|
||||
type constant_delay
|
||||
delay_ms 200
|
||||
}
|
||||
}
|
||||
|
||||
retry_policy Exponential {
|
||||
max_retries 2
|
||||
// Strategy is optional
|
||||
strategy {
|
||||
type exponential_backoff
|
||||
delay_ms 300
|
||||
multiplier 1.5
|
||||
max_delay_ms 10000
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// This helps use auto generate libraries you can use in the language of
|
||||
// your choice. You can have multiple generators if you use multiple languages.
|
||||
// Just ensure that the output_dir is different for each generator.
|
||||
generator target {
|
||||
// Valid values: "python/pydantic", "typescript", "ruby/sorbet", "rest/openapi"
|
||||
output_type "typescript"
|
||||
|
||||
// Where the generated code will be saved (relative to baml_src/)
|
||||
output_dir "../"
|
||||
|
||||
// The version of the BAML package you have installed (e.g. same version as your baml-py or @boundaryml/baml).
|
||||
// The BAML VSCode extension version should also match this version.
|
||||
version "0.85.0"
|
||||
|
||||
// Valid values: "sync", "async"
|
||||
// This controls what `b.FunctionName()` will be (sync or async).
|
||||
default_client_mode async
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
type CalculatorTools = AddTool | SubtractTool | MultiplyTool | DivideTool
|
||||
|
||||
|
||||
class AddTool {
|
||||
intent "add"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class SubtractTool {
|
||||
intent "subtract"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class MultiplyTool {
|
||||
intent "multiply"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class DivideTool {
|
||||
intent "divide"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
2295
workshops/2025-05/sections/05-human-tools/package-lock.json
generated
Normal file
2295
workshops/2025-05/sections/05-human-tools/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
workshops/2025-05/sections/05-human-tools/package.json
Normal file
20
workshops/2025-05/sections/05-human-tools/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "my-agent",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "tsx src/index.ts",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"baml": "^0.0.0",
|
||||
"tsx": "^4.15.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"eslint": "^8.0.0"
|
||||
}
|
||||
}
|
||||
86
workshops/2025-05/sections/05-human-tools/src/agent.ts
Normal file
86
workshops/2025-05/sections/05-human-tools/src/agent.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
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() {
|
||||
// can change this to whatever custom serialization you want to do, XML, etc
|
||||
// e.g. https://github.com/got-agents/agents/blob/59ebbfa236fc376618f16ee08eb0f3bf7b698892/linear-assistant-ts/src/agent.ts#L66-L105
|
||||
return JSON.stringify(this.events);
|
||||
}
|
||||
}
|
||||
|
||||
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<string> {
|
||||
|
||||
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":
|
||||
// response to human, return the next step object
|
||||
return nextStep.message;
|
||||
case "add":
|
||||
case "subtract":
|
||||
case "multiply":
|
||||
case "divide":
|
||||
thread = await handleNextStep(nextStep, thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
23
workshops/2025-05/sections/05-human-tools/src/cli.ts
Normal file
23
workshops/2025-05/sections/05-human-tools/src/cli.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// cli.ts lets you invoke the agent loop from the command line
|
||||
|
||||
import { agentLoop, Thread, Event } from "./agent";
|
||||
|
||||
export async function cli() {
|
||||
// Get command line arguments, skipping the first two (node and script name)
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error("Error: Please provide a message as a command line argument");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Join all arguments into a single message
|
||||
const message = args.join(" ");
|
||||
|
||||
// Create a new thread with the user's message as the initial event
|
||||
const thread = new Thread([{ type: "user_input", data: message }]);
|
||||
|
||||
// Run the agent loop with the thread
|
||||
const result = await agentLoop(thread);
|
||||
console.log(result);
|
||||
}
|
||||
11
workshops/2025-05/sections/05-human-tools/src/index.ts
Normal file
11
workshops/2025-05/sections/05-human-tools/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { cli } from "./cli"
|
||||
|
||||
async function hello(): Promise<void> {
|
||||
console.log('hello, world!')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await cli()
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
24
workshops/2025-05/sections/05-human-tools/tsconfig.json
Normal file
24
workshops/2025-05/sections/05-human-tools/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", "walkthrough"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// human tools are async requests to a human
|
||||
type HumanTools = ClarificationRequest | DoneForNow
|
||||
|
||||
class ClarificationRequest {
|
||||
intent "request_more_information" @description("you can request more information from me")
|
||||
message string
|
||||
}
|
||||
|
||||
class DoneForNow {
|
||||
intent "done_for_now"
|
||||
|
||||
message string @description(#"
|
||||
message to send to the user about the work that was done.
|
||||
"#)
|
||||
}
|
||||
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
) -> HumanTools | CalculatorTools {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
prompt #"
|
||||
{{ _.role("system") }}
|
||||
|
||||
You are a helpful assistant that can help with tasks.
|
||||
|
||||
{{ _.role("user") }}
|
||||
|
||||
You are working on the following thread:
|
||||
|
||||
{{ thread }}
|
||||
|
||||
What should the next step be?
|
||||
|
||||
{{ ctx.output_format }}
|
||||
"#
|
||||
}
|
||||
|
||||
test HelloWorld {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "hello!"
|
||||
}
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "done_for_now"}})
|
||||
}
|
||||
|
||||
test MathOperation {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "can you multiply 3 and 4?"
|
||||
}
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "multiply"}})
|
||||
}
|
||||
|
||||
test LongMath {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
[
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "can you multiply 3 and 4, then divide the result by 2 and then add 12 to that result?"
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "multiply",
|
||||
"a": 3,
|
||||
"b": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 12
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "divide",
|
||||
"a": 12,
|
||||
"b": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 6
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "add",
|
||||
"a": 6,
|
||||
"b": 12
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 18
|
||||
}
|
||||
]
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "done_for_now"}})
|
||||
@@assert(answer, {{"18" in this.message}})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
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() {
|
||||
// can change this to whatever custom serialization you want to do, XML, etc
|
||||
// e.g. https://github.com/got-agents/agents/blob/59ebbfa236fc376618f16ee08eb0f3bf7b698892/linear-assistant-ts/src/agent.ts#L66-L105
|
||||
return JSON.stringify(this.events);
|
||||
}
|
||||
}
|
||||
|
||||
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 thread
|
||||
return thread;
|
||||
case "add":
|
||||
case "subtract":
|
||||
case "multiply":
|
||||
case "divide":
|
||||
thread = await handleNextStep(nextStep, thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// cli.ts lets you invoke the agent loop from the command line
|
||||
|
||||
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);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error("Error: Please provide a message as a command line argument");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Join all arguments into a single message
|
||||
const message = args.join(" ");
|
||||
|
||||
// Create a new thread with the user's message as the initial event
|
||||
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];
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
// print the final result
|
||||
// optional - you could loop here too
|
||||
console.log(lastEvent.data.message);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function askHuman(message: string) {
|
||||
const readline = require('readline').createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
readline.question(`${message}\n> `, (answer: string) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// human tools are async requests to a human
|
||||
type HumanTools = ClarificationRequest | DoneForNow
|
||||
|
||||
class ClarificationRequest {
|
||||
intent "request_more_information" @description("you can request more information from me")
|
||||
message string
|
||||
}
|
||||
|
||||
class DoneForNow {
|
||||
intent "done_for_now"
|
||||
|
||||
message string @description(#"
|
||||
message to send to the user about the work that was done.
|
||||
"#)
|
||||
}
|
||||
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
) -> HumanTools | CalculatorTools {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
prompt #"
|
||||
{{ _.role("system") }}
|
||||
|
||||
You are a helpful assistant that can help with tasks.
|
||||
|
||||
{{ _.role("user") }}
|
||||
|
||||
You are working on the following thread:
|
||||
|
||||
{{ thread }}
|
||||
|
||||
What should the next step be?
|
||||
|
||||
{{ ctx.output_format }}
|
||||
"#
|
||||
}
|
||||
|
||||
test HelloWorld {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "hello!"
|
||||
}
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "done_for_now"}})
|
||||
}
|
||||
|
||||
test MathOperation {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "can you multiply 3 and 4?"
|
||||
}
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "multiply"}})
|
||||
}
|
||||
|
||||
test LongMath {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
[
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "can you multiply 3 and 4, then divide the result by 2 and then add 12 to that result?"
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "multiply",
|
||||
"a": 3,
|
||||
"b": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 12
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "divide",
|
||||
"a": 12,
|
||||
"b": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 6
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "add",
|
||||
"a": 6,
|
||||
"b": 12
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 18
|
||||
}
|
||||
]
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "done_for_now"}})
|
||||
@@assert(answer, {{"18" in this.message}})
|
||||
}
|
||||
|
||||
|
||||
|
||||
test MathOperationWithClarification {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
[{"type":"user_input","data":"can you multiply 3 and feee9ff10"}]
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "request_more_information"}})
|
||||
}
|
||||
|
||||
test MathOperationPostClarification {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
[
|
||||
{"type":"user_input","data":"can you multiply 3 and FD*(#F&& ?"},
|
||||
{"type":"tool_call","data":{"intent":"request_more_information","message":"It seems like there was a typo or mistake in your request. Could you please clarify or provide the correct numbers you would like to multiply?"}},
|
||||
{"type":"human_response","data":"lets try 12 instead"},
|
||||
]
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "multiply"}})
|
||||
@@assert(a, {{this.b == 12}})
|
||||
@@assert(b, {{this.a == 3}})
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// human tools are async requests to a human
|
||||
type HumanTools = ClarificationRequest | DoneForNow
|
||||
|
||||
class ClarificationRequest {
|
||||
intent "request_more_information" @description("you can request more information from me")
|
||||
message string
|
||||
}
|
||||
|
||||
class DoneForNow {
|
||||
intent "done_for_now"
|
||||
|
||||
message string @description(#"
|
||||
message to send to the user about the work that was done.
|
||||
"#)
|
||||
}
|
||||
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
) -> HumanTools | CalculatorTools {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
prompt #"
|
||||
{{ _.role("system") }}
|
||||
|
||||
You are a helpful assistant that can help with tasks.
|
||||
|
||||
{{ _.role("user") }}
|
||||
|
||||
You are working on the following thread:
|
||||
|
||||
{{ thread }}
|
||||
|
||||
What should the next step be?
|
||||
|
||||
{{ ctx.output_format }}
|
||||
"#
|
||||
}
|
||||
|
||||
test HelloWorld {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "hello!"
|
||||
}
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "request_more_information"}})
|
||||
}
|
||||
|
||||
test MathOperation {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "can you multiply 3 and 4?"
|
||||
}
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "multiply"}})
|
||||
}
|
||||
|
||||
test LongMath {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
[
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "can you multiply 3 and 4, then divide the result by 2 and then add 12 to that result?"
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "multiply",
|
||||
"a": 3,
|
||||
"b": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 12
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "divide",
|
||||
"a": 12,
|
||||
"b": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 6
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "add",
|
||||
"a": 6,
|
||||
"b": 12
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 18
|
||||
}
|
||||
]
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "done_for_now"}})
|
||||
@@assert(answer, {{"18" in this.message}})
|
||||
}
|
||||
|
||||
|
||||
|
||||
test MathOperationWithClarification {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
[{"type":"user_input","data":"can you multiply 3 and feee9ff10"}]
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "request_more_information"}})
|
||||
}
|
||||
|
||||
test MathOperationPostClarification {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
[
|
||||
{"type":"user_input","data":"can you multiply 3 and FD*(#F&& ?"},
|
||||
{"type":"tool_call","data":{"intent":"request_more_information","message":"It seems like there was a typo or mistake in your request. Could you please clarify or provide the correct numbers you would like to multiply?"}},
|
||||
{"type":"human_response","data":"lets try 12 instead"},
|
||||
]
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "multiply"}})
|
||||
@@assert(a, {{this.b == 12}})
|
||||
@@assert(b, {{this.a == 3}})
|
||||
}
|
||||
|
||||
|
||||
|
||||
2
workshops/2025-05/sections/06-customize-prompt/.gitignore
vendored
Normal file
2
workshops/2025-05/sections/06-customize-prompt/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
baml_client/
|
||||
node_modules/
|
||||
37
workshops/2025-05/sections/06-customize-prompt/README.md
Normal file
37
workshops/2025-05/sections/06-customize-prompt/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Chapter 6 - Customize Your Prompt with Reasoning
|
||||
|
||||
Improve the agent's prompting by adding reasoning steps.
|
||||
|
||||
Update agent with reasoning steps
|
||||
|
||||
```diff
|
||||
baml_src/agent.baml
|
||||
|
||||
{{ ctx.output_format }}
|
||||
+
|
||||
+ Always think about what to do next first, like:
|
||||
+
|
||||
+ - ...
|
||||
+ - ...
|
||||
+ - ...
|
||||
+
|
||||
+ {...} // schema
|
||||
"#
|
||||
}
|
||||
@@assert(b, {{this.a == 3}})
|
||||
}
|
||||
-
|
||||
-
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>skip this step</summary>
|
||||
|
||||
cp ./walkthrough/06-agent.baml baml_src/agent.baml
|
||||
|
||||
</details>
|
||||
|
||||
Generate updated client
|
||||
|
||||
npx baml-cli generate
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// human tools are async requests to a human
|
||||
type HumanTools = ClarificationRequest | DoneForNow
|
||||
|
||||
class ClarificationRequest {
|
||||
intent "request_more_information" @description("you can request more information from me")
|
||||
message string
|
||||
}
|
||||
|
||||
class DoneForNow {
|
||||
intent "done_for_now"
|
||||
|
||||
message string @description(#"
|
||||
message to send to the user about the work that was done.
|
||||
"#)
|
||||
}
|
||||
|
||||
function DetermineNextStep(
|
||||
thread: string
|
||||
) -> HumanTools | CalculatorTools {
|
||||
client "openai/gpt-4o"
|
||||
|
||||
prompt #"
|
||||
{{ _.role("system") }}
|
||||
|
||||
You are a helpful assistant that can help with tasks.
|
||||
|
||||
{{ _.role("user") }}
|
||||
|
||||
You are working on the following thread:
|
||||
|
||||
{{ thread }}
|
||||
|
||||
What should the next step be?
|
||||
|
||||
{{ ctx.output_format }}
|
||||
"#
|
||||
}
|
||||
|
||||
test HelloWorld {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "hello!"
|
||||
}
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "request_more_information"}})
|
||||
}
|
||||
|
||||
test MathOperation {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "can you multiply 3 and 4?"
|
||||
}
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "multiply"}})
|
||||
}
|
||||
|
||||
test LongMath {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
[
|
||||
{
|
||||
"type": "user_input",
|
||||
"data": "can you multiply 3 and 4, then divide the result by 2 and then add 12 to that result?"
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "multiply",
|
||||
"a": 3,
|
||||
"b": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 12
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "divide",
|
||||
"a": 12,
|
||||
"b": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 6
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
"intent": "add",
|
||||
"a": 6,
|
||||
"b": 12
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool_response",
|
||||
"data": 18
|
||||
}
|
||||
]
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "done_for_now"}})
|
||||
@@assert(answer, {{"18" in this.message}})
|
||||
}
|
||||
|
||||
|
||||
|
||||
test MathOperationWithClarification {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
[{"type":"user_input","data":"can you multiply 3 and feee9ff10"}]
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "request_more_information"}})
|
||||
}
|
||||
|
||||
test MathOperationPostClarification {
|
||||
functions [DetermineNextStep]
|
||||
args {
|
||||
thread #"
|
||||
[
|
||||
{"type":"user_input","data":"can you multiply 3 and FD*(#F&& ?"},
|
||||
{"type":"tool_call","data":{"intent":"request_more_information","message":"It seems like there was a typo or mistake in your request. Could you please clarify or provide the correct numbers you would like to multiply?"}},
|
||||
{"type":"human_response","data":"lets try 12 instead"},
|
||||
]
|
||||
"#
|
||||
}
|
||||
@@assert(intent, {{this.intent == "multiply"}})
|
||||
@@assert(a, {{this.b == 12}})
|
||||
@@assert(b, {{this.a == 3}})
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Learn more about clients at https://docs.boundaryml.com/docs/snippets/clients/overview
|
||||
|
||||
client<llm> CustomGPT4o {
|
||||
provider openai
|
||||
options {
|
||||
model "gpt-4o"
|
||||
api_key env.OPENAI_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
client<llm> CustomGPT4oMini {
|
||||
provider openai
|
||||
retry_policy Exponential
|
||||
options {
|
||||
model "gpt-4o-mini"
|
||||
api_key env.OPENAI_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
client<llm> CustomSonnet {
|
||||
provider anthropic
|
||||
options {
|
||||
model "claude-3-5-sonnet-20241022"
|
||||
api_key env.ANTHROPIC_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
client<llm> CustomHaiku {
|
||||
provider anthropic
|
||||
retry_policy Constant
|
||||
options {
|
||||
model "claude-3-haiku-20240307"
|
||||
api_key env.ANTHROPIC_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/round-robin
|
||||
client<llm> CustomFast {
|
||||
provider round-robin
|
||||
options {
|
||||
// This will alternate between the two clients
|
||||
strategy [CustomGPT4oMini, CustomHaiku]
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/fallback
|
||||
client<llm> OpenaiFallback {
|
||||
provider fallback
|
||||
options {
|
||||
// This will try the clients in order until one succeeds
|
||||
strategy [CustomGPT4oMini, CustomGPT4oMini]
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.boundaryml.com/docs/snippets/clients/retry
|
||||
retry_policy Constant {
|
||||
max_retries 3
|
||||
// Strategy is optional
|
||||
strategy {
|
||||
type constant_delay
|
||||
delay_ms 200
|
||||
}
|
||||
}
|
||||
|
||||
retry_policy Exponential {
|
||||
max_retries 2
|
||||
// Strategy is optional
|
||||
strategy {
|
||||
type exponential_backoff
|
||||
delay_ms 300
|
||||
multiplier 1.5
|
||||
max_delay_ms 10000
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// This helps use auto generate libraries you can use in the language of
|
||||
// your choice. You can have multiple generators if you use multiple languages.
|
||||
// Just ensure that the output_dir is different for each generator.
|
||||
generator target {
|
||||
// Valid values: "python/pydantic", "typescript", "ruby/sorbet", "rest/openapi"
|
||||
output_type "typescript"
|
||||
|
||||
// Where the generated code will be saved (relative to baml_src/)
|
||||
output_dir "../"
|
||||
|
||||
// The version of the BAML package you have installed (e.g. same version as your baml-py or @boundaryml/baml).
|
||||
// The BAML VSCode extension version should also match this version.
|
||||
version "0.85.0"
|
||||
|
||||
// Valid values: "sync", "async"
|
||||
// This controls what `b.FunctionName()` will be (sync or async).
|
||||
default_client_mode async
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
type CalculatorTools = AddTool | SubtractTool | MultiplyTool | DivideTool
|
||||
|
||||
|
||||
class AddTool {
|
||||
intent "add"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class SubtractTool {
|
||||
intent "subtract"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class MultiplyTool {
|
||||
intent "multiply"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
class DivideTool {
|
||||
intent "divide"
|
||||
a int | float
|
||||
b int | float
|
||||
}
|
||||
|
||||
2295
workshops/2025-05/sections/06-customize-prompt/package-lock.json
generated
Normal file
2295
workshops/2025-05/sections/06-customize-prompt/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
workshops/2025-05/sections/06-customize-prompt/package.json
Normal file
20
workshops/2025-05/sections/06-customize-prompt/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "my-agent",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "tsx src/index.ts",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"baml": "^0.0.0",
|
||||
"tsx": "^4.15.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"eslint": "^8.0.0"
|
||||
}
|
||||
}
|
||||
87
workshops/2025-05/sections/06-customize-prompt/src/agent.ts
Normal file
87
workshops/2025-05/sections/06-customize-prompt/src/agent.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
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() {
|
||||
// can change this to whatever custom serialization you want to do, XML, etc
|
||||
// e.g. https://github.com/got-agents/agents/blob/59ebbfa236fc376618f16ee08eb0f3bf7b698892/linear-assistant-ts/src/agent.ts#L66-L105
|
||||
return JSON.stringify(this.events);
|
||||
}
|
||||
}
|
||||
|
||||
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 thread
|
||||
return thread;
|
||||
case "add":
|
||||
case "subtract":
|
||||
case "multiply":
|
||||
case "divide":
|
||||
thread = await handleNextStep(nextStep, thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
50
workshops/2025-05/sections/06-customize-prompt/src/cli.ts
Normal file
50
workshops/2025-05/sections/06-customize-prompt/src/cli.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// cli.ts lets you invoke the agent loop from the command line
|
||||
|
||||
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);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error("Error: Please provide a message as a command line argument");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Join all arguments into a single message
|
||||
const message = args.join(" ");
|
||||
|
||||
// Create a new thread with the user's message as the initial event
|
||||
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];
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
// print the final result
|
||||
// optional - you could loop here too
|
||||
console.log(lastEvent.data.message);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function askHuman(message: string) {
|
||||
const readline = require('readline').createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
readline.question(`${message}\n> `, (answer: string) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user