Skip to content

Commit cf84bf2

Browse files
authored
Adds JSON mode to Ollama and ChatOllama (#3229)
* Update cookbook * Adds format option to Ollama * Fix type * Format * Factor out utility type * Update docs
1 parent 92ead3a commit cf84bf2

File tree

12 files changed

+121
-14
lines changed

12 files changed

+121
-14
lines changed

β€Ždocs/docs/integrations/chat/ollama.mdxβ€Ž

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,13 @@ import CodeBlock from "@theme/CodeBlock";
2121
import OllamaExample from "@examples/models/chat/integration_ollama.ts";
2222

2323
<CodeBlock language="typescript">{OllamaExample}</CodeBlock>
24+
25+
## JSON mode
26+
27+
Ollama also supports a JSON mode that coerces model outputs to only return JSON. Here's an example of how this can be useful for extraction:
28+
29+
import OllamaExample from "@examples/models/chat/integration_ollama_json_mode.ts";
30+
31+
<CodeBlock language="typescript">{OllamaExample}</CodeBlock>
32+
33+
You can see a simple LangSmith trace of this here: https://smith.langchain.com/public/92aebeca-d701-4de0-a845-f55df04eff04/r
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ChatOllama } from "langchain/chat_models/ollama";
2+
import { ChatPromptTemplate } from "langchain/prompts";
3+
4+
const prompt = ChatPromptTemplate.fromMessages([
5+
[
6+
"system",
7+
`You are an expert translator. Format all responses as JSON objects with two keys: "original" and "translated".`,
8+
],
9+
["human", `Translate "{input}" into {language}.`],
10+
]);
11+
12+
const model = new ChatOllama({
13+
baseUrl: "http://localhost:11434", // Default value
14+
model: "llama2", // Default value
15+
format: "json",
16+
});
17+
18+
const chain = prompt.pipe(model);
19+
20+
const result = await chain.invoke({
21+
input: "I love programming",
22+
language: "German",
23+
});
24+
25+
console.log(result);
26+
27+
/*
28+
AIMessage {
29+
content: '{"original": "I love programming", "translated": "Ich liebe das Programmieren"}',
30+
additional_kwargs: {}
31+
}
32+
*/

β€Žlangchain/src/chat_models/ollama.tsβ€Ž

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ChatGenerationChunk,
99
ChatMessage,
1010
} from "../schema/index.js";
11+
import type { StringWithAutocomplete } from "../util/types.js";
1112

1213
/**
1314
* An interface defining the options for an Ollama API call. It extends
@@ -94,6 +95,8 @@ export class ChatOllama
9495

9596
vocabOnly?: boolean;
9697

98+
format?: StringWithAutocomplete<"json">;
99+
97100
constructor(fields: OllamaInput & BaseChatModelParams) {
98101
super(fields);
99102
this.model = fields.model ?? this.model;
@@ -130,6 +133,7 @@ export class ChatOllama
130133
this.useMLock = fields.useMLock;
131134
this.useMMap = fields.useMMap;
132135
this.vocabOnly = fields.vocabOnly;
136+
this.format = fields.format;
133137
}
134138

135139
_llmType() {
@@ -145,6 +149,7 @@ export class ChatOllama
145149
invocationParams(options?: this["ParsedCallOptions"]) {
146150
return {
147151
model: this.model,
152+
format: this.format,
148153
options: {
149154
embedding_only: this.embeddingOnly,
150155
f16_kv: this.f16KV,

β€Žlangchain/src/chat_models/tests/chatollama.int.test.tsβ€Ž

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { AIMessage, HumanMessage } from "../../schema/index.js";
44
import { LLMChain } from "../../chains/llm_chain.js";
55
import { PromptTemplate } from "../../prompts/prompt.js";
66
import { BufferMemory } from "../../memory/buffer_memory.js";
7-
import { BytesOutputParser } from "../../schema/output_parser.js";
7+
import {
8+
BytesOutputParser,
9+
StringOutputParser,
10+
} from "../../schema/output_parser.js";
811

912
test.skip("test call", async () => {
1013
const ollama = new ChatOllama({});
@@ -129,3 +132,25 @@ test.skip("should stream through with a bytes output parser", async () => {
129132
console.log(chunks.join(""));
130133
expect(chunks.length).toBeGreaterThan(1);
131134
});
135+
136+
test.skip("JSON mode", async () => {
137+
const TEMPLATE = `You are a pirate named Patchy. All responses must be in pirate dialect and in JSON format, with a property named "response" followed by the value.
138+
139+
User: {input}
140+
AI:`;
141+
142+
// Infer the input variables from the template
143+
const prompt = PromptTemplate.fromTemplate(TEMPLATE);
144+
145+
const ollama = new ChatOllama({
146+
model: "llama2",
147+
baseUrl: "http://127.0.0.1:11434",
148+
format: "json",
149+
});
150+
const outputParser = new StringOutputParser();
151+
const chain = prompt.pipe(ollama).pipe(outputParser);
152+
const res = await chain.invoke({
153+
input: `Translate "I love programming" into German.`,
154+
});
155+
expect(JSON.parse(res).response).toBeDefined();
156+
});

β€Žlangchain/src/document_loaders/fs/unstructured.tsβ€Ž

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { getEnv } from "../../util/env.js";
99
import { Document } from "../../document.js";
1010
import { BaseDocumentLoader } from "../base.js";
11+
import type { StringWithAutocomplete } from "../../util/types.js";
1112

1213
const UNSTRUCTURED_API_FILETYPES = [
1314
".txt",
@@ -95,12 +96,6 @@ export type SkipInferTableTypes =
9596
*/
9697
type ChunkingStrategy = "None" | "by_title";
9798

98-
/**
99-
* Represents a string value with autocomplete suggestions. It is used for
100-
* the `strategy` property in the UnstructuredLoaderOptions.
101-
*/
102-
type StringWithAutocomplete<T> = T | (string & Record<never, never>);
103-
10499
export type UnstructuredLoaderOptions = {
105100
apiKey?: string;
106101
apiUrl?: string;

β€Žlangchain/src/embeddings/ollama.tsβ€Ž

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { OllamaInput, OllamaRequestParams } from "../util/ollama.js";
22
import { Embeddings, EmbeddingsParams } from "./base.js";
33

4-
type CamelCasedRequestOptions = Omit<OllamaInput, "baseUrl" | "model">;
4+
type CamelCasedRequestOptions = Omit<
5+
OllamaInput,
6+
"baseUrl" | "model" | "format"
7+
>;
58

69
/**
710
* Interface for OllamaEmbeddings parameters. Extends EmbeddingsParams and

β€Žlangchain/src/llms/ollama.tsβ€Ž

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "../util/ollama.js";
77
import { CallbackManagerForLLMRun } from "../callbacks/manager.js";
88
import { GenerationChunk } from "../schema/index.js";
9+
import type { StringWithAutocomplete } from "../util/types.js";
910

1011
/**
1112
* Class that represents the Ollama language model. It extends the base
@@ -82,6 +83,8 @@ export class Ollama extends LLM<OllamaCallOptions> implements OllamaInput {
8283

8384
vocabOnly?: boolean;
8485

86+
format?: StringWithAutocomplete<"json">;
87+
8588
constructor(fields: OllamaInput & BaseLLMParams) {
8689
super(fields);
8790
this.model = fields.model ?? this.model;
@@ -119,6 +122,7 @@ export class Ollama extends LLM<OllamaCallOptions> implements OllamaInput {
119122
this.useMLock = fields.useMLock;
120123
this.useMMap = fields.useMMap;
121124
this.vocabOnly = fields.vocabOnly;
125+
this.format = fields.format;
122126
}
123127

124128
_llmType() {
@@ -128,6 +132,7 @@ export class Ollama extends LLM<OllamaCallOptions> implements OllamaInput {
128132
invocationParams(options?: this["ParsedCallOptions"]) {
129133
return {
130134
model: this.model,
135+
format: this.format,
131136
options: {
132137
embedding_only: this.embeddingOnly,
133138
f16_kv: this.f16KV,

β€Žlangchain/src/llms/tests/ollama.int.test.tsβ€Ž

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { test } from "@jest/globals";
22
import { Ollama } from "../ollama.js";
33
import { PromptTemplate } from "../../prompts/prompt.js";
4-
import { BytesOutputParser } from "../../schema/output_parser.js";
4+
import {
5+
BytesOutputParser,
6+
StringOutputParser,
7+
} from "../../schema/output_parser.js";
58

69
test.skip("test call", async () => {
710
const ollama = new Ollama({});
@@ -86,3 +89,25 @@ test.skip("should stream through with a bytes output parser", async () => {
8689
console.log(chunks.join(""));
8790
expect(chunks.length).toBeGreaterThan(1);
8891
});
92+
93+
test.skip("JSON mode", async () => {
94+
const TEMPLATE = `You are a pirate named Patchy. All responses must be in pirate dialect and in JSON format, with a property named "response" followed by the value.
95+
96+
User: {input}
97+
AI:`;
98+
99+
// Infer the input variables from the template
100+
const prompt = PromptTemplate.fromTemplate(TEMPLATE);
101+
102+
const ollama = new Ollama({
103+
model: "llama2",
104+
baseUrl: "http://127.0.0.1:11434",
105+
format: "json",
106+
});
107+
const outputParser = new StringOutputParser();
108+
const chain = prompt.pipe(ollama).pipe(outputParser);
109+
const res = await chain.invoke({
110+
input: `Translate "I love programming" into German.`,
111+
});
112+
expect(JSON.parse(res).response).toBeDefined();
113+
});

β€Žlangchain/src/prompts/base.tsβ€Ž

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import { SerializedBasePromptTemplate } from "./serde.js";
1414
import { SerializedFields } from "../load/map_keys.js";
1515
import { Runnable } from "../schema/runnable/index.js";
1616
import { BaseCallbackConfig } from "../callbacks/manager.js";
17+
import type { StringWithAutocomplete } from "../util/types.js";
1718

1819
export type TypedPromptInputValues<RunInput> = InputValues<
19-
Extract<keyof RunInput, string> | (string & Record<never, never>)
20+
StringWithAutocomplete<Extract<keyof RunInput, string>>
2021
>;
2122

2223
/**

β€Žlangchain/src/schema/index.tsβ€Ž

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { OpenAI as OpenAIClient } from "openai";
22
import { Document } from "../document.js";
33
import { Serializable, SerializedConstructor } from "../load/serializable.js";
4+
import type { StringWithAutocomplete } from "../util/types.js";
45

56
export const RUN_KEY = "__run";
67

@@ -622,10 +623,7 @@ export class ChatMessage
622623

623624
export type BaseMessageLike =
624625
| BaseMessage
625-
| [
626-
MessageType | "user" | "assistant" | (string & Record<never, never>),
627-
string
628-
]
626+
| [StringWithAutocomplete<MessageType | "user" | "assistant">, string]
629627
| string;
630628

631629
export function isBaseMessage(

0 commit comments

Comments
 (0)