Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: added runnable examples to agent docs #3000

Merged
merged 16 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions docs/docs/modules/agents/how_to/custom_llm_agent.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import CodeBlock from "@theme/CodeBlock";
import Example from "@examples/agents/custom_llm_agent.ts";
import RunnableExample from "@examples/agents/custom_llm_agent_runnable.ts";

# Custom LLM Agent

This notebook goes through how to create your own custom LLM agent.
Expand All @@ -20,7 +24,10 @@ The LLMAgent is used in an AgentExecutor. This AgentExecutor can largely be thou

`AgentFinish` is a response that contains the final message to be sent back to the user. This should be used to end an agent run.

import CodeBlock from "@theme/CodeBlock";
import Example from "@examples/agents/custom_llm_agent.ts";
# With LCEL

<CodeBlock language="typescript">{RunnableExample}</CodeBlock>

# With `LLMChain`

<CodeBlock language="typescript">{Example}</CodeBlock>
13 changes: 10 additions & 3 deletions docs/docs/modules/agents/how_to/custom_llm_chat_agent.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import CodeBlock from "@theme/CodeBlock";
import ChatModelExample from "@examples/agents/custom_llm_agent_chat.ts";
import RunnableExample from "@examples/agents/custom_llm_agent_chat_runnable.ts";

# Custom LLM Agent (with a ChatModel)

This notebook goes through how to create your own custom agent based on a chat model.
Expand All @@ -20,7 +24,10 @@ The LLMAgent is used in an AgentExecutor. This AgentExecutor can largely be thou

`AgentFinish` is a response that contains the final message to be sent back to the user. This should be used to end an agent run.

import CodeBlock from "@theme/CodeBlock";
import Example from "@examples/agents/custom_llm_agent_chat.ts";
# With LCEL

<CodeBlock language="typescript">{RunnableExample}</CodeBlock>

# With `LLMChain`

<CodeBlock language="typescript">{Example}</CodeBlock>
<CodeBlock language="typescript">{ChatModelExample}</CodeBlock>
238 changes: 238 additions & 0 deletions docs/docs/modules/agents/how_to/custom_mrkl_agent.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
# Custom MRKL agent

This notebook goes through how to create your own custom Modular Reasoning, Knowledge and Language (MRKL, pronounced “miracle”) agent using LCEL.

A MRKL agent consists of three parts:

- Tools: The tools the agent has available to use.
- `Runnable`: The `Runnable` that produces the text that is parsed in a certain way to determine which action to take.
- The agent class itself: this parses the output of the LLMChain to determine which action to take.

In this notebook we walk through how to create a custom MRKL agent by creating a custom `Runnable`.

## Custom `Runnable`

The first way to create a custom agent is to use a custom `Runnable`.

Most of the work in creating the custom `Runnable` comes down to the input and outputs. Because we're using a custom `Runnable` we are not provided with any pre-built input/output parsers.
Instead, we must create our own to format the inputs and outputs a the way we define.

Additionally, we need an `agent_scratchpad` input variable to put notes on previous actions and observations.
This should almost always be the final part of the prompt. This is a very important step, because without the `agent_scratchpad` the agent will have no context on the previous actions it has taken.

To ensure the prompt we create contains the appropriate instructions and input variables, we'll create a helper function which takes in a list of input variables, and returns the final formatted prompt.
We will also do something similar with the output parser, ensuring our input prompts and outputs are always formatted the same way.

The first step is to import all the necessary modules.

```typescript
import { AgentExecutor } from "langchain/agents";
import { ChatOpenAI } from "langchain/chat_models/openai";
import { PromptTemplate } from "langchain/prompts";
import {
AgentAction,
AgentFinish,
AgentStep,
BaseMessage,
InputValues,
SystemMessage,
} from "langchain/schema";
import { RunnableSequence } from "langchain/schema/runnable";
import { SerpAPI } from "langchain/tools";
import { Calculator } from "langchain/tools/calculator";
```

Next, we'll instantiate our chat model and tools.

```typescript
/**
* Instantiate the chat model and bind the stop token
* @important The stop token must be set, if not the LLM will happily continue generating text forever.
*/
const model = new ChatOpenAI({ temperature: 0 }).bind({
stop: ["\nObservation"],
});
/** Define the tools */
const tools = [
new SerpAPI(process.env.SERPAPI_API_KEY, {
location: "Austin,Texas,United States",
hl: "en",
gl: "us",
}),
new Calculator(),
];
```

After this, we can define our prompts using the `PromptTemplate` class. This will come in handy later when formatting our prompts as it provides a helper `.format()` method we'll use.

```typescript
const PREFIX = new PromptTemplate({
template: `Answer the following questions as best you can, but speaking as a pirate might speak. You have access to the following tools:
{tools}`,
inputVariables: ["tools"],
});
/**
* @important This prompt is used as an example for the LLM on how it should
* respond to the user.
*/
const TOOL_INSTRUCTIONS = new PromptTemplate({
template: `Use the following format in your response:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question`,
inputVariables: ["tool_names"],
});
const SUFFIX = new PromptTemplate({
template: `Begin! Remember to speak as a pirate when giving your final answer. Use lots of "Args

Question: {input}
{agent_scratchpad}`,
inputVariables: ["input", "agent_scratchpad"],
});
```

Once we've defined our prompts we can create our custom input prompt formatter.
This function will take in an arbatary list of inputs and return a formatted prompt.
Inside we're first checking `input` and `agent_scratchpad` exist on the input.
Then, we're formatting the `agent_scratchpad` as a string in a way the LLM can easily interpret.
Finally, we call the `.format()` methods on all our prompts, passing in the input variables. With these, we can then return the final prompt inside a `SystemMessage`.

```typescript
async function formatPrompt(inputValues: InputValues) {
/** Verify input and agent_scratchpad exist in the input object. */
if (!("input" in inputValues) || !("agent_scratchpad" in inputValues)) {
throw new Error(
`Missing input or agent_scratchpad in input object: ${JSON.stringify(
inputValues
)}`
);
}
const input = inputValues.input as string;
/** agent_scratchpad will be undefined on the first iteration. */
const agentScratchpad = (inputValues.agent_scratchpad ?? []) as AgentStep[];
/** Convert the list of AgentStep's into a more agent friendly string format. */
const formattedScratchpad = agentScratchpad.reduce(
(thoughts, { action, observation }) =>
thoughts +
[action.log, `\nObservation: ${observation}`, "Thought:"].join("\n"),
""
);
/** Format our prompts by passing in the input variables */
const formattedPrefix = await PREFIX.format({
tools: tools
.map((tool) => `Name: ${tool.name}\nDescription: ${tool.description}`)
.join("\n"),
});
const formattedToolInstructions = await TOOL_INSTRUCTIONS.format({
tool_names: tools.map((tool) => tool.name).join(", "),
});
const formattedSuffix = await SUFFIX.format({
input,
agent_scratchpad: formattedScratchpad,
});
/** Join all the prompts together, and return as an instance of `SystemMessage` */
const formatted = [
formattedPrefix,
formattedToolInstructions,
formattedSuffix,
].join("\n");
return [new SystemMessage(formatted)];
}
```

If we're curious as to what the final prompt looks like, we can console log it before returning. It should look like this:

```typescript
console.log(new SystemMessage(formatted));
```

```txt
Answer the following questions as best you can, but speaking as a pirate might speak. You have access to the following tools:
Name: search
Description: a search engine. useful for when you need to answer questions about current events. input should be a search query.
Name: calculator
Description: Useful for getting the result of a math expression. The input to this tool should be a valid mathematical expression that could be executed by a simple calculator.
Use the following format in your response:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [search, calculator]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question
Begin! Remember to speak as a pirate when giving your final answer. Use lots of "Args

Question: How many people live in canada as of 2023?
```

If you want to dive deeper and see the exact steps, inputs, outputs and more you can use (LangSmith)[https://docs.smith.langchain.com/] to visualize your agent.

After defining our input prompt formatter, we can define our output parser.
This function takes in a message in the form of `BaseMessage`, parses it and either returns an instance of `AgentAction` if there is more work to be done, or `AgentFinish` if the agent is done.

```typescript
function customOutputParser(message: BaseMessage): AgentAction | AgentFinish {
const text = message.content;
/** If the input includes "Final Answer" return as an instance of `AgentFinish` */
if (text.includes("Final Answer:")) {
const parts = text.split("Final Answer:");
const input = parts[parts.length - 1].trim();
const finalAnswers = { output: input };
return { log: text, returnValues: finalAnswers };
}
/** Use RegEx to extract any actions and their values */
const match = /Action: (.*)\nAction Input: (.*)/s.exec(text);
if (!match) {
throw new Error(`Could not parse LLM output: ${text}`);
}
/** Return as an instance of `AgentAction` */
return {
tool: match[1].trim(),
toolInput: match[2].trim().replace(/^"+|"+$/g, ""),
log: text,
};
}
```

After this, all that is left is to chain all the pieces together using a `RunnableSequence` and pass it through to the `AgentExecutor` so we can execute our agent.

```typescript
const runnable = RunnableSequence.from([
{
input: (i: InputValues) => i.input,
agent_scratchpad: (i: InputValues) => i.steps,
},
formatPrompt,
model,
customOutputParser,
]);

/** Pass our runnable to `AgentExecutor` to make our agent executable */
const executor = AgentExecutor.fromAgentAndTools({
agent: runnable,
tools,
});
```

Once we have our `executor` calling the agent is simple!

```typescript
console.log("Loaded agent.");
const input = `How many people live in canada as of 2023?`;
console.log(`Executing with input "${input}"...`);
const result = await executor.call({ input });
console.log(`Got output ${result.output}`);
/**
Loaded agent.
Executing with input "How many people live in canada as of 2023?"...
Got output Arrr, there be 38,781,291 people livin' in Canada in 2023.
*/
```
2 changes: 1 addition & 1 deletion examples/src/agents/custom_llm_agent_chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class CustomPromptTemplate extends BaseChatPromptTemplate {
}

_getPromptType(): string {
throw new Error("Not implemented");
return "chat";
}

async formatMessages(values: InputValues): Promise<BaseMessage[]> {
Expand Down
Loading