Skip to content

Commit 7c401a0

Browse files
committed
Update a bunch of tests and docs, preparing for 0.1.0
1 parent 71b2595 commit 7c401a0

38 files changed

+811
-970
lines changed

.formatter.exs

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"{mix,.formatter}.exs",
77
"{config,lib,test}/**/*.{ex,exs}",
88
"pages/cookbook/**/*.{ex,exs}",
9-
"examples/**/*.{ex,exs}"
9+
"examples/**/*.{ex,exs}",
10+
"scripts/**/*.{ex,exs}"
1011
]
1112
]

CHANGELOG.md

+23
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,29 @@
22

33
## [Unreleased](https://github.com/thmsmlr/instructor_ex/compare/v0.0.5..main)
44

5+
### Added
6+
- **New Adapters**: Anthropic, Gemini, Groq, Ollama, and VLLM. Each of these provides specialized support for their respective LLM APIs.
7+
- **`:json_schema` Mode**: The OpenAI adapter and others now support a `:json_schema` mode for more structured JSON outputs.
8+
- **`Instructor.Extras.ChainOfThought`**: A new module to guide multi-step reasoning processes with partial returns and final answers.
9+
- **Enhanced Streaming**: More robust partial/array streaming pipelines, plus improved SSE-based parsing for streamed responses.
10+
- **Re-ask/Follow-up Logic**: Adapters can now handle re-asking the LLM to correct invalid JSON responses when `max_retries` is set.
11+
12+
### Changed
13+
- **OpenAI Adapter Refactor**: A major internal refactor for more flexible streaming modes, additional “response format” options, and better error handling.
14+
- **Ecto Dependency**: Updated from `3.11` to `3.12`.
15+
- **Req Dependency**: Now supports `~> 0.5` or `~> 1.0`.
16+
17+
### Deprecated
18+
- **Schema Documentation via `@doc`**: Schemas using `@doc` to send instructions to the LLM will now emit a warning. Please migrate to `@llm_doc` via `use Instructor`.
19+
20+
### Breaking Changes
21+
- Some adapter configurations now require specifying an `:api_path` or `:auth_mode`. Verify your adapter config matches the new format.
22+
- The OpenAI adapter’s `:json_schema` mode strips unsupported fields (e.g., `format`, `pattern`) from schemas before sending them to the LLM.
23+
24+
### Fixed
25+
- Various improvements to JSON parsing and streaming handling, including better handling of partial/invalid responses.
26+
27+
528
## [v0.0.5](https://github.com/thmsmlr/instructor_ex/compare/v0.0.4..v0.0.5)
629

730
### Added

README.md

+38-85
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,49 @@ _Structured, Ecto outputs with OpenAI (and OSS LLMs)_
1313

1414
<!-- Docs -->
1515

16-
Structured prompting for LLMs. Instructor is a spiritual port of the great [Instructor Python Library](https://github.com/jxnl/instructor) by [@jxnlco](https://twitter.com/jxnlco), check out his [talk on YouTube](https://www.youtube.com/watch?v=yj-wSRJwrrc).
17-
18-
The Instructor library is useful for coaxing an LLM to return JSON that maps to an Ecto schema that you provide, rather than the default unstructured text output. If you define your own validation logic, Instructor can automatically retry prompts when validation fails (returning natural language error messages to the LLM, to guide it when making corrections).
16+
Check out our [Quickstart Guide](https://hexdocs.pm/instructor/quickstart.html) to get up and running with Instructor in minutes.
1917

20-
Instructor is designed to be used with the [OpenAI API](https://platform.openai.com/docs/api-reference/chat-completions/create) by default, but it also works with [llama.cpp](https://github.com/ggerganov/llama.cpp) and [Bumblebee](https://github.com/elixir-nx/bumblebee) (Coming Soon!) by using an extendable adapter behavior.
18+
Instructor provides structured prompting for LLMs. It is a spiritual port of the great [Instructor Python Library](https://github.com/jxnl/instructor) by [@jxnlco](https://twitter.com/jxnlco).
19+
20+
Instructor allows you to get structured output out of an LLM using Ecto.
21+
You don't have to define any JSON schemas.
22+
You can just use Ecto as you've always used it.
23+
And since it's just ecto, you can provide change set validations that you can use to ensure that what you're getting back from the LLM is not only properly structured, but semantically correct.
24+
25+
To learn more about the philosophy behind Instructor and its motivations, check out this Elixir Denver Meetup talk:
26+
27+
<div style="text-align: center">
28+
29+
[![Instructor: Structured prompting for LLMs](assets/youtube-thumbnail.png)](https://www.youtube.com/watch?v=RABXu7zqnT0)
30+
31+
</div>
32+
33+
While Instructor is designed to be used with OpenAI, it also supports every major AI lab and open source LLM inference server:
34+
35+
- OpenAI
36+
- Anthropic
37+
- Groq
38+
- Ollama
39+
- Gemini
40+
- vLLM
41+
- llama.cpp
2142

2243
At its simplest, usage is pretty straightforward:
2344

24-
1. Create an ecto schema, with a `@doc` string that explains the schema definition to the LLM.
25-
2. Define a `validate_changeset/1` function on the schema, and use the `Instructor.Validator` macro in order for Instructor to know about it.
45+
1. Create an ecto schema, with a `@llm_doc` string that explains the schema definition to the LLM.
46+
2. Define a `validate_changeset/1` function on the schema, and use the `use Instructor` macro in order for Instructor to know about it.
2647
2. Make a call to `Instructor.chat_completion/1` with an instruction for the LLM to execute.
2748

2849
You can use the `max_retries` parameter to automatically, iteratively go back and forth with the LLM to try fixing validation errorswhen they occur.
2950

3051
```elixir
52+
Mix.install([:instructor])
53+
3154
defmodule SpamPrediction do
3255
use Ecto.Schema
33-
use Instructor.Validator
56+
use Validator
3457

35-
@doc """
58+
@llm_doc """
3659
## Field Descriptions:
3760
- class: Whether or not the email is spam.
3861
- reason: A short, less than 10 word rationalization for the classification.
@@ -57,7 +80,7 @@ end
5780

5881
is_spam? = fn text ->
5982
Instructor.chat_completion(
60-
model: "gpt-3.5-turbo",
83+
model: "gpt-4o-mini",
6184
response_model: SpamPrediction,
6285
max_retries: 3,
6386
messages: [
@@ -69,9 +92,10 @@ is_spam? = fn text ->
6992
They sell all types of clothing.
7093
7194
Classify the following email:
72-
```
73-
#{text}
74-
```
95+
96+
<email>
97+
#{text}
98+
</email>
7599
"""
76100
}
77101
]
@@ -83,17 +107,6 @@ is_spam?.("Hello I am a Nigerian prince and I would like to send you money")
83107
# => {:ok, %SpamPrediction{class: :spam, reason: "Nigerian prince email scam", score: 0.98}}
84108
```
85109

86-
Check out our [Quickstart Guide](https://hexdocs.pm/instructor/quickstart.html) for more code snippets that you can run locally (in Livebook). Or, to get a better idea of the thinking behind Instructor, read more about our [Philosophy & Motivations](https://hexdocs.pm/instructor/philosophy.html).
87-
88-
Optionally, you can also customize the your llama.cpp calls (with defaults shown):
89-
```elixir
90-
llamacpp
91-
config :instructor, adapter: Instructor.Adapters.Llamacpp
92-
config :instructor, :llamacpp,
93-
chat_template: :mistral_instruct,
94-
api_url: "http://localhost:8080/completion"
95-
````
96-
97110
<!-- Docs -->
98111

99112
## Installation
@@ -103,67 +116,7 @@ In your mix.exs,
103116
```elixir
104117
def deps do
105118
[
106-
{:instructor, "~> 0.0.5"}
107-
]
108-
end
109-
```
110-
111-
InstructorEx uses [Code.fetch_docs/1](https://hexdocs.pm/elixir/1.16.2/Code.html#fetch_docs/1) to fetch LLM instructions from the Ecto schema specified in `response_model`. If your project is deployed using [releases](https://hexdocs.pm/mix/Mix.Tasks.Release.html), add the following configuration to mix.exs to prevent docs from being stripped from the release:
112-
113-
```elixir
114-
def project do
115-
# ...
116-
releases: [
117-
myapp: [
118-
strip_beams: [keep: ["Docs"]]
119-
]
119+
{:instructor, "~> 0.1.0"}
120120
]
121121
end
122-
```
123-
124-
## TODO
125-
126-
- [ ] Partial Schemaless doesn't work since fields are set to required in Ecto.
127-
- [x] Groq adapter
128-
- [ ] @doc gets stripped in release, find a workaround
129-
- [ ] ChainOfThought doesn't work with max_retries
130-
- [ ] Logging for Distillation / Finetuning
131-
- [ ] Add a Bumblebee adapter
132-
- [ ] Support naked ecto types by auto-wrapping, not just maps of ecto types, do not wrap if we don't need to... Current codepaths are muddled
133-
- [ ] Optional/Maybe types
134-
- [ ] Add Livebook Tutorials, include in Hexdocs
135-
- [x] Text Classification
136-
- [ ] Self Critique
137-
- [ ] Image Extracting Tables
138-
- [ ] Moderation
139-
- [x] Citations
140-
- [ ] Knowledge Graph
141-
- [ ] Entity Resolution
142-
- [ ] Search Queries
143-
- [ ] Query Decomposition
144-
- [ ] Recursive Schemas
145-
- [x] Table Extraction
146-
- [x] Action Item and Dependency Mapping
147-
- [ ] Multi-File Code Generation
148-
- [ ] PII Data Sanitizatiommersed
149-
- [x] Update hexdocs homepage to include example for tutorial
150-
151-
## Blog Posts
152-
153-
- [ ] Why structured prompting?
154-
155-
Meditations on new HCI.
156-
Finally we have software that can understand text. f(text) -> text.
157-
This is great, as it gives us a new domain, but the range is still text.
158-
While we can use string interpolation to map Software 1.0 into f(text), the outputs are not interoperable with Software 1.0.
159-
Hence why UXs available to us are things like Chatbots as our users have to interpret the output.
160-
161-
Instructor, structure prompting, gives use f(text) -> ecto_schema.
162-
Schemas are the lingua franca of Software 1.0.
163-
With Instrutor we can now seamlessly move back and forth between Software 1.0 and Software 2.0.
164-
165-
Now we can maximally leverage AI...
166-
167-
- [ ] From GPT-4 to zero-cost production - Distilation, local-llms, and the cost structure of AI.
168-
169-
... 😘
122+
```

assets/youtube-thumbnail.png

510 KB
Loading

lib/instructor.ex

+7-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ defmodule Instructor do
33

44
alias Instructor.JSONSchema
55

6+
@type stream :: Enumerable.t()
7+
68
@external_resource "README.md"
79

810
[_, readme_docs, _] =
@@ -36,7 +38,7 @@ defmodule Instructor do
3638
## Examples
3739
3840
iex> Instructor.chat_completion(
39-
...> model: "gpt-3.5-turbo",
41+
...> model: "gpt-4o-mini",
4042
...> response_model: Instructor.Demos.SpamPrediction,
4143
...> messages: [
4244
...> %{
@@ -56,7 +58,7 @@ defmodule Instructor do
5658
Partial streaming will emit the record multiple times until it's complete.
5759
5860
iex> Instructor.chat_completion(
59-
...> model: "gpt-3.5-turbo",
61+
...> model: "gpt-4o-mini",
6062
...> response_model: {:partial, %{name: :string, birth_date: :date}}
6163
...> messages: [
6264
...> %{
@@ -74,7 +76,7 @@ defmodule Instructor do
7476
and instructor will emit them one at a time as they arrive in complete form and validated.
7577
7678
iex> Instructor.chat_completion(
77-
...> model: "gpt-3.5-turbo",
79+
...> model: "gpt-4o-mini",
7880
...> response_model: {:array, %{name: :string, birth_date: :date}}
7981
...> messages: [
8082
...> %{
@@ -94,7 +96,7 @@ defmodule Instructor do
9496
If there's a validation error, it will return an error tuple with the change set describing the errors.
9597
9698
iex> Instructor.chat_completion(
97-
...> model: "gpt-3.5-turbo",
99+
...> model: "gpt-4o-mini",
98100
...> response_model: Instructor.Demos.SpamPrediction,
99101
...> messages: [
100102
...> %{
@@ -118,7 +120,7 @@ defmodule Instructor do
118120
{:ok, Ecto.Schema.t()}
119121
| {:error, Ecto.Changeset.t()}
120122
| {:error, String.t()}
121-
| Stream.t()
123+
| stream()
122124
def chat_completion(params, config \\ nil) do
123125
params =
124126
params

lib/instructor/adapter.ex

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ defmodule Instructor.Adapter do
66
@type params :: [Keyword.t()]
77
@type config :: any()
88
@type raw_response :: any()
9+
@type stream :: Enumerable.t()
910

1011
@callback chat_completion(params(), config()) ::
11-
Stream.t() | {:ok, raw_response(), String.t()} | {:error, String.t()}
12+
stream() | {:ok, raw_response(), String.t()} | {:error, String.t()}
1213

1314
@callback reask_messages(raw_response(), params(), config()) :: [map()]
1415
end

lib/instructor/adapters/anthropic.ex

+34
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,40 @@ defmodule Instructor.Adapters.Anthropic do
125125
args
126126
end
127127

128+
@impl true
129+
def reask_messages(raw_response, params, _config) do
130+
reask_messages_for_mode(params[:mode], raw_response)
131+
end
132+
133+
defp reask_messages_for_mode(:tools, %{
134+
"choices" => [
135+
%{
136+
"message" =>
137+
%{
138+
"tool_calls" => [
139+
%{"id" => tool_call_id, "function" => %{"name" => name, "arguments" => args}} =
140+
function
141+
]
142+
} = message
143+
}
144+
]
145+
}) do
146+
[
147+
Map.put(message, "content", function |> Jason.encode!())
148+
|> Map.new(fn {k, v} -> {String.to_atom(k), v} end),
149+
%{
150+
role: "tool",
151+
tool_call_id: tool_call_id,
152+
name: name,
153+
content: args
154+
}
155+
]
156+
end
157+
158+
defp reask_messages_for_mode(_mode, _raw_response) do
159+
[]
160+
end
161+
128162
defp url(config), do: api_url(config) <> "/v1/messages"
129163

130164
defp api_url(config), do: Keyword.fetch!(config, :api_url)

0 commit comments

Comments
 (0)