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.
@@ -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
+ ![](https://github.com/lumpenspace/raft/blob/main/Screenshot%202024-03-18%20at%2021.05.39.png?raw=true)
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
@@ -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
+ ![](https://github.com/lumpenspace/raft/blob/main/Screenshot%202024-03-18%20at%2021.05.39.png?raw=true)
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,9 @@
1
+ """RAFT: Retrieval-Augmented Fine-Tuning."""
2
+
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ __all__ = ["__version__"]
8
+
9
+ __version__ = "2.2.0"
@@ -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}")