raft-ft 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- raft_ft-2.2.0/.gitignore +39 -0
- raft_ft-2.2.0/LICENSE +22 -0
- raft_ft-2.2.0/PKG-INFO +215 -0
- raft_ft-2.2.0/README.md +179 -0
- raft_ft-2.2.0/pyproject.toml +75 -0
- raft_ft-2.2.0/src/raft/__init__.py +9 -0
- raft_ft-2.2.0/src/raft/chunker.py +138 -0
- raft_ft-2.2.0/src/raft/cli.py +189 -0
- raft_ft-2.2.0/src/raft/convo_structurer.py +338 -0
- raft_ft-2.2.0/src/raft/embeddings_helpers.py +106 -0
- raft_ft-2.2.0/src/raft/files_helper.py +179 -0
- raft_ft-2.2.0/src/raft/flows.py +116 -0
- raft_ft-2.2.0/src/raft/generate_finetune.py +83 -0
- raft_ft-2.2.0/src/raft/hf_finetune.py +268 -0
- raft_ft-2.2.0/src/raft/hx.py +169 -0
- raft_ft-2.2.0/src/raft/interactive.py +42 -0
- raft_ft-2.2.0/src/raft/memories.py +212 -0
- raft_ft-2.2.0/src/raft/oai_finetune.py +199 -0
- raft_ft-2.2.0/src/raft/prompt_manager.py +122 -0
- raft_ft-2.2.0/src/raft/substack_embeddings.py +69 -0
- raft_ft-2.2.0/src/raft/tweet_mode.py +320 -0
- raft_ft-2.2.0/tests/test_v2.py +389 -0
raft_ft-2.2.0/.gitignore
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# python / pipenv gitignore
|
|
2
|
+
|
|
3
|
+
*.pyc
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.pyo
|
|
6
|
+
*.pyd
|
|
7
|
+
*.egg-info/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
htmlcov/
|
|
13
|
+
.tox/
|
|
14
|
+
*.so
|
|
15
|
+
*.o
|
|
16
|
+
*.dylib
|
|
17
|
+
.Python
|
|
18
|
+
env/
|
|
19
|
+
venv/
|
|
20
|
+
ENV/
|
|
21
|
+
env.bak/
|
|
22
|
+
venv.bak/
|
|
23
|
+
.env
|
|
24
|
+
.venv
|
|
25
|
+
.spyderproject
|
|
26
|
+
.spyproject
|
|
27
|
+
.ropeproject
|
|
28
|
+
.idea/
|
|
29
|
+
.vscode/
|
|
30
|
+
*.swp
|
|
31
|
+
*.swo
|
|
32
|
+
.DS_Store
|
|
33
|
+
secret/
|
|
34
|
+
data/garymarcus/**/*.*
|
|
35
|
+
data/garymarcus/*
|
|
36
|
+
data/garymarcus/
|
|
37
|
+
data/garymarcus/chroma.sqlite3
|
|
38
|
+
data/garymarcus/9878a8dc-7749-4f04-897e-ce480fc4f0f0/*.bin
|
|
39
|
+
.ruff_cache/
|
raft_ft-2.2.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Claudio Brandolino
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
raft_ft-2.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: raft-ft
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: RAFT: Retrieval-Augmented Fine-Tuning — build persona datasets and finetune models on them.
|
|
5
|
+
Project-URL: Homepage, https://github.com/lumpenspace/raft
|
|
6
|
+
Project-URL: Issues, https://github.com/lumpenspace/raft/issues
|
|
7
|
+
Author: lumpenspace
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: finetuning,huggingface,llm,openai,persona,rag
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
21
|
+
Requires-Dist: chromadb<2,>=1.0
|
|
22
|
+
Requires-Dist: openai>=1.14
|
|
23
|
+
Requires-Dist: pandas>=2.2
|
|
24
|
+
Requires-Dist: python-dotenv>=1.0
|
|
25
|
+
Requires-Dist: requests>=2.31
|
|
26
|
+
Requires-Dist: rich>=13.7
|
|
27
|
+
Requires-Dist: tiktoken>=0.6
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
32
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
33
|
+
Provides-Extra: hf
|
|
34
|
+
Requires-Dist: opbdh>=1.3; extra == 'hf'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# RAFT: Retrieval-Augmented Fine-Tuning
|
|
38
|
+
|
|
39
|
+
Note from [@lumpenspace](http://x.com/lumpenspace):
|
|
40
|
+
|
|
41
|
+
> This technique is something ive been working last summer/fall, originally planning to get a paper out of it. Then it seemed obvious so i didn't, and instead used
|
|
42
|
+
> pieces of this repo for other projects and abandoned this repo.
|
|
43
|
+
>
|
|
44
|
+
> I [discovered not without horror](https://x.com/lumpenspace/status/1769809977030426831?s=20) that some of the tech is still cutting edge, so i might as well share it.
|
|
45
|
+
>
|
|
46
|
+
> In this old version, the main simulee was Gary Marcus; the idea was to make a model that could pass as him in a conversation and demonstrate how stochastic
|
|
47
|
+
> parrots are still plenty capable to mimic the deterministic ones, but there's a couple interesting tidbits that i've moved to more decent repos but, given my
|
|
48
|
+
> pretty annoying habit of not sharing subpar code, you might as well start here.
|
|
49
|
+
>
|
|
50
|
+
> Scroll to [usage and functionality](#usage-and-functionality) for cli options, what you can do (apart from what's described below) is automagically fetch, chunk, embed, story, and query a db starting from a substack url.
|
|
51
|
+
>
|
|
52
|
+
> Not guaranteeing anything works, but it's a good starting point for a lot of things and includes a couple of new ideas.
|
|
53
|
+
|
|
54
|
+
### OH GOSH
|
|
55
|
+
|
|
56
|
+
ok then, a friend asked so now it is more lenient with the version number and uses poetry for the dependencies. It's still a mess, but it's a more runnable mess.
|
|
57
|
+
|
|
58
|
+
(2.1: poetry is gone — it's uv + hatchling now, like the other repos in this constellation.)
|
|
59
|
+
|
|
60
|
+
## 2.0
|
|
61
|
+
|
|
62
|
+
New major version. Substack is no longer the only way in:
|
|
63
|
+
|
|
64
|
+
- **`raft interactive`** — guided end-to-end session. Asks who the target is, collects
|
|
65
|
+
text sources (substack / tweets / local files) and conversation examples. Structured
|
|
66
|
+
inputs (raft transcripts, chat-message JSON, grounding jsonl) are recognised and
|
|
67
|
+
imported as-is; unstructured ones (raw chat logs, podcast transcripts, whatever) are
|
|
68
|
+
converted into transcript datasets with an LLM (`RAFT_LLM_MODEL`, default `gpt-4o`).
|
|
69
|
+
Then chunk/embed/ft:gen/ft:run, each step optional.
|
|
70
|
+
- **`raft tweets`** — tweet mode. First asks which network(s) to draw from — **X /
|
|
71
|
+
Twitter**, **Bluesky**, or **both, merged into one dataset** — then calls
|
|
72
|
+
[ariadne](https://github.com/lumpenspace/ariadne)'s Python API to reconstruct reply
|
|
73
|
+
branches and imports them: thread texts become grounding documents, reply branches
|
|
74
|
+
become q/a transcripts. The target's own posts become the answers, whoever they were
|
|
75
|
+
replying to becomes the questioner. For X you choose the source (archive export,
|
|
76
|
+
CSV/JSON dump, or a public handle) and can add the **Community Archive**
|
|
77
|
+
(community-archive.org — no key, and it completes reply threads whose parents were
|
|
78
|
+
authored by other people) and/or a **twitterapi.io** key. Bluesky needs nothing but a
|
|
79
|
+
handle. Needs ariadne (≥ 0.4), which is not on PyPI (the name is taken by the GraphQL
|
|
80
|
+
library): `pip install git+https://github.com/lumpenspace/ariadne`.
|
|
81
|
+
- **`raft ft:run <name> --model <model>`** — model routing. OpenAI-finetunable ids
|
|
82
|
+
(gpt-4o-mini and friends) go through the OpenAI finetuning API as before. Any other
|
|
83
|
+
model — i.e. a huggingface `org/name` id — is trained on a rented GPU pod via
|
|
84
|
+
[opbdh](https://github.com/lumpenspace/opbdh): raft generates a self-contained LoRA SFT
|
|
85
|
+
run directory (script + requirements + dataset) and hands it to `opbdh launch`.
|
|
86
|
+
Interactively it helps you pick the model (`opbdh models search`) and size the pod;
|
|
87
|
+
non-interactively, opbdh settings pass straight through:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
raft ft:run garymarcus --model Qwen/Qwen2.5-7B-Instruct --vram-gb 48 --max-spend 5
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Anything opbdh accepts (`--provider`, `--max-dollars-per-hour`, ...) can be appended
|
|
94
|
+
and is forwarded to its Python API, and an `opbdh.json` in the project root works too.
|
|
95
|
+
The trained adapter lands in `runpod_results/<run_id>/results/adapter`. Install with
|
|
96
|
+
`pip install 'raft-ft[hf]'` (python ≥ 3.11) plus a one-time `opbdh config wizard`.
|
|
97
|
+
|
|
98
|
+
Both integrations go through the two tools' Python APIs rather than shelling out, so
|
|
99
|
+
raft gets the reconstructed threads and the run result as data — and surfaces their
|
|
100
|
+
errors (spend guard tripped, remote job failed) directly.
|
|
101
|
+
|
|
102
|
+
# RAFT / RATF
|
|
103
|
+
|
|
104
|
+
- [RAFT: Retrieval-Augmented Fine-Tuning](#raft-retrieval-augmented-fine-tuning)
|
|
105
|
+
- [OH GOSH](#oh-gosh)
|
|
106
|
+
- [RAFT / RATF](#raft--ratf)
|
|
107
|
+
- [Abstract](#abstract)
|
|
108
|
+
- [Process](#process)
|
|
109
|
+
- [Retrieval-Augmented Fine-Tuning](#retrieval-augmented-fine-tuning)
|
|
110
|
+
- [Generation](#generation)
|
|
111
|
+
- [Usage and Functionality](#usage-and-functionality)
|
|
112
|
+
- [Installation](#installation)
|
|
113
|
+
- [Usage](#usage)
|
|
114
|
+
- [Licence](#licence)
|
|
115
|
+
|
|
116
|
+
RAFT, or Retrieval-Augmented Fine-Tuning, is a method comprising of a fine-tuning and a RAG-based retrieval phase. It is particularly suited for the creation of agents that realistically emulate a specific human target.
|
|
117
|
+
|
|
118
|
+
RATF, or Replica Agent Testing Framework, is a framework for evaluating the performance of dialogue agents emulating real-world targets.
|
|
119
|
+
|
|
120
|
+
## Abstract
|
|
121
|
+
|
|
122
|
+
The emulation of specific humans in conversational agents presents unique challenges and opportunities for contextual understanding, theory of mind and personalization. In this paper, we introduce the Retrieval-Augmented Fine-Tuning (RAFT) methodology, designed explicitly for simulating individual humans.
|
|
123
|
+
|
|
124
|
+
RAFT employs a dual-phase process:
|
|
125
|
+
|
|
126
|
+
In the **Retrieval-Augmented Fine-Tuning phase** proper, combines interview transcripts featuring the human target with appropriately selected, rephrased and evaluated "memories" from the author's past output to give the model a sense of the way the target human combines past writings with the current context to generate responses.
|
|
127
|
+
|
|
128
|
+
In the **generation phase**, these memories augment the language model's responses to create a nuanced and personalized dialogue.
|
|
129
|
+
|
|
130
|
+
We demonstrate the efficacy of RAFT through a unique evaluation metric, RATF (Replica Agent Testing Framework) that compares model-generated responses with original human responses in an interview setting. Our findings highlight RAFT's potential to significantly advance the field of personalized, context-sensitive conversational agents.
|
|
131
|
+
|
|
132
|
+
## Process
|
|
133
|
+
|
|
134
|
+
### Retrieval-Augmented Fine-Tuning
|
|
135
|
+
|
|
136
|
+
Two datasets are required for the fine-tuning phase:
|
|
137
|
+
|
|
138
|
+
- A dataset of **interview transcripts** featuring the target human
|
|
139
|
+
- A dataset of the **target's past written output** (tweets, essays, etc.)
|
|
140
|
+
|
|
141
|
+
The interview transcripts used within a RAG-inspired process retreiving "memories" from the target's written output for each of the interviewer's questions. These memories are then rephrased and evaluated in the context of the target user's answer and, if found useful, they are interpolated between question and answer for the fine-tuning phase.
|
|
142
|
+
|
|
143
|
+
The steps to reproduce this process are as follows:
|
|
144
|
+
|
|
145
|
+
1. Create a dataset of interview transcripts featuring the target human. Each interview is a separate `data/{name}_transcript_{i}.json` file holding `{"participants": {"q": ..., "a": ...}, "date": ..., "url": ..., "exchanges": [[question, answer], ...]}`. As of 2.0 you don't have to write these by hand: `raft interactive` takes chat-message JSON, ariadne output or plain unstructured transcripts and produces them for you.
|
|
146
|
+
2. Create a dataset of the author's past written output — `data/{name}.jsonl`, one `{"title", "link", "date", "content"}` object per line. `raft fetch` builds this from a substack; `raft tweets` from a tweet archive; `raft interactive` from arbitrary local files.
|
|
147
|
+
3. Split the past output dataset in chunks of a size suitable for the chosen embedding model (8192 tokens for Openai's text-embedding-ada-002), and collect metadata and embeddings for each chunk.
|
|
148
|
+
4. Store the resulting metadata and embeddings in a vector database (we use ChromaDB).
|
|
149
|
+
|
|
150
|
+
Then, in order to generate a fine-tuning dataset:
|
|
151
|
+
|
|
152
|
+
1. For each interview, run the RAG process to retrieve memories from the author's past output for each of the interviewer's questions.
|
|
153
|
+
2. Ask the model to rephrase each memory in the context of the interviewer's question. The same model and prompt will be used in the generation phase.
|
|
154
|
+
3. Evaluate the resulting memory by the question only first, and discard it if it is not considered useful by the model. We apply this first pass separately because, at inference time, we will not have access to the target human's answer.
|
|
155
|
+
4. Save the resulting context including question, memory and as many of the previous [question, memory and answers] tuples as possible, up to the maximum context size the finetune allows, as a new finetune sample.
|
|
156
|
+
|
|
157
|
+
#### Before/after pics (interview/ ft dataset)
|
|
158
|
+
|
|
159
|
+

|
|
160
|
+
|
|
161
|
+
### Generation
|
|
162
|
+
|
|
163
|
+
The fine-tuned model is then used to generate responses to the interviewer's questions. The model is prompted with the question and the rephrased memories, and the resulting response is evaluated using the RATF framework.
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
## Usage and Functionality
|
|
167
|
+
|
|
168
|
+
### Installation
|
|
169
|
+
|
|
170
|
+
The distribution is named `raft-ft` (`raft` was taken on PyPI); the import and the
|
|
171
|
+
CLI are still `raft`. Until the first `raft-ft` release lands on PyPI, install from
|
|
172
|
+
git:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
pip install git+https://github.com/lumpenspace/raft
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Once released:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
pip install raft-ft
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
For development, [uv](https://docs.astral.sh/uv/) manages the environment:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
uv sync --extra dev --extra hf
|
|
188
|
+
uv run pytest
|
|
189
|
+
uv run ruff check .
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Usage
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
raft -h
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The following actions are available:
|
|
201
|
+
|
|
202
|
+
- interactive: Guided end-to-end session: sources, conversations, finetune.
|
|
203
|
+
- tweets: Build a dataset from tweets via ariadne interactive.
|
|
204
|
+
- fetch: Fetch the blog from Substack and store it in the data directory.
|
|
205
|
+
- chunk: Chunk the blog into 4096 token pieces and store them in the data directory.
|
|
206
|
+
- embed: Create embeddings for the chunks and store them.
|
|
207
|
+
- ft:gen: Generate finetune files for the blog.
|
|
208
|
+
- ft:run: Run the finetune job (OpenAI, or huggingface via opbdh).
|
|
209
|
+
- bench:setup: Setup the benchmark for the blog.
|
|
210
|
+
- ask: Ask a question about the blog content.
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Licence
|
|
214
|
+
|
|
215
|
+
MIT
|
raft_ft-2.2.0/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# RAFT: Retrieval-Augmented Fine-Tuning
|
|
2
|
+
|
|
3
|
+
Note from [@lumpenspace](http://x.com/lumpenspace):
|
|
4
|
+
|
|
5
|
+
> This technique is something ive been working last summer/fall, originally planning to get a paper out of it. Then it seemed obvious so i didn't, and instead used
|
|
6
|
+
> pieces of this repo for other projects and abandoned this repo.
|
|
7
|
+
>
|
|
8
|
+
> I [discovered not without horror](https://x.com/lumpenspace/status/1769809977030426831?s=20) that some of the tech is still cutting edge, so i might as well share it.
|
|
9
|
+
>
|
|
10
|
+
> In this old version, the main simulee was Gary Marcus; the idea was to make a model that could pass as him in a conversation and demonstrate how stochastic
|
|
11
|
+
> parrots are still plenty capable to mimic the deterministic ones, but there's a couple interesting tidbits that i've moved to more decent repos but, given my
|
|
12
|
+
> pretty annoying habit of not sharing subpar code, you might as well start here.
|
|
13
|
+
>
|
|
14
|
+
> Scroll to [usage and functionality](#usage-and-functionality) for cli options, what you can do (apart from what's described below) is automagically fetch, chunk, embed, story, and query a db starting from a substack url.
|
|
15
|
+
>
|
|
16
|
+
> Not guaranteeing anything works, but it's a good starting point for a lot of things and includes a couple of new ideas.
|
|
17
|
+
|
|
18
|
+
### OH GOSH
|
|
19
|
+
|
|
20
|
+
ok then, a friend asked so now it is more lenient with the version number and uses poetry for the dependencies. It's still a mess, but it's a more runnable mess.
|
|
21
|
+
|
|
22
|
+
(2.1: poetry is gone — it's uv + hatchling now, like the other repos in this constellation.)
|
|
23
|
+
|
|
24
|
+
## 2.0
|
|
25
|
+
|
|
26
|
+
New major version. Substack is no longer the only way in:
|
|
27
|
+
|
|
28
|
+
- **`raft interactive`** — guided end-to-end session. Asks who the target is, collects
|
|
29
|
+
text sources (substack / tweets / local files) and conversation examples. Structured
|
|
30
|
+
inputs (raft transcripts, chat-message JSON, grounding jsonl) are recognised and
|
|
31
|
+
imported as-is; unstructured ones (raw chat logs, podcast transcripts, whatever) are
|
|
32
|
+
converted into transcript datasets with an LLM (`RAFT_LLM_MODEL`, default `gpt-4o`).
|
|
33
|
+
Then chunk/embed/ft:gen/ft:run, each step optional.
|
|
34
|
+
- **`raft tweets`** — tweet mode. First asks which network(s) to draw from — **X /
|
|
35
|
+
Twitter**, **Bluesky**, or **both, merged into one dataset** — then calls
|
|
36
|
+
[ariadne](https://github.com/lumpenspace/ariadne)'s Python API to reconstruct reply
|
|
37
|
+
branches and imports them: thread texts become grounding documents, reply branches
|
|
38
|
+
become q/a transcripts. The target's own posts become the answers, whoever they were
|
|
39
|
+
replying to becomes the questioner. For X you choose the source (archive export,
|
|
40
|
+
CSV/JSON dump, or a public handle) and can add the **Community Archive**
|
|
41
|
+
(community-archive.org — no key, and it completes reply threads whose parents were
|
|
42
|
+
authored by other people) and/or a **twitterapi.io** key. Bluesky needs nothing but a
|
|
43
|
+
handle. Needs ariadne (≥ 0.4), which is not on PyPI (the name is taken by the GraphQL
|
|
44
|
+
library): `pip install git+https://github.com/lumpenspace/ariadne`.
|
|
45
|
+
- **`raft ft:run <name> --model <model>`** — model routing. OpenAI-finetunable ids
|
|
46
|
+
(gpt-4o-mini and friends) go through the OpenAI finetuning API as before. Any other
|
|
47
|
+
model — i.e. a huggingface `org/name` id — is trained on a rented GPU pod via
|
|
48
|
+
[opbdh](https://github.com/lumpenspace/opbdh): raft generates a self-contained LoRA SFT
|
|
49
|
+
run directory (script + requirements + dataset) and hands it to `opbdh launch`.
|
|
50
|
+
Interactively it helps you pick the model (`opbdh models search`) and size the pod;
|
|
51
|
+
non-interactively, opbdh settings pass straight through:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
raft ft:run garymarcus --model Qwen/Qwen2.5-7B-Instruct --vram-gb 48 --max-spend 5
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Anything opbdh accepts (`--provider`, `--max-dollars-per-hour`, ...) can be appended
|
|
58
|
+
and is forwarded to its Python API, and an `opbdh.json` in the project root works too.
|
|
59
|
+
The trained adapter lands in `runpod_results/<run_id>/results/adapter`. Install with
|
|
60
|
+
`pip install 'raft-ft[hf]'` (python ≥ 3.11) plus a one-time `opbdh config wizard`.
|
|
61
|
+
|
|
62
|
+
Both integrations go through the two tools' Python APIs rather than shelling out, so
|
|
63
|
+
raft gets the reconstructed threads and the run result as data — and surfaces their
|
|
64
|
+
errors (spend guard tripped, remote job failed) directly.
|
|
65
|
+
|
|
66
|
+
# RAFT / RATF
|
|
67
|
+
|
|
68
|
+
- [RAFT: Retrieval-Augmented Fine-Tuning](#raft-retrieval-augmented-fine-tuning)
|
|
69
|
+
- [OH GOSH](#oh-gosh)
|
|
70
|
+
- [RAFT / RATF](#raft--ratf)
|
|
71
|
+
- [Abstract](#abstract)
|
|
72
|
+
- [Process](#process)
|
|
73
|
+
- [Retrieval-Augmented Fine-Tuning](#retrieval-augmented-fine-tuning)
|
|
74
|
+
- [Generation](#generation)
|
|
75
|
+
- [Usage and Functionality](#usage-and-functionality)
|
|
76
|
+
- [Installation](#installation)
|
|
77
|
+
- [Usage](#usage)
|
|
78
|
+
- [Licence](#licence)
|
|
79
|
+
|
|
80
|
+
RAFT, or Retrieval-Augmented Fine-Tuning, is a method comprising of a fine-tuning and a RAG-based retrieval phase. It is particularly suited for the creation of agents that realistically emulate a specific human target.
|
|
81
|
+
|
|
82
|
+
RATF, or Replica Agent Testing Framework, is a framework for evaluating the performance of dialogue agents emulating real-world targets.
|
|
83
|
+
|
|
84
|
+
## Abstract
|
|
85
|
+
|
|
86
|
+
The emulation of specific humans in conversational agents presents unique challenges and opportunities for contextual understanding, theory of mind and personalization. In this paper, we introduce the Retrieval-Augmented Fine-Tuning (RAFT) methodology, designed explicitly for simulating individual humans.
|
|
87
|
+
|
|
88
|
+
RAFT employs a dual-phase process:
|
|
89
|
+
|
|
90
|
+
In the **Retrieval-Augmented Fine-Tuning phase** proper, combines interview transcripts featuring the human target with appropriately selected, rephrased and evaluated "memories" from the author's past output to give the model a sense of the way the target human combines past writings with the current context to generate responses.
|
|
91
|
+
|
|
92
|
+
In the **generation phase**, these memories augment the language model's responses to create a nuanced and personalized dialogue.
|
|
93
|
+
|
|
94
|
+
We demonstrate the efficacy of RAFT through a unique evaluation metric, RATF (Replica Agent Testing Framework) that compares model-generated responses with original human responses in an interview setting. Our findings highlight RAFT's potential to significantly advance the field of personalized, context-sensitive conversational agents.
|
|
95
|
+
|
|
96
|
+
## Process
|
|
97
|
+
|
|
98
|
+
### Retrieval-Augmented Fine-Tuning
|
|
99
|
+
|
|
100
|
+
Two datasets are required for the fine-tuning phase:
|
|
101
|
+
|
|
102
|
+
- A dataset of **interview transcripts** featuring the target human
|
|
103
|
+
- A dataset of the **target's past written output** (tweets, essays, etc.)
|
|
104
|
+
|
|
105
|
+
The interview transcripts used within a RAG-inspired process retreiving "memories" from the target's written output for each of the interviewer's questions. These memories are then rephrased and evaluated in the context of the target user's answer and, if found useful, they are interpolated between question and answer for the fine-tuning phase.
|
|
106
|
+
|
|
107
|
+
The steps to reproduce this process are as follows:
|
|
108
|
+
|
|
109
|
+
1. Create a dataset of interview transcripts featuring the target human. Each interview is a separate `data/{name}_transcript_{i}.json` file holding `{"participants": {"q": ..., "a": ...}, "date": ..., "url": ..., "exchanges": [[question, answer], ...]}`. As of 2.0 you don't have to write these by hand: `raft interactive` takes chat-message JSON, ariadne output or plain unstructured transcripts and produces them for you.
|
|
110
|
+
2. Create a dataset of the author's past written output — `data/{name}.jsonl`, one `{"title", "link", "date", "content"}` object per line. `raft fetch` builds this from a substack; `raft tweets` from a tweet archive; `raft interactive` from arbitrary local files.
|
|
111
|
+
3. Split the past output dataset in chunks of a size suitable for the chosen embedding model (8192 tokens for Openai's text-embedding-ada-002), and collect metadata and embeddings for each chunk.
|
|
112
|
+
4. Store the resulting metadata and embeddings in a vector database (we use ChromaDB).
|
|
113
|
+
|
|
114
|
+
Then, in order to generate a fine-tuning dataset:
|
|
115
|
+
|
|
116
|
+
1. For each interview, run the RAG process to retrieve memories from the author's past output for each of the interviewer's questions.
|
|
117
|
+
2. Ask the model to rephrase each memory in the context of the interviewer's question. The same model and prompt will be used in the generation phase.
|
|
118
|
+
3. Evaluate the resulting memory by the question only first, and discard it if it is not considered useful by the model. We apply this first pass separately because, at inference time, we will not have access to the target human's answer.
|
|
119
|
+
4. Save the resulting context including question, memory and as many of the previous [question, memory and answers] tuples as possible, up to the maximum context size the finetune allows, as a new finetune sample.
|
|
120
|
+
|
|
121
|
+
#### Before/after pics (interview/ ft dataset)
|
|
122
|
+
|
|
123
|
+

|
|
124
|
+
|
|
125
|
+
### Generation
|
|
126
|
+
|
|
127
|
+
The fine-tuned model is then used to generate responses to the interviewer's questions. The model is prompted with the question and the rephrased memories, and the resulting response is evaluated using the RATF framework.
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
## Usage and Functionality
|
|
131
|
+
|
|
132
|
+
### Installation
|
|
133
|
+
|
|
134
|
+
The distribution is named `raft-ft` (`raft` was taken on PyPI); the import and the
|
|
135
|
+
CLI are still `raft`. Until the first `raft-ft` release lands on PyPI, install from
|
|
136
|
+
git:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
pip install git+https://github.com/lumpenspace/raft
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Once released:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
pip install raft-ft
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
For development, [uv](https://docs.astral.sh/uv/) manages the environment:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
uv sync --extra dev --extra hf
|
|
152
|
+
uv run pytest
|
|
153
|
+
uv run ruff check .
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Usage
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
raft -h
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The following actions are available:
|
|
165
|
+
|
|
166
|
+
- interactive: Guided end-to-end session: sources, conversations, finetune.
|
|
167
|
+
- tweets: Build a dataset from tweets via ariadne interactive.
|
|
168
|
+
- fetch: Fetch the blog from Substack and store it in the data directory.
|
|
169
|
+
- chunk: Chunk the blog into 4096 token pieces and store them in the data directory.
|
|
170
|
+
- embed: Create embeddings for the chunks and store them.
|
|
171
|
+
- ft:gen: Generate finetune files for the blog.
|
|
172
|
+
- ft:run: Run the finetune job (OpenAI, or huggingface via opbdh).
|
|
173
|
+
- bench:setup: Setup the benchmark for the blog.
|
|
174
|
+
- ask: Ask a question about the blog content.
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Licence
|
|
178
|
+
|
|
179
|
+
MIT
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# The distribution is `raft-ft` because `raft` is taken on PyPI; the import
|
|
7
|
+
# and the CLI are still `raft`.
|
|
8
|
+
name = "raft-ft"
|
|
9
|
+
dynamic = ["version"]
|
|
10
|
+
description = "RAFT: Retrieval-Augmented Fine-Tuning — build persona datasets and finetune models on them."
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
license = "MIT"
|
|
14
|
+
license-files = ["LICENSE"]
|
|
15
|
+
authors = [
|
|
16
|
+
{ name = "lumpenspace" }
|
|
17
|
+
]
|
|
18
|
+
keywords = ["finetuning", "rag", "llm", "persona", "openai", "huggingface"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Environment :: Console",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"beautifulsoup4>=4.12",
|
|
31
|
+
"chromadb>=1.0,<2",
|
|
32
|
+
"openai>=1.14",
|
|
33
|
+
"pandas>=2.2",
|
|
34
|
+
"python-dotenv>=1.0",
|
|
35
|
+
"requests>=2.31",
|
|
36
|
+
"rich>=13.7",
|
|
37
|
+
"tiktoken>=0.6",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/lumpenspace/raft"
|
|
42
|
+
Issues = "https://github.com/lumpenspace/raft/issues"
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
raft = "raft.cli:main"
|
|
46
|
+
|
|
47
|
+
[project.optional-dependencies]
|
|
48
|
+
# Huggingface finetuning on a GPU pod (`raft ft:run` with a non-OpenAI model).
|
|
49
|
+
hf = ["opbdh>=1.3"]
|
|
50
|
+
# Tweet mode also needs ariadne, which is not on PyPI (the name is taken by
|
|
51
|
+
# the GraphQL library): pip install git+https://github.com/lumpenspace/ariadne
|
|
52
|
+
dev = ["build>=1.2", "pytest>=8.0", "ruff>=0.8", "twine>=5.0"]
|
|
53
|
+
|
|
54
|
+
[tool.hatch.version]
|
|
55
|
+
path = "src/raft/__init__.py"
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel]
|
|
58
|
+
packages = ["src/raft"]
|
|
59
|
+
|
|
60
|
+
# Without this, hatchling's sdist default packs the repo root — including the
|
|
61
|
+
# 1.9 MB of sample corpora and chroma databases under data/.
|
|
62
|
+
[tool.hatch.build.targets.sdist]
|
|
63
|
+
include = ["src/raft", "tests", "README.md", "LICENSE"]
|
|
64
|
+
|
|
65
|
+
[tool.pytest.ini_options]
|
|
66
|
+
testpaths = ["tests"]
|
|
67
|
+
|
|
68
|
+
[tool.ruff]
|
|
69
|
+
line-length = 120
|
|
70
|
+
|
|
71
|
+
# Pinned explicitly rather than inherited, mirroring opbdh: ruff's default
|
|
72
|
+
# rule set changes between releases, and `ruff>=0.8` means CI can install any
|
|
73
|
+
# of them. These are ruff's pre-0.16 defaults.
|
|
74
|
+
[tool.ruff.lint]
|
|
75
|
+
select = ["E4", "E7", "E9", "F"]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the chunker function, which splits blog posts into chunks
|
|
3
|
+
of a maximum length.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from typing import List, Dict, Tuple, Generator
|
|
8
|
+
import tiktoken
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
MAX_EMBEDDING_LENGTH = 2048
|
|
12
|
+
encoding = tiktoken.encoding_for_model("gpt-3.5-turbo")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def map_line(line: str) -> Tuple[str, str, int]:
|
|
16
|
+
"""
|
|
17
|
+
Encode and potentially truncate a line of text.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
line (str): The input line of text.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Tuple[str, str, int]: A tuple containing:
|
|
24
|
+
- the original or truncated line,
|
|
25
|
+
- the decoded version, and
|
|
26
|
+
- the length of the encoded line.
|
|
27
|
+
"""
|
|
28
|
+
encoded = encoding.encode(line)
|
|
29
|
+
if len(encoded) < MAX_EMBEDDING_LENGTH:
|
|
30
|
+
return (line, encoding.decode(encoded), len(encoded))
|
|
31
|
+
else:
|
|
32
|
+
truncated_encoded = encoded[:MAX_EMBEDDING_LENGTH]
|
|
33
|
+
return (
|
|
34
|
+
line[:MAX_EMBEDDING_LENGTH],
|
|
35
|
+
encoding.decode(truncated_encoded),
|
|
36
|
+
MAX_EMBEDDING_LENGTH,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def split_into_chunks(
|
|
41
|
+
blog_posts: pd.DataFrame,
|
|
42
|
+
) -> Generator[Tuple[Dict[str, str], str], None, None]:
|
|
43
|
+
"""
|
|
44
|
+
Split blog posts into chunks of a maximum length.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
blog_posts (pd.DataFrame): DataFrame containing blog post data.
|
|
48
|
+
|
|
49
|
+
Yields:
|
|
50
|
+
Tuple[Dict[str, str], str]: A tuple containing metadata
|
|
51
|
+
and the chunked content.
|
|
52
|
+
"""
|
|
53
|
+
for _, post in blog_posts.iterrows():
|
|
54
|
+
print(f"Splitting {post['title']}")
|
|
55
|
+
lines = list(map(map_line, post["content"].split("\n")))
|
|
56
|
+
|
|
57
|
+
# Calculate total parts
|
|
58
|
+
total_parts = 0
|
|
59
|
+
temp_chunk_length = 0
|
|
60
|
+
for _, _, length in lines:
|
|
61
|
+
if temp_chunk_length + length > MAX_EMBEDDING_LENGTH:
|
|
62
|
+
total_parts += 1
|
|
63
|
+
temp_chunk_length = length
|
|
64
|
+
else:
|
|
65
|
+
temp_chunk_length += length
|
|
66
|
+
total_parts += 1 # for the last chunk
|
|
67
|
+
|
|
68
|
+
chunk: List[str] = []
|
|
69
|
+
chunk_length = 0
|
|
70
|
+
part = 1
|
|
71
|
+
|
|
72
|
+
for line, _, length in lines:
|
|
73
|
+
if chunk_length + length > MAX_EMBEDDING_LENGTH:
|
|
74
|
+
metadata = {
|
|
75
|
+
"title": post["title"],
|
|
76
|
+
"url": post["link"],
|
|
77
|
+
"date": (
|
|
78
|
+
post["date"].isoformat()
|
|
79
|
+
if isinstance(post["date"], pd.Timestamp)
|
|
80
|
+
else post["date"]
|
|
81
|
+
),
|
|
82
|
+
"total_parts": str(total_parts),
|
|
83
|
+
"part": str(part),
|
|
84
|
+
}
|
|
85
|
+
yield metadata, "\n".join(chunk)
|
|
86
|
+
chunk = []
|
|
87
|
+
chunk_length = 0
|
|
88
|
+
part += 1
|
|
89
|
+
chunk.append(line)
|
|
90
|
+
chunk_length += length
|
|
91
|
+
|
|
92
|
+
if chunk:
|
|
93
|
+
metadata = {
|
|
94
|
+
"title": post["title"],
|
|
95
|
+
"url": post["link"],
|
|
96
|
+
"date": (
|
|
97
|
+
post["date"].isoformat()
|
|
98
|
+
if isinstance(post["date"], pd.Timestamp)
|
|
99
|
+
else post["date"]
|
|
100
|
+
),
|
|
101
|
+
"total_parts": str(total_parts),
|
|
102
|
+
"part": str(part),
|
|
103
|
+
}
|
|
104
|
+
yield metadata, "\n".join(chunk)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def chunker(name: str) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Process a JSONL file of blog posts, split them into chunks,
|
|
110
|
+
and save the results.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
name (str): The name of the file to process (without extension).
|
|
114
|
+
"""
|
|
115
|
+
sourcefile = f"data/{name}.jsonl"
|
|
116
|
+
outputfile = f"data/{name}_chunked.jsonl"
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
# Read the JSONL file line by line
|
|
120
|
+
with open(sourcefile, "r") as f:
|
|
121
|
+
data = [json.loads(line) for line in f]
|
|
122
|
+
|
|
123
|
+
blog_posts: pd.DataFrame = pd.DataFrame(data)
|
|
124
|
+
print(f"Splitting {len(blog_posts)} blog posts into chunks")
|
|
125
|
+
|
|
126
|
+
with open(outputfile, "w") as f:
|
|
127
|
+
for item in split_into_chunks(blog_posts):
|
|
128
|
+
f.write(json.dumps(item) + "\n")
|
|
129
|
+
|
|
130
|
+
except json.JSONDecodeError as e:
|
|
131
|
+
print(f"Error decoding JSON: {e}")
|
|
132
|
+
print(f"Error occurred at line {e.lineno}")
|
|
133
|
+
with open(sourcefile, "r") as f:
|
|
134
|
+
problematic_line = f.readlines()[e.lineno - 1]
|
|
135
|
+
print(f"Problematic line: {problematic_line}")
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
print(f"An error occurred: {e}")
|