Skip to content

Commit

Permalink
Stepless model/network/agent instantiations (#24)
Browse files Browse the repository at this point in the history
* Upgrade to `inngest@pr-756`

* Strip models, use `inngest` `AiAdapter` instead

* Update demo

* Exploration

* Add `createRoutingAgent`, refactor state, messages, and routing

* Don't set routing model, and always inject into run if unset

* Allow tool choice of "auto", "any", or forcing of particular tools

* WIP: SWEBench

* Fix `step` type errors

* Make stateful `.withModel()` return a new `Agent`

* nit in readfile tool

* Updates to swebench to add code editing agents

* Pull out to make stateless

* Merge

* Use `@inngest/agent-kit` for imports

* Separate network instantiation

* Make `Network` entirely stateless

* Create ten-apes-argue.md

---------

Co-authored-by: Tony Holdstock-Brown <[email protected]>
  • Loading branch information
jpwilliams and tonyhb authored Dec 12, 2024
1 parent a1fa17b commit f7158e4
Show file tree
Hide file tree
Showing 30 changed files with 688 additions and 651 deletions.
5 changes: 5 additions & 0 deletions .changeset/ten-apes-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inngest/agent-kit": minor
---

Stepless model/network/agent instantiations
68 changes: 31 additions & 37 deletions demo/inngest.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
anthropic,
createAgent,
createNetwork,
createTool,
defaultRoutingAgent,
openai,
} from "../src/index";
import { EventSchemas, Inngest } from "inngest";
} from "@inngest/agent-kit";
import { EventSchemas, Inngest, openai } from "inngest";
import { z } from "zod";

export const inngest = new Inngest({
Expand All @@ -22,44 +20,26 @@ export const inngest = new Inngest({
});

export const fn = inngest.createFunction(
{ id: "agent", retries: 0, },
{ id: "agent", retries: 0 },
{ event: "agent/run" },
async ({ event, step }) => {
const model = openai({ model: "gpt-4", step });

async ({ event }) => {
// 1. Single agent

// Run a single agent as a prompt without a network.
// await codeWritingAgent.run(event.data.input, {
// model,
// });

// 2. A network of agents that works together
const network = createNetwork({
agents: [
codeWritingAgent.withModel(model),
executingAgent.withModel(model),
],
defaultModel: model,
maxIter: 4,
});

// This uses the defaut agentic router to determine which agent to handle first. You can
// optionally specifiy the agent that should execute first, and provide your own logic for
// handling logic in between agent calls.
const result = await network.run(event.data.input, ({ network }) => {
if (network.state.kv.has("files")) {
// Okay, we have some files. Did an agent run tests?
return executingAgent;
}

return defaultRoutingAgent.withModel(model);
});

return result;
},
// A network of agents that works together.
//
// This uses the defaut agentic router to determine which agent to handle
// first. You can optionally specifiy the agent that should execute first,
// and provide your own logic for handling logic in between agent calls.
return network.run(event.data.input);
}
);

const model = openai({ model: "gpt-4" });

const systemPrompt =
"You are an expert TypeScript programmer. You can create files with idiomatic TypeScript code, with comments and associated tests.";

Expand All @@ -68,14 +48,14 @@ const codeWritingAgent = createAgent({
// description helps LLM routers choose the right agents to run.
description: "An expert TypeScript programmer which can write and debug code",
// system defines a system prompt generated each time the agent is called by a network.
system: (network) => {
if (!network) {
system: ({ network }) => {
if (!network?.state) {
return systemPrompt;
}

// Each time this agent runs, it may produce "file" content. Check if any
// content has already been produced in an agentic workflow.
const files = network.state.kv.get<Record<string, string>>("files");
const files = network?.state.kv.get<Record<string, string>>("files");

if (files === undefined) {
// Use the default system prompt.
Expand Down Expand Up @@ -109,7 +89,7 @@ const codeWritingAgent = createAgent({
filename: z.string(),
content: z.string(),
})
.required(),
.required()
),
})
.required(),
Expand Down Expand Up @@ -157,3 +137,17 @@ Think carefully about the request that the user is asking for. Do not respond wi
</command>
`,
});

const network = createNetwork({
agents: [codeWritingAgent.withModel(model), executingAgent.withModel(model)],
defaultModel: model,
maxIter: 4,
defaultRouter: ({ network }) => {
if (network.state.kv.has("files")) {
// Okay, we have some files. Did an agent run tests?
return executingAgent;
}

return defaultRoutingAgent.withModel(model);
},
});
10 changes: 6 additions & 4 deletions demo/mw.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { agenticOpenai, createAgent, createNetwork } from "@inngest/agent-kit";
import { InngestMiddleware, type OpenAi } from "inngest";
import { createAgent, createNetwork } from "@inngest/agent-kit";
import { InngestMiddleware, openai, type OpenAi } from "inngest";

export const codeWritingNetworkMiddleware = (
defaultModelOptions: OpenAi.AiModelOptions,
) => {
return new InngestMiddleware({
name: "Code Writing Agent Middleware",
init() {
const model = openai({ ...defaultModelOptions });

return {
onFunctionRun() {
return {
transformInput({ ctx: { step } }) {
transformInput() {
const codeWritingNetwork = createNetwork({
agents: [codeWritingAgent, executingAgent],
maxIter: 4,
defaultModel: agenticOpenai({ ...defaultModelOptions, step }),
defaultModel: model,
});

return {
Expand Down
4 changes: 2 additions & 2 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
"license": "ISC",
"description": "",
"dependencies": {
"@inngest/agent-kit": "~0.0.1",
"@inngest/agent-kit": "file:../inngest-agent-kit-0.0.3.tgz",
"express": "^4.21.1",
"inngest": "^3.27.3"
"inngest": "^3.27.6-pr-776.2"
},
"devDependencies": {
"@types/express": "^5.0.0",
Expand Down
17 changes: 9 additions & 8 deletions examples/swebench/agents/editor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createAgent, createTool } from "../../../src";
import { createAgent, createTool } from "@inngest/agent-kit";
import {
extractClassAndFnsTool,
readFileTool,
Expand All @@ -12,7 +12,8 @@ import {
*/
export const editingAgent = createAgent({
name: "Editor",
description: "Edits code by replacing contents in files, or creating new files with new code.",
description:
"Edits code by replacing contents in files, or creating new files with new code.",
tools: [
extractClassAndFnsTool,
replaceClassMethodTool,
Expand All @@ -38,16 +39,16 @@ export const editingAgent = createAgent({
// things from the planning agent. We update the system prompt to include details from the
// plan via network state.
onStart: ({ agent, prompt, network }) => {
const history = (network?.state.results || []).
filter(i => i.agent === agent). // Return the current history from this agent only.
map(i => i.output.concat(i.toolCalls)). // Only add the output and tool calls to the conversation history
flat();
const history = (network?.state.results || [])
.filter((i) => i.agent === agent) // Return the current history from this agent only.
.map((i) => i.output.concat(i.toolCalls)) // Only add the output and tool calls to the conversation history
.flat();

return { prompt, history, stop: false };
},
},

system: (network) => `
system: ({ network }) => `
You are an expert Python programmer working on a specific project: ${network?.state.kv.get("repo")}. You have been
given a plan to fix the given issue supplied by the user.
Expand All @@ -63,4 +64,4 @@ export const editingAgent = createAgent({
Once the files have been edited and you are confident in the updated code, you MUST finish your editing via calling the "done" tool.
`,
})
});
22 changes: 12 additions & 10 deletions examples/swebench/agents/planner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createAgent, createTool } from "../../../src";
import { createAgent, createTool } from "@inngest/agent-kit";
import { z } from "zod";
import {
extractClassAndFnsTool,
Expand All @@ -16,15 +16,18 @@ export const planningAgent = createAgent({
extractClassAndFnsTool,
createTool({
name: "create_plan",
description: "Describe a formal plan for how to fix the issue, including which files to edit and reasoning.",
description:
"Describe a formal plan for how to fix the issue, including which files to edit and reasoning.",
parameters: z.object({
thoughts: z.string(),
plan_details: z.string(),
edits: z.array(z.object({
filename: z.string(),
idea: z.string(),
reasoning: z.string(),
}))
edits: z.array(
z.object({
filename: z.string(),
idea: z.string(),
reasoning: z.string(),
})
),
}),

handler: async (plan, opts) => {
Expand All @@ -35,7 +38,7 @@ export const planningAgent = createAgent({
}),
],

system: (network) => `
system: ({ network }) => `
You are an expert Python programmer working on a specific project: ${network?.state.kv.get("repo")}.
You are given an issue reported within the project. You are planning how to fix the issue by investigating the report,
Expand All @@ -48,5 +51,4 @@ export const planningAgent = createAgent({
- Read entire files
- Find specific classes and functions within a file
`,
})

});
6 changes: 6 additions & 0 deletions examples/swebench/agents/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createAgent } from "@inngest/agent-kit";

createAgent({
name: "setup",
system: "This is a system prompt",
});
4 changes: 2 additions & 2 deletions examples/swebench/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const app = express();
const port = 3001;

// Important: ensure you add JSON middleware to process incoming JSON POST payloads.
app.use(express.json({limit: '50mb'}));
app.use(express.json({ limit: "50mb" }));

app.use(
// Expose the middleware on our recommended path at `/api/inngest`.
Expand All @@ -15,7 +15,7 @@ app.use(
serve({
client: inngest,
functions: [fn],
}),
})
);

app.listen(port, () => {
Expand Down
69 changes: 15 additions & 54 deletions examples/swebench/inngest.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { execSync } from "child_process";
import fs from "fs";
import { execSync } from 'child_process';
import { EventSchemas, Inngest } from "inngest";
import { z } from "zod";
import {
createNetwork,
anthropic,
State,
} from "../../src/index";
import { Inngest, EventSchemas } from "inngest";
import { planningAgent } from "./agents/planner";
import { editingAgent } from "./agents/editor";
import { codeWritingNetwork } from "./networks/codeWritingNetwork";

export const inngest = new Inngest({
id: "agents",
Expand All @@ -20,16 +13,15 @@ export const inngest = new Inngest({
base_commit: z.string(),
environment_setup_commit: z.string(),
problem_statement: z.string(),
})
}),
},
}),
});

export const fn = inngest.createFunction(
{ id: "agent", retries: 2, },
{ id: "agent", retries: 2 },
{ event: "swebench/run" },
async ({ event, step }) => {

// This is some basic stuff to initialize and set up the repos
// for the swebench test.
//
Expand All @@ -38,57 +30,26 @@ export const fn = inngest.createFunction(
await step.run("clone repo", async () => {
// Check if the dir already exists.
if (fs.existsSync(dir)) {
return
return;
}
console.log("creating repo");
fs.mkdirSync(dir, { recursive: true });
execSync(`cd ${dir} && git init`);
execSync(`cd ${dir} && git remote add origin [email protected]:${event.data.repo}.git`);
execSync(
`cd ${dir} && git remote add origin [email protected]:${event.data.repo}.git`
);
});

await step.run("check out commit", async () => {
console.log("checking out commit");
execSync(`cd ${dir} && git fetch origin ${event.data.base_commit} --depth=1`);
execSync(
`cd ${dir} && git fetch origin ${event.data.base_commit} --depth=1`
);
execSync(`cd ${dir} && git reset --hard FETCH_HEAD`);
});

// Use Claude as the base model of the network.
const model = anthropic({
model: "claude-3-5-haiku-latest",
max_tokens: 1000,
step: step as any,
});

// Create new network state, and set the repo we're editing directly from the event
// input.
const state = new State();
state.kv.set("repo", event.data.repo);

const network = createNetwork({
agents: [planningAgent, editingAgent],
defaultModel: model,
state,
});
await network.run(event.data.problem_statement, (opts) => {
if (opts.network.state.kv.get("done")) {
// We're done editing. This is set when the editing agent finishes
// implementing the plan.
//
// At this point, we should hand off to another agent that tests, critiques,
// and validates the edits.
return;
}

// If there's a plan, we should switch to the editing agent to begin implementing.
//
// This lets us separate the concerns of planning vs editing, including using differing
// prompts and tools at various stages of the editing process.
if (opts.network.state.kv.get("plan") !== undefined) {
return editingAgent;
}

// By default, use the planning agent.
return planningAgent;
await codeWritingNetwork.run(event.data.problem_statement, {
state: { repo: event.data.repo },
});
},
}
);
Loading

0 comments on commit f7158e4

Please sign in to comment.