feedloop 0.1.1__tar.gz → 0.1.2__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.
- feedloop-0.1.2/HOWTO.md +302 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/PKG-INFO +1 -1
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/App.tsx +14 -7
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/api.ts +19 -6
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/components/ExportPanel.tsx +9 -5
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/hooks/useComparison.ts +3 -3
- {feedloop-0.1.1 → feedloop-0.1.2}/pyproject.toml +1 -1
- {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/__init__.py +2 -2
- {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/_routes.py +12 -1
- {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/_server.py +5 -4
- feedloop-0.1.1/src/feedloop/_static/assets/index-BMXdI_V0.js → feedloop-0.1.2/src/feedloop/_static/assets/index-VGrg1Km3.js +18 -18
- {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/_static/index.html +1 -1
- {feedloop-0.1.1 → feedloop-0.1.2}/.claude/settings.local.json +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/.gitignore +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/LICENSE +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/Makefile +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/PLAN.md +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/README.md +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/demo.py +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/index.html +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/package-lock.json +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/package.json +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/components/ComparisonView.tsx +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/components/EmptyState.tsx +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/components/FeedbackBar.tsx +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/components/ProgressBar.tsx +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/components/PromptHeader.tsx +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/components/ResponseCard.tsx +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/main.tsx +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/styles.css +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/vite-env.d.ts +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/tsconfig.json +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/tsconfig.tsbuildinfo +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/frontend/vite.config.ts +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/preferences.jsonl +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/__main__.py +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/_config.py +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/_db.py +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/_export.py +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/_models.py +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/_static/assets/index-CNg3lXzh.css +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/py.typed +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/tests/__init__.py +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/tests/test_db.py +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/tests/test_export.py +0 -0
- {feedloop-0.1.1 → feedloop-0.1.2}/tests/test_routes.py +0 -0
feedloop-0.1.2/HOWTO.md
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# feedloop — How-To Guide
|
|
2
|
+
|
|
3
|
+
> The fastest way to collect human preference data for LLMs.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What is feedloop?
|
|
8
|
+
|
|
9
|
+
feedloop is a lightweight developer tool that lets you collect **human preference feedback** on LLM outputs directly from your Python code. You submit pairs of model responses, a human reviews them in a browser UI, and feedloop exports the results in **DPO-ready JSONL format** — ready to use for fine-tuning or evaluation.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install feedloop
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Requirements:** Python 3.10+
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Quickstart (5 minutes)
|
|
24
|
+
|
|
25
|
+
### 1. Import and start the server
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import feedloop
|
|
29
|
+
|
|
30
|
+
feedloop.start()
|
|
31
|
+
# feedloop: server running at http://localhost:7777
|
|
32
|
+
# (browser opens automatically)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This launches a local web server in the background and opens the review UI in your browser. Your script keeps running normally.
|
|
36
|
+
|
|
37
|
+
### 2. Submit a comparison
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
comparison_id = feedloop.compare(
|
|
41
|
+
prompt="Explain quantum entanglement in simple terms.",
|
|
42
|
+
outputs=[
|
|
43
|
+
"Quantum entanglement is when two particles become linked...", # Output A
|
|
44
|
+
"Imagine two magic coins that always land on opposite sides...", # Output B
|
|
45
|
+
]
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The comparison immediately appears in the browser UI for a human to review.
|
|
50
|
+
|
|
51
|
+
### 3. (Optional) Wait for feedback
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# Block until this specific comparison is rated
|
|
55
|
+
result = feedloop.wait(comparison_id)
|
|
56
|
+
|
|
57
|
+
print(result["chosen"]) # the preferred output
|
|
58
|
+
print(result["rejected"]) # the other output
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or wait for **all** pending comparisons to be rated:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
feedloop.wait() # blocks until inbox is empty
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 4. Export your data
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
count = feedloop.export("my_preferences.jsonl")
|
|
71
|
+
# feedloop: exported 42 preferences to my_preferences.jsonl
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## The Review UI
|
|
77
|
+
|
|
78
|
+
When you call `feedloop.start()`, a browser tab opens at `http://localhost:7777`.
|
|
79
|
+
|
|
80
|
+
- **Left / Right buttons** — click to choose the preferred output
|
|
81
|
+
- **Skip** — skip a comparison if neither output is clearly better
|
|
82
|
+
- **Progress bar** — shows how many comparisons remain in the session
|
|
83
|
+
|
|
84
|
+
The UI polls automatically — new comparisons appear in real time as your script submits them.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Exported Data Format
|
|
89
|
+
|
|
90
|
+
feedloop exports in **DPO (Direct Preference Optimization)** format — one JSON object per line:
|
|
91
|
+
|
|
92
|
+
```jsonl
|
|
93
|
+
{"prompt": "Explain quantum entanglement...", "chosen": "Imagine two magic coins...", "rejected": "Quantum entanglement is when two particles..."}
|
|
94
|
+
{"prompt": "Write a haiku about autumn.", "chosen": "Leaves fall silently...", "rejected": "Autumn brings cool air..."}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This format is directly compatible with:
|
|
98
|
+
- [TRL](https://github.com/huggingface/trl) `DPOTrainer`
|
|
99
|
+
- [OpenRLHF](https://github.com/OpenRLHF/OpenRLHF)
|
|
100
|
+
- Any custom fine-tuning pipeline that accepts preference pairs
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Full API Reference
|
|
105
|
+
|
|
106
|
+
### `feedloop.start(port, db_path, open_browser)`
|
|
107
|
+
|
|
108
|
+
Launches the feedback server. Safe to call multiple times — idempotent.
|
|
109
|
+
|
|
110
|
+
| Parameter | Type | Default | Description |
|
|
111
|
+
|---|---|---|---|
|
|
112
|
+
| `port` | `int` | `7777` | Port to run the server on |
|
|
113
|
+
| `db_path` | `str \| None` | `None` | Path to SQLite DB file. Uses a temp file if not set |
|
|
114
|
+
| `open_browser` | `bool` | `True` | Auto-open browser on start |
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
feedloop.start(port=8080, db_path="./feedback.db", open_browser=False)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### `feedloop.compare(prompt, outputs, metadata)`
|
|
123
|
+
|
|
124
|
+
Submits a pair of outputs for human review. Returns a `comparison_id` string.
|
|
125
|
+
|
|
126
|
+
| Parameter | Type | Required | Description |
|
|
127
|
+
|---|---|---|---|
|
|
128
|
+
| `prompt` | `str` | ✅ | The input prompt shown to the reviewer |
|
|
129
|
+
| `outputs` | `list[str]` | ✅ | Exactly 2 model outputs to compare |
|
|
130
|
+
| `metadata` | `dict \| None` | ❌ | Optional key/value data attached to the record |
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
cid = feedloop.compare(
|
|
134
|
+
prompt="Summarise this article in one sentence.",
|
|
135
|
+
outputs=[response_from_model_a, response_from_model_b],
|
|
136
|
+
metadata={"model_a": "gpt-4o", "model_b": "claude-3-5-sonnet"},
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### `feedloop.wait(comparison_id, timeout)`
|
|
143
|
+
|
|
144
|
+
Blocks until a comparison is rated (or all pending comparisons are done).
|
|
145
|
+
|
|
146
|
+
| Parameter | Type | Default | Description |
|
|
147
|
+
|---|---|---|---|
|
|
148
|
+
| `comparison_id` | `str \| None` | `None` | Wait for a specific comparison. If `None`, waits for all pending |
|
|
149
|
+
| `timeout` | `float \| None` | `None` | Max seconds to wait. Returns `None` on timeout |
|
|
150
|
+
|
|
151
|
+
**Return value (single comparison):**
|
|
152
|
+
```python
|
|
153
|
+
{"prompt": "...", "chosen": "...", "rejected": "..."}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Return value (wait for all):**
|
|
157
|
+
```python
|
|
158
|
+
{"completed": 42, "total": 42}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
### `feedloop.status()`
|
|
164
|
+
|
|
165
|
+
Returns a live count of comparisons in the current session.
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
feedloop.status()
|
|
169
|
+
# {"pending": 3, "completed": 10, "skipped": 1, "total": 14}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
### `feedloop.export(path, format)`
|
|
175
|
+
|
|
176
|
+
Exports all completed comparisons to a JSONL file.
|
|
177
|
+
|
|
178
|
+
| Parameter | Type | Default | Description |
|
|
179
|
+
|---|---|---|---|
|
|
180
|
+
| `path` | `str` | `"preferences.jsonl"` | Output file path |
|
|
181
|
+
| `format` | `str` | `"dpo"` | Export format. Currently `"dpo"` only |
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
feedloop.export("data/run_1_preferences.jsonl")
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### `feedloop.stop()`
|
|
190
|
+
|
|
191
|
+
Shuts down the server and closes the database. Called automatically on script exit.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
feedloop.stop()
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Running the Server Standalone (CLI)
|
|
200
|
+
|
|
201
|
+
You can run feedloop as a standalone server without embedding it in a script:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
feedloop
|
|
205
|
+
# feedloop: server running at http://localhost:7777
|
|
206
|
+
# feedloop: Press Ctrl+C to stop
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**CLI options:**
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
feedloop --port 8080 # use a different port
|
|
213
|
+
feedloop --db ./feedback.db # persist data to a specific file
|
|
214
|
+
feedloop --no-browser # don't auto-open the browser
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
This is useful when you want to keep the UI open across multiple script runs, or when running feedloop on a remote machine.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Complete Example: Comparing Two Models
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
import feedloop
|
|
225
|
+
from openai import OpenAI
|
|
226
|
+
|
|
227
|
+
client = OpenAI()
|
|
228
|
+
|
|
229
|
+
prompts = [
|
|
230
|
+
"What is the capital of France?",
|
|
231
|
+
"Explain recursion to a 10-year-old.",
|
|
232
|
+
"Write a one-line poem about the ocean.",
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
feedloop.start(db_path="session.db")
|
|
236
|
+
|
|
237
|
+
for prompt in prompts:
|
|
238
|
+
response_a = client.chat.completions.create(
|
|
239
|
+
model="gpt-4o-mini",
|
|
240
|
+
messages=[{"role": "user", "content": prompt}],
|
|
241
|
+
).choices[0].message.content
|
|
242
|
+
|
|
243
|
+
response_b = client.chat.completions.create(
|
|
244
|
+
model="gpt-4o",
|
|
245
|
+
messages=[{"role": "user", "content": prompt}],
|
|
246
|
+
).choices[0].message.content
|
|
247
|
+
|
|
248
|
+
feedloop.compare(
|
|
249
|
+
prompt=prompt,
|
|
250
|
+
outputs=[response_a, response_b],
|
|
251
|
+
metadata={"model_a": "gpt-4o-mini", "model_b": "gpt-4o"},
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
print(f"Submitted {len(prompts)} comparisons — review them in the browser.")
|
|
255
|
+
|
|
256
|
+
# Wait for all feedback, then export
|
|
257
|
+
feedloop.wait()
|
|
258
|
+
feedloop.export("gpt4o_vs_mini.jsonl")
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Persisting Data Across Sessions
|
|
264
|
+
|
|
265
|
+
By default, feedloop stores data in a temporary SQLite file that is deleted when the server stops. To keep your data:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
feedloop.start(db_path="./my_project_feedback.db")
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The `.db` file will persist across runs. You can export from it at any time — even from a different script:
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
feedloop.start(db_path="./my_project_feedback.db", open_browser=False)
|
|
275
|
+
feedloop.export("all_preferences.jsonl")
|
|
276
|
+
feedloop.stop()
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## FAQ
|
|
282
|
+
|
|
283
|
+
**Q: Can I submit comparisons without waiting for them to be reviewed?**
|
|
284
|
+
Yes. `feedloop.compare()` is non-blocking. You can submit all your comparisons up front and review them in the UI at your own pace.
|
|
285
|
+
|
|
286
|
+
**Q: What happens if I skip a comparison?**
|
|
287
|
+
Skipped comparisons are excluded from the export. They count toward `skipped` in `feedloop.status()`.
|
|
288
|
+
|
|
289
|
+
**Q: Can I use feedloop without opening a browser?**
|
|
290
|
+
Yes. Pass `open_browser=False` to `feedloop.start()`. The UI is still available at `http://localhost:7777` whenever you want it.
|
|
291
|
+
|
|
292
|
+
**Q: Does feedloop work in Jupyter notebooks?**
|
|
293
|
+
Yes. Call `feedloop.start()` in one cell and `feedloop.compare()` in subsequent cells.
|
|
294
|
+
|
|
295
|
+
**Q: Is my data sent anywhere?**
|
|
296
|
+
No. feedloop runs entirely on your local machine. No data leaves your environment.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Links
|
|
301
|
+
|
|
302
|
+
- **PyPI:** https://pypi.org/project/feedloop/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: feedloop
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: The fastest way to collect human preference data for LLMs
|
|
5
5
|
Project-URL: Homepage, https://github.com/rammuthiah/feedloop
|
|
6
6
|
Project-URL: Repository, https://github.com/rammuthiah/feedloop
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
|
-
import { fetchStats } from "./api";
|
|
2
|
+
import { fetchSession, fetchStats } from "./api";
|
|
3
3
|
import { useComparison } from "./hooks/useComparison";
|
|
4
4
|
import ComparisonView from "./components/ComparisonView";
|
|
5
5
|
import EmptyState from "./components/EmptyState";
|
|
6
6
|
import ExportPanel from "./components/ExportPanel";
|
|
7
7
|
|
|
8
8
|
export default function App() {
|
|
9
|
-
const
|
|
10
|
-
|
|
9
|
+
const [sessionId, setSessionId] = useState<string | null>(null);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
fetchSession().then(setSessionId).catch(() => {});
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
15
|
+
const { comparison, loading, refresh } = useComparison(sessionId);
|
|
16
|
+
const doneState = useCheckDone(comparison, loading, sessionId);
|
|
11
17
|
|
|
12
18
|
return (
|
|
13
19
|
<div className="app">
|
|
@@ -22,7 +28,7 @@ export default function App() {
|
|
|
22
28
|
) : comparison ? (
|
|
23
29
|
<ComparisonView comparison={comparison} onComplete={refresh} />
|
|
24
30
|
) : doneState === "done" ? (
|
|
25
|
-
<ExportPanel />
|
|
31
|
+
<ExportPanel sessionId={sessionId} />
|
|
26
32
|
) : (
|
|
27
33
|
<EmptyState />
|
|
28
34
|
)}
|
|
@@ -33,14 +39,15 @@ export default function App() {
|
|
|
33
39
|
|
|
34
40
|
function useCheckDone(
|
|
35
41
|
comparison: ReturnType<typeof useComparison>["comparison"],
|
|
36
|
-
loading: boolean
|
|
42
|
+
loading: boolean,
|
|
43
|
+
sessionId: string | null
|
|
37
44
|
): "done" | "empty" | "loading" {
|
|
38
45
|
const [state, setState] = useState<"done" | "empty" | "loading">("loading");
|
|
39
46
|
|
|
40
47
|
useEffect(() => {
|
|
41
48
|
if (loading || comparison) return;
|
|
42
49
|
|
|
43
|
-
fetchStats()
|
|
50
|
+
fetchStats(sessionId)
|
|
44
51
|
.then((stats) => {
|
|
45
52
|
if (stats.total > 0 && stats.pending === 0) {
|
|
46
53
|
setState("done");
|
|
@@ -49,7 +56,7 @@ function useCheckDone(
|
|
|
49
56
|
}
|
|
50
57
|
})
|
|
51
58
|
.catch(() => setState("empty"));
|
|
52
|
-
}, [comparison, loading]);
|
|
59
|
+
}, [comparison, loading, sessionId]);
|
|
53
60
|
|
|
54
61
|
return state;
|
|
55
62
|
}
|
|
@@ -16,8 +16,18 @@ export interface Stats {
|
|
|
16
16
|
|
|
17
17
|
const BASE = "/api";
|
|
18
18
|
|
|
19
|
-
export async function
|
|
20
|
-
const res = await fetch(`${BASE}/
|
|
19
|
+
export async function fetchSession(): Promise<string | null> {
|
|
20
|
+
const res = await fetch(`${BASE}/session`);
|
|
21
|
+
if (!res.ok) return null;
|
|
22
|
+
const data = await res.json();
|
|
23
|
+
return data.session_id ?? null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function fetchPending(sessionId: string | null): Promise<Comparison | null> {
|
|
27
|
+
const url = sessionId
|
|
28
|
+
? `${BASE}/pending?session_id=${sessionId}`
|
|
29
|
+
: `${BASE}/pending`;
|
|
30
|
+
const res = await fetch(url);
|
|
21
31
|
if (!res.ok) throw new Error(`Failed to fetch pending: ${res.status}`);
|
|
22
32
|
const data = await res.json();
|
|
23
33
|
return data;
|
|
@@ -38,12 +48,15 @@ export async function submitFeedback(
|
|
|
38
48
|
if (!res.ok) throw new Error(`Failed to submit feedback: ${res.status}`);
|
|
39
49
|
}
|
|
40
50
|
|
|
41
|
-
export async function fetchStats(): Promise<Stats> {
|
|
42
|
-
const
|
|
51
|
+
export async function fetchStats(sessionId: string | null): Promise<Stats> {
|
|
52
|
+
const url = sessionId
|
|
53
|
+
? `${BASE}/stats?session_id=${sessionId}`
|
|
54
|
+
: `${BASE}/stats`;
|
|
55
|
+
const res = await fetch(url);
|
|
43
56
|
if (!res.ok) throw new Error(`Failed to fetch stats: ${res.status}`);
|
|
44
57
|
return res.json();
|
|
45
58
|
}
|
|
46
59
|
|
|
47
|
-
export function getExportUrl(): string {
|
|
48
|
-
return `${BASE}/export`;
|
|
60
|
+
export function getExportUrl(sessionId: string | null): string {
|
|
61
|
+
return sessionId ? `${BASE}/export?session_id=${sessionId}` : `${BASE}/export`;
|
|
49
62
|
}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
2
|
import { fetchStats, getExportUrl, type Stats } from "../api";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
interface ExportPanelProps {
|
|
5
|
+
sessionId: string | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function ExportPanel({ sessionId }: ExportPanelProps) {
|
|
5
9
|
const [stats, setStats] = useState<Stats | null>(null);
|
|
6
10
|
|
|
7
11
|
useEffect(() => {
|
|
8
|
-
fetchStats().then(setStats).catch(() => {});
|
|
12
|
+
fetchStats(sessionId).then(setStats).catch(() => {});
|
|
9
13
|
const interval = setInterval(() => {
|
|
10
|
-
fetchStats().then(setStats).catch(() => {});
|
|
14
|
+
fetchStats(sessionId).then(setStats).catch(() => {});
|
|
11
15
|
}, 2000);
|
|
12
16
|
return () => clearInterval(interval);
|
|
13
|
-
}, []);
|
|
17
|
+
}, [sessionId]);
|
|
14
18
|
|
|
15
19
|
return (
|
|
16
20
|
<div className="export-panel">
|
|
@@ -22,7 +26,7 @@ export default function ExportPanel() {
|
|
|
22
26
|
)}
|
|
23
27
|
<a
|
|
24
28
|
className="btn btn-export"
|
|
25
|
-
href={getExportUrl()}
|
|
29
|
+
href={getExportUrl(sessionId)}
|
|
26
30
|
download="preferences.jsonl"
|
|
27
31
|
>
|
|
28
32
|
Download JSONL
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { useEffect, useState, useCallback } from "react";
|
|
2
2
|
import { fetchPending, type Comparison } from "../api";
|
|
3
3
|
|
|
4
|
-
export function useComparison() {
|
|
4
|
+
export function useComparison(sessionId: string | null) {
|
|
5
5
|
const [comparison, setComparison] = useState<Comparison | null>(null);
|
|
6
6
|
const [loading, setLoading] = useState(true);
|
|
7
7
|
|
|
8
8
|
const poll = useCallback(async () => {
|
|
9
9
|
try {
|
|
10
|
-
const data = await fetchPending();
|
|
10
|
+
const data = await fetchPending(sessionId);
|
|
11
11
|
setComparison(data);
|
|
12
12
|
} catch {
|
|
13
13
|
// server may not be ready yet
|
|
14
14
|
} finally {
|
|
15
15
|
setLoading(false);
|
|
16
16
|
}
|
|
17
|
-
}, []);
|
|
17
|
+
}, [sessionId]);
|
|
18
18
|
|
|
19
19
|
useEffect(() => {
|
|
20
20
|
poll();
|
|
@@ -12,7 +12,7 @@ from ._db import Database
|
|
|
12
12
|
from ._export import export_dpo
|
|
13
13
|
from ._server import BackgroundServer
|
|
14
14
|
|
|
15
|
-
__version__ = "0.1.
|
|
15
|
+
__version__ = "0.1.2"
|
|
16
16
|
|
|
17
17
|
# ── module-level state ──────────────────────────────────────
|
|
18
18
|
|
|
@@ -46,7 +46,7 @@ def start(
|
|
|
46
46
|
|
|
47
47
|
_session_id = uuid.uuid4().hex
|
|
48
48
|
_db = Database(db_path=db_path)
|
|
49
|
-
_server = BackgroundServer(_db, port=port)
|
|
49
|
+
_server = BackgroundServer(_db, session_id=_session_id, port=port)
|
|
50
50
|
_server.start()
|
|
51
51
|
|
|
52
52
|
print(f"feedloop: server running at {_server.url}")
|
|
@@ -11,8 +11,9 @@ from ._models import ComparisonOut, FeedbackRequest, StatsResponse
|
|
|
11
11
|
|
|
12
12
|
router = APIRouter(prefix="/api")
|
|
13
13
|
|
|
14
|
-
# The database
|
|
14
|
+
# The database and session_id are set by _server.py at startup.
|
|
15
15
|
_db: Database | None = None
|
|
16
|
+
_session_id: str | None = None
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def set_db(db: Database) -> None:
|
|
@@ -20,12 +21,22 @@ def set_db(db: Database) -> None:
|
|
|
20
21
|
_db = db
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def set_session_id(session_id: str) -> None:
|
|
25
|
+
global _session_id
|
|
26
|
+
_session_id = session_id
|
|
27
|
+
|
|
28
|
+
|
|
23
29
|
def _get_db() -> Database:
|
|
24
30
|
if _db is None:
|
|
25
31
|
raise RuntimeError("Database not initialised")
|
|
26
32
|
return _db
|
|
27
33
|
|
|
28
34
|
|
|
35
|
+
@router.get("/session")
|
|
36
|
+
def get_session() -> dict:
|
|
37
|
+
return {"session_id": _session_id}
|
|
38
|
+
|
|
39
|
+
|
|
29
40
|
@router.get("/pending")
|
|
30
41
|
def get_pending(session_id: str | None = None) -> ComparisonOut | None:
|
|
31
42
|
db = _get_db()
|
|
@@ -11,12 +11,12 @@ from fastapi.staticfiles import StaticFiles
|
|
|
11
11
|
|
|
12
12
|
from ._config import DEFAULT_PORT, PORT_SCAN_RANGE
|
|
13
13
|
from ._db import Database
|
|
14
|
-
from ._routes import router, set_db
|
|
14
|
+
from ._routes import router, set_db, set_session_id
|
|
15
15
|
|
|
16
16
|
STATIC_DIR = Path(__file__).parent / "_static"
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def _create_app(db: Database) -> FastAPI:
|
|
19
|
+
def _create_app(db: Database, session_id: str) -> FastAPI:
|
|
20
20
|
app = FastAPI(title="feedloop", docs_url=None, redoc_url=None)
|
|
21
21
|
|
|
22
22
|
app.add_middleware(
|
|
@@ -27,6 +27,7 @@ def _create_app(db: Database) -> FastAPI:
|
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
set_db(db)
|
|
30
|
+
set_session_id(session_id)
|
|
30
31
|
app.include_router(router)
|
|
31
32
|
|
|
32
33
|
# Serve React static files (only if built)
|
|
@@ -51,10 +52,10 @@ def _find_open_port(start: int) -> int:
|
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
class BackgroundServer:
|
|
54
|
-
def __init__(self, db: Database, port: int = DEFAULT_PORT) -> None:
|
|
55
|
+
def __init__(self, db: Database, session_id: str, port: int = DEFAULT_PORT) -> None:
|
|
55
56
|
self.db = db
|
|
56
57
|
self.port = _find_open_port(port)
|
|
57
|
-
self.app = _create_app(db)
|
|
58
|
+
self.app = _create_app(db, session_id)
|
|
58
59
|
self._thread: threading.Thread | None = None
|
|
59
60
|
self._server: uvicorn.Server | None = None
|
|
60
61
|
|