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.
Files changed (46) hide show
  1. feedloop-0.1.2/HOWTO.md +302 -0
  2. {feedloop-0.1.1 → feedloop-0.1.2}/PKG-INFO +1 -1
  3. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/App.tsx +14 -7
  4. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/api.ts +19 -6
  5. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/components/ExportPanel.tsx +9 -5
  6. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/hooks/useComparison.ts +3 -3
  7. {feedloop-0.1.1 → feedloop-0.1.2}/pyproject.toml +1 -1
  8. {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/__init__.py +2 -2
  9. {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/_routes.py +12 -1
  10. {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/_server.py +5 -4
  11. 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
  12. {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/_static/index.html +1 -1
  13. {feedloop-0.1.1 → feedloop-0.1.2}/.claude/settings.local.json +0 -0
  14. {feedloop-0.1.1 → feedloop-0.1.2}/.gitignore +0 -0
  15. {feedloop-0.1.1 → feedloop-0.1.2}/LICENSE +0 -0
  16. {feedloop-0.1.1 → feedloop-0.1.2}/Makefile +0 -0
  17. {feedloop-0.1.1 → feedloop-0.1.2}/PLAN.md +0 -0
  18. {feedloop-0.1.1 → feedloop-0.1.2}/README.md +0 -0
  19. {feedloop-0.1.1 → feedloop-0.1.2}/demo.py +0 -0
  20. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/index.html +0 -0
  21. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/package-lock.json +0 -0
  22. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/package.json +0 -0
  23. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/components/ComparisonView.tsx +0 -0
  24. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/components/EmptyState.tsx +0 -0
  25. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/components/FeedbackBar.tsx +0 -0
  26. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/components/ProgressBar.tsx +0 -0
  27. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/components/PromptHeader.tsx +0 -0
  28. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/components/ResponseCard.tsx +0 -0
  29. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/main.tsx +0 -0
  30. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/styles.css +0 -0
  31. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/src/vite-env.d.ts +0 -0
  32. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/tsconfig.json +0 -0
  33. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/tsconfig.tsbuildinfo +0 -0
  34. {feedloop-0.1.1 → feedloop-0.1.2}/frontend/vite.config.ts +0 -0
  35. {feedloop-0.1.1 → feedloop-0.1.2}/preferences.jsonl +0 -0
  36. {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/__main__.py +0 -0
  37. {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/_config.py +0 -0
  38. {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/_db.py +0 -0
  39. {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/_export.py +0 -0
  40. {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/_models.py +0 -0
  41. {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/_static/assets/index-CNg3lXzh.css +0 -0
  42. {feedloop-0.1.1 → feedloop-0.1.2}/src/feedloop/py.typed +0 -0
  43. {feedloop-0.1.1 → feedloop-0.1.2}/tests/__init__.py +0 -0
  44. {feedloop-0.1.1 → feedloop-0.1.2}/tests/test_db.py +0 -0
  45. {feedloop-0.1.1 → feedloop-0.1.2}/tests/test_export.py +0 -0
  46. {feedloop-0.1.1 → feedloop-0.1.2}/tests/test_routes.py +0 -0
@@ -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.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 { comparison, loading, refresh } = useComparison();
10
- const doneState = useCheckDone(comparison, loading);
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 fetchPending(): Promise<Comparison | null> {
20
- const res = await fetch(`${BASE}/pending`);
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 res = await fetch(`${BASE}/stats`);
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
- export default function ExportPanel() {
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();
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "feedloop"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "The fastest way to collect human preference data for LLMs"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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.0"
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 instance is set by _server.py at startup.
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