flowent 0.0.11 → 0.0.13

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 (53) hide show
  1. package/README.md +14 -0
  2. package/backend/README.md +14 -0
  3. package/backend/pyproject.toml +1 -1
  4. package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
  5. package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
  6. package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
  7. package/backend/src/flowent/__pycache__/channels.cpython-313.pyc +0 -0
  8. package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
  9. package/backend/src/flowent/__pycache__/context.cpython-313.pyc +0 -0
  10. package/backend/src/flowent/__pycache__/llm.cpython-313.pyc +0 -0
  11. package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
  12. package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
  13. package/backend/src/flowent/__pycache__/patch.cpython-313.pyc +0 -0
  14. package/backend/src/flowent/__pycache__/paths.cpython-313.pyc +0 -0
  15. package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
  16. package/backend/src/flowent/__pycache__/storage.cpython-313.pyc +0 -0
  17. package/backend/src/flowent/__pycache__/tools.cpython-313.pyc +0 -0
  18. package/backend/src/flowent/agent.py +15 -0
  19. package/backend/src/flowent/channels.py +296 -0
  20. package/backend/src/flowent/cli.py +11 -0
  21. package/backend/src/flowent/llm.py +49 -1
  22. package/backend/src/flowent/main.py +143 -2
  23. package/backend/src/flowent/sandbox.py +18 -3
  24. package/backend/src/flowent/static/assets/index-CEZrWoDG.css +2 -0
  25. package/backend/src/flowent/static/assets/index-S5a0Rkj1.js +81 -0
  26. package/backend/src/flowent/static/index.html +2 -2
  27. package/backend/src/flowent/storage.py +217 -8
  28. package/backend/src/flowent/tools.py +28 -5
  29. package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  30. package/backend/tests/__pycache__/test_agent_tools.cpython-313-pytest-9.0.3.pyc +0 -0
  31. package/backend/tests/__pycache__/test_channels.cpython-313-pytest-9.0.3.pyc +0 -0
  32. package/backend/tests/__pycache__/test_health.cpython-313-pytest-9.0.3.pyc +0 -0
  33. package/backend/tests/__pycache__/test_llm_providers.cpython-313-pytest-9.0.3.pyc +0 -0
  34. package/backend/tests/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
  35. package/backend/tests/__pycache__/test_persistence.cpython-313-pytest-9.0.3.pyc +0 -0
  36. package/backend/tests/__pycache__/test_startup_requirements.cpython-313-pytest-9.0.3.pyc +0 -0
  37. package/backend/tests/__pycache__/test_workspace_chat.cpython-313-pytest-9.0.3.pyc +0 -0
  38. package/backend/tests/conftest.py +21 -0
  39. package/backend/tests/test_agent_tools.py +99 -0
  40. package/backend/tests/test_channels.py +360 -0
  41. package/backend/tests/test_llm_providers.py +58 -0
  42. package/backend/tests/test_persistence.py +46 -0
  43. package/backend/tests/test_startup_requirements.py +48 -0
  44. package/backend/tests/test_workspace_chat.py +28 -0
  45. package/backend/uv.lock +1 -1
  46. package/dist/frontend/assets/index-CEZrWoDG.css +2 -0
  47. package/dist/frontend/assets/index-S5a0Rkj1.js +81 -0
  48. package/dist/frontend/index.html +2 -2
  49. package/package.json +1 -1
  50. package/backend/src/flowent/static/assets/index-C76K95ty.js +0 -81
  51. package/backend/src/flowent/static/assets/index-iUMNKvlU.css +0 -2
  52. package/dist/frontend/assets/index-C76K95ty.js +0 -81
  53. package/dist/frontend/assets/index-iUMNKvlU.css +0 -2
@@ -0,0 +1,360 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ import pytest
7
+ from fastapi.testclient import TestClient
8
+
9
+ from flowent.channels import TelegramBotManager, split_telegram_message
10
+ from flowent.main import create_app
11
+ from flowent.storage import StateStore, StoredTelegramBot, StoredTelegramSession
12
+
13
+
14
+ class FakeTelegramTransport:
15
+ def __init__(self) -> None:
16
+ self.sent_messages: list[dict[str, str]] = []
17
+ self.updates: list[dict[str, Any]] = []
18
+ self.error: Exception | None = None
19
+ self.get_updates_calls = 0
20
+
21
+ async def get_updates(
22
+ self,
23
+ *,
24
+ offset: int | None,
25
+ timeout: int,
26
+ token: str,
27
+ ) -> list[dict[str, Any]]:
28
+ self.get_updates_calls += 1
29
+ if self.error is not None:
30
+ raise self.error
31
+ return self.updates
32
+
33
+ async def send_message(
34
+ self,
35
+ *,
36
+ chat_id: str,
37
+ text: str,
38
+ token: str,
39
+ ) -> None:
40
+ self.sent_messages.append(
41
+ {
42
+ "chat_id": chat_id,
43
+ "text": text,
44
+ "token": token,
45
+ }
46
+ )
47
+
48
+
49
+ def stored_bot(
50
+ *,
51
+ bot_token: str = "telegram-secret",
52
+ enabled: bool = True,
53
+ sessions: list[StoredTelegramSession] | None = None,
54
+ ) -> StoredTelegramBot:
55
+ return StoredTelegramBot(
56
+ bot_token=bot_token,
57
+ enabled=enabled,
58
+ sessions=sessions or [],
59
+ )
60
+
61
+
62
+ def stored_session(
63
+ *,
64
+ chat_id: str = "2001",
65
+ display_name: str = "Alice Example",
66
+ recent_message: str = "Hello Flowent",
67
+ status: str = "approved",
68
+ user_id: str = "1001",
69
+ username: str = "alice",
70
+ ) -> StoredTelegramSession:
71
+ return StoredTelegramSession(
72
+ chat_id=chat_id,
73
+ display_name=display_name,
74
+ recent_message=recent_message,
75
+ status=status,
76
+ user_id=user_id,
77
+ username=username,
78
+ )
79
+
80
+
81
+ def telegram_update(
82
+ *,
83
+ chat_id: int = 2001,
84
+ text: str = "Hello Flowent",
85
+ update_id: int = 1,
86
+ user_id: int = 1001,
87
+ username: str = "alice",
88
+ ) -> dict[str, Any]:
89
+ return {
90
+ "message": {
91
+ "chat": {"id": chat_id},
92
+ "from": {
93
+ "first_name": "Alice",
94
+ "id": user_id,
95
+ "last_name": "Example",
96
+ "username": username,
97
+ },
98
+ "text": text,
99
+ },
100
+ "update_id": update_id,
101
+ }
102
+
103
+
104
+ async def static_reply(_: str) -> str:
105
+ return "Reply"
106
+
107
+
108
+ @pytest.mark.anyio
109
+ async def test_disabled_telegram_bot_does_not_start_polling(
110
+ tmp_path, monkeypatch
111
+ ) -> None:
112
+ monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
113
+ transport = FakeTelegramTransport()
114
+ manager = TelegramBotManager(
115
+ message_handler=static_reply,
116
+ store=StateStore(tmp_path),
117
+ telegram_transport=transport,
118
+ )
119
+
120
+ await manager.poll_once(stored_bot(enabled=False))
121
+
122
+ assert transport.get_updates_calls == 0
123
+ assert manager.bot_with_status(stored_bot(enabled=False)).status == "disabled"
124
+
125
+
126
+ @pytest.mark.anyio
127
+ async def test_enabled_telegram_bot_polls_and_reports_running_status(
128
+ tmp_path, monkeypatch
129
+ ) -> None:
130
+ monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
131
+ transport = FakeTelegramTransport()
132
+ manager = TelegramBotManager(
133
+ message_handler=static_reply,
134
+ store=StateStore(tmp_path),
135
+ telegram_transport=transport,
136
+ )
137
+
138
+ await manager.poll_once(stored_bot())
139
+
140
+ assert transport.get_updates_calls == 1
141
+ assert manager.bot_with_status(stored_bot()).status == "running"
142
+
143
+
144
+ @pytest.mark.anyio
145
+ async def test_unapproved_telegram_message_creates_pending_request(
146
+ tmp_path, monkeypatch
147
+ ) -> None:
148
+ monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
149
+ handled_messages: list[str] = []
150
+ transport = FakeTelegramTransport()
151
+ transport.updates = [telegram_update(text="Pair this chat")]
152
+ store = StateStore(tmp_path)
153
+
154
+ async def handle_message(content: str) -> str:
155
+ handled_messages.append(content)
156
+ return "Reply"
157
+
158
+ manager = TelegramBotManager(
159
+ message_handler=handle_message,
160
+ store=store,
161
+ telegram_transport=transport,
162
+ )
163
+
164
+ await manager.poll_once(stored_bot())
165
+
166
+ assert handled_messages == []
167
+ assert store.read_telegram_bot().sessions == [
168
+ StoredTelegramSession(
169
+ chat_id="2001",
170
+ display_name="Alice Example",
171
+ recent_message="Pair this chat",
172
+ status="pending",
173
+ updated_at=store.read_telegram_bot().sessions[0].updated_at,
174
+ user_id="1001",
175
+ username="alice",
176
+ )
177
+ ]
178
+ assert transport.sent_messages == [
179
+ {
180
+ "chat_id": "2001",
181
+ "text": "Request received. Approve this conversation in Flowent.",
182
+ "token": "telegram-secret",
183
+ }
184
+ ]
185
+
186
+
187
+ @pytest.mark.anyio
188
+ async def test_approved_telegram_message_enters_workspace_and_replies(
189
+ tmp_path, monkeypatch
190
+ ) -> None:
191
+ monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
192
+ handled_messages: list[str] = []
193
+ transport = FakeTelegramTransport()
194
+ transport.updates = [telegram_update()]
195
+ store = StateStore(tmp_path)
196
+ store.save_telegram_session(stored_session())
197
+
198
+ async def handle_message(content: str) -> str:
199
+ handled_messages.append(content)
200
+ return "Reply"
201
+
202
+ manager = TelegramBotManager(
203
+ message_handler=handle_message,
204
+ store=store,
205
+ telegram_transport=transport,
206
+ )
207
+
208
+ await manager.poll_once(stored_bot())
209
+
210
+ assert handled_messages == ["Hello Flowent"]
211
+ assert transport.sent_messages == [
212
+ {
213
+ "chat_id": "2001",
214
+ "text": "Reply",
215
+ "token": "telegram-secret",
216
+ }
217
+ ]
218
+
219
+
220
+ def test_approved_telegram_message_is_persisted_in_workspace(
221
+ tmp_path, monkeypatch
222
+ ) -> None:
223
+ monkeypatch.chdir(tmp_path)
224
+ monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
225
+ transport = FakeTelegramTransport()
226
+ transport.updates = [telegram_update(text="Draft from Telegram")]
227
+
228
+ async def fake_completion(**request: object) -> object:
229
+ async def chunks() -> object:
230
+ yield {"choices": [{"delta": {"content": "Telegram reply"}}]}
231
+
232
+ return chunks()
233
+
234
+ app = create_app(
235
+ serve_frontend=False,
236
+ chat_completion=fake_completion,
237
+ telegram_transport=transport,
238
+ )
239
+ client = TestClient(app)
240
+ with client:
241
+ client.post(
242
+ "/api/providers",
243
+ json={
244
+ "api_key": "sk-local",
245
+ "base_url": "",
246
+ "id": "provider-openai",
247
+ "models": ["gpt-5.1"],
248
+ "name": "OpenAI",
249
+ "type": "openai",
250
+ },
251
+ )
252
+ client.put(
253
+ "/api/settings",
254
+ json={
255
+ "reasoning_effort": "default",
256
+ "selected_model": "gpt-5.1",
257
+ "selected_provider_id": "provider-openai",
258
+ },
259
+ )
260
+ StateStore(tmp_path / "data").save_telegram_session(stored_session())
261
+ asyncio.run(app.state.telegram_bot_manager.poll_once(stored_bot()))
262
+
263
+ state = client.get("/api/state").json()
264
+
265
+ assert [message["content"] for message in state["messages"]] == [
266
+ "Draft from Telegram",
267
+ "Telegram reply",
268
+ ]
269
+
270
+
271
+ def test_telegram_bot_config_is_saved_and_reported_in_state(
272
+ tmp_path, monkeypatch
273
+ ) -> None:
274
+ monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
275
+ client = TestClient(create_app(serve_frontend=False))
276
+
277
+ response = client.put(
278
+ "/api/telegram-bot",
279
+ json={
280
+ "bot_token": "telegram-secret",
281
+ "enabled": False,
282
+ "sessions": [],
283
+ },
284
+ )
285
+ state = client.get("/api/state").json()
286
+
287
+ assert response.status_code == 200
288
+ assert state["telegram_bot"] == {
289
+ "bot_token": "telegram-secret",
290
+ "enabled": False,
291
+ "error": "",
292
+ "sessions": [],
293
+ "status": "disabled",
294
+ }
295
+
296
+
297
+ def test_pending_telegram_request_can_be_approved(tmp_path, monkeypatch) -> None:
298
+ monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
299
+ store = StateStore(tmp_path)
300
+ store.save_telegram_session(stored_session(status="pending"))
301
+ client = TestClient(create_app(serve_frontend=False))
302
+
303
+ response = client.post(
304
+ "/api/telegram-bot/approve",
305
+ json={"chat_id": "2001"},
306
+ )
307
+ state = client.get("/api/state").json()
308
+
309
+ assert response.status_code == 200
310
+ assert response.json()["status"] == "approved"
311
+ assert state["telegram_bot"]["sessions"][0]["status"] == "approved"
312
+
313
+
314
+ @pytest.mark.anyio
315
+ async def test_telegram_reply_is_split_when_it_is_too_long(
316
+ tmp_path, monkeypatch
317
+ ) -> None:
318
+ monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
319
+ transport = FakeTelegramTransport()
320
+ transport.updates = [telegram_update()]
321
+ long_reply = "x" * 4100
322
+ store = StateStore(tmp_path)
323
+ store.save_telegram_session(stored_session())
324
+
325
+ async def handle_message(_: str) -> str:
326
+ return long_reply
327
+
328
+ manager = TelegramBotManager(
329
+ message_handler=handle_message,
330
+ store=store,
331
+ telegram_transport=transport,
332
+ )
333
+
334
+ await manager.poll_once(stored_bot())
335
+
336
+ assert [len(message["text"]) for message in transport.sent_messages] == [4096, 4]
337
+
338
+
339
+ @pytest.mark.anyio
340
+ async def test_telegram_connection_failure_reports_error_status(
341
+ tmp_path, monkeypatch
342
+ ) -> None:
343
+ monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
344
+ transport = FakeTelegramTransport()
345
+ transport.error = RuntimeError("Secret is invalid")
346
+ manager = TelegramBotManager(
347
+ message_handler=static_reply,
348
+ store=StateStore(tmp_path),
349
+ telegram_transport=transport,
350
+ )
351
+
352
+ await manager.poll_once(stored_bot())
353
+ bot = manager.bot_with_status(stored_bot())
354
+
355
+ assert bot.status == "error"
356
+ assert bot.error == "Secret is invalid"
357
+
358
+
359
+ def test_split_telegram_message_keeps_empty_reply_sendable() -> None:
360
+ assert split_telegram_message("") == [""]
@@ -4,7 +4,9 @@ from flowent.llm import (
4
4
  ChatMessage,
5
5
  ProviderConnection,
6
6
  ProviderFormat,
7
+ ReasoningEffort,
7
8
  build_litellm_request,
9
+ chunk_delta_reasoning,
8
10
  complete_chat,
9
11
  stream_chat,
10
12
  )
@@ -45,6 +47,62 @@ def test_build_litellm_request_maps_provider_connection_to_completion_args() ->
45
47
  }
46
48
 
47
49
 
50
+ def test_build_litellm_request_omits_default_reasoning_effort() -> None:
51
+ connection = ProviderConnection(
52
+ name="Primary",
53
+ provider=ProviderFormat.OPENAI,
54
+ model="gpt-5.1",
55
+ secret_reference="connection-primary",
56
+ reasoning_effort=ReasoningEffort.DEFAULT,
57
+ )
58
+
59
+ request = build_litellm_request(
60
+ connection, [ChatMessage(role="user", content="Draft a checklist.")]
61
+ )
62
+
63
+ assert "reasoning_effort" not in request
64
+
65
+
66
+ def test_build_litellm_request_includes_selected_reasoning_effort() -> None:
67
+ connection = ProviderConnection(
68
+ name="Primary",
69
+ provider=ProviderFormat.OPENAI,
70
+ model="gpt-5.1",
71
+ secret_reference="connection-primary",
72
+ reasoning_effort=ReasoningEffort.XHIGH,
73
+ )
74
+
75
+ request = build_litellm_request(
76
+ connection, [ChatMessage(role="user", content="Draft a checklist.")]
77
+ )
78
+
79
+ assert request["reasoning_effort"] == "xhigh"
80
+
81
+
82
+ def test_chunk_delta_reasoning_reads_litellm_reasoning_fields() -> None:
83
+ assert (
84
+ chunk_delta_reasoning(
85
+ {"choices": [{"delta": {"reasoning_content": "Checking files."}}]}
86
+ )
87
+ == "Checking files."
88
+ )
89
+ assert (
90
+ chunk_delta_reasoning(
91
+ {
92
+ "choices": [
93
+ {
94
+ "delta": {
95
+ "thinking_blocks": [{"thinking": "Read files."}],
96
+ "reasoning_items": [{"summary": "Summarize."}],
97
+ }
98
+ }
99
+ ]
100
+ }
101
+ )
102
+ == "Read files.Summarize."
103
+ )
104
+
105
+
48
106
  @pytest.mark.anyio
49
107
  async def test_complete_chat_uses_injected_litellm_completion() -> None:
50
108
  captured_request: dict[str, object] = {}
@@ -39,6 +39,36 @@ def test_app_state_persists_providers_across_app_instances(
39
39
  ]
40
40
 
41
41
 
42
+ def test_app_state_persists_telegram_bot_across_app_instances(
43
+ tmp_path, monkeypatch
44
+ ) -> None:
45
+ monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
46
+ client = TestClient(create_app(serve_frontend=False))
47
+
48
+ response = client.put(
49
+ "/api/telegram-bot",
50
+ json={
51
+ "bot_token": "telegram-secret",
52
+ "enabled": False,
53
+ "sessions": [],
54
+ },
55
+ )
56
+
57
+ assert response.status_code == 200
58
+
59
+ restarted_client = TestClient(create_app(serve_frontend=False))
60
+ state_response = restarted_client.get("/api/state")
61
+
62
+ assert state_response.status_code == 200
63
+ assert state_response.json()["telegram_bot"] == {
64
+ "bot_token": "telegram-secret",
65
+ "enabled": False,
66
+ "error": "",
67
+ "sessions": [],
68
+ "status": "disabled",
69
+ }
70
+
71
+
42
72
  def test_app_state_persists_settings_and_workspace_messages(
43
73
  tmp_path, monkeypatch
44
74
  ) -> None:
@@ -59,6 +89,7 @@ def test_app_state_persists_settings_and_workspace_messages(
59
89
  settings_response = client.put(
60
90
  "/api/settings",
61
91
  json={
92
+ "reasoning_effort": "xhigh",
62
93
  "selected_model": "claude-sonnet-4-5",
63
94
  "selected_provider_id": "provider-anthropic",
64
95
  },
@@ -71,6 +102,7 @@ def test_app_state_persists_settings_and_workspace_messages(
71
102
  "author": "assistant",
72
103
  "content": "Draft a launch checklist",
73
104
  "id": "message-1",
105
+ "thinking": "Read the request.",
74
106
  "tools": [
75
107
  {
76
108
  "id": "tool-1",
@@ -91,6 +123,7 @@ def test_app_state_persists_settings_and_workspace_messages(
91
123
  state = restarted_client.get("/api/state").json()
92
124
 
93
125
  assert state["settings"] == {
126
+ "reasoning_effort": "xhigh",
94
127
  "selected_model": "claude-sonnet-4-5",
95
128
  "selected_provider_id": "provider-anthropic",
96
129
  }
@@ -99,6 +132,7 @@ def test_app_state_persists_settings_and_workspace_messages(
99
132
  "author": "assistant",
100
133
  "content": "Draft a launch checklist",
101
134
  "id": "message-1",
135
+ "thinking": "Read the request.",
102
136
  "tools": [
103
137
  {
104
138
  "arguments": None,
@@ -123,3 +157,15 @@ def test_data_directory_uses_flowent_data_dir(tmp_path, monkeypatch) -> None:
123
157
 
124
158
  assert response.status_code == 200
125
159
  assert (data_dir / "flowent.db").is_file()
160
+
161
+
162
+ def test_app_state_defaults_reasoning_effort_for_existing_settings(
163
+ tmp_path, monkeypatch
164
+ ) -> None:
165
+ monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
166
+ client = TestClient(create_app(serve_frontend=False))
167
+
168
+ response = client.get("/api/state")
169
+
170
+ assert response.status_code == 200
171
+ assert response.json()["settings"]["reasoning_effort"] == "default"
@@ -0,0 +1,48 @@
1
+ import pytest
2
+
3
+ from flowent.cli import main
4
+ from flowent.main import create_app
5
+ from flowent.sandbox import SandboxError
6
+
7
+
8
+ def test_create_app_fails_when_sandbox_is_missing(monkeypatch) -> None:
9
+ monkeypatch.setattr("flowent.sandbox.sandbox_binary", lambda: None)
10
+
11
+ with pytest.raises(SandboxError, match="Install bubblewrap"):
12
+ create_app(serve_frontend=False)
13
+
14
+
15
+ def test_create_app_starts_when_bwrap_is_available(monkeypatch) -> None:
16
+ monkeypatch.setattr("flowent.sandbox.sandbox_binary", lambda: "/usr/bin/bwrap")
17
+
18
+ app = create_app(serve_frontend=False)
19
+
20
+ assert app.title == "Flowent"
21
+
22
+
23
+ def test_create_app_starts_when_bubblewrap_fallback_is_available(monkeypatch) -> None:
24
+ monkeypatch.setattr("flowent.sandbox.sandbox_binary", lambda: "/usr/bin/bubblewrap")
25
+
26
+ app = create_app(serve_frontend=False)
27
+
28
+ assert app.title == "Flowent"
29
+
30
+
31
+ def test_doctor_reports_missing_sandbox(monkeypatch, capsys) -> None:
32
+ monkeypatch.setattr("flowent.sandbox.sandbox_binary", lambda: None)
33
+
34
+ with pytest.raises(SystemExit) as error:
35
+ main(["doctor"])
36
+
37
+ assert error.value.code == 1
38
+ assert "Sandbox: missing." in capsys.readouterr().err
39
+
40
+
41
+ def test_doctor_reports_available_sandbox(monkeypatch, capsys) -> None:
42
+ monkeypatch.setattr("flowent.sandbox.sandbox_binary", lambda: "/usr/bin/bwrap")
43
+
44
+ with pytest.raises(SystemExit) as error:
45
+ main(["doctor"])
46
+
47
+ assert error.value.code == 0
48
+ assert "Sandbox: /usr/bin/bwrap" in capsys.readouterr().out
@@ -12,6 +12,7 @@ def configure_provider(
12
12
  name: str = "OpenAI",
13
13
  provider_id: str = "provider-openai",
14
14
  provider_type: str = "openai",
15
+ reasoning_effort: str = "default",
15
16
  ) -> None:
16
17
  client.post(
17
18
  "/api/providers",
@@ -27,6 +28,7 @@ def configure_provider(
27
28
  client.put(
28
29
  "/api/settings",
29
30
  json={
31
+ "reasoning_effort": reasoning_effort,
30
32
  "selected_model": model,
31
33
  "selected_provider_id": provider_id,
32
34
  },
@@ -324,6 +326,32 @@ def test_workspace_response_includes_project_and_environment_context(
324
326
  }
325
327
 
326
328
 
329
+ def test_workspace_response_uses_selected_reasoning_effort(
330
+ tmp_path, monkeypatch
331
+ ) -> None:
332
+ monkeypatch.chdir(tmp_path)
333
+ monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
334
+ captured_request: dict[str, object] = {}
335
+
336
+ async def fake_completion(**request: object) -> object:
337
+ captured_request.update(request)
338
+
339
+ async def chunks() -> object:
340
+ yield {"choices": [{"delta": {"content": "Done."}}]}
341
+
342
+ return chunks()
343
+
344
+ client = TestClient(
345
+ create_app(serve_frontend=False, chat_completion=fake_completion)
346
+ )
347
+ configure_provider(client, reasoning_effort="xhigh")
348
+
349
+ response = client.post("/api/workspace/respond", json={"content": "Hello."})
350
+
351
+ assert response.status_code == 200
352
+ assert captured_request["reasoning_effort"] == "xhigh"
353
+
354
+
327
355
  def test_workspace_response_prefers_agents_override(tmp_path, monkeypatch) -> None:
328
356
  monkeypatch.chdir(tmp_path)
329
357
  monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
package/backend/uv.lock CHANGED
@@ -388,7 +388,7 @@ wheels = [
388
388
 
389
389
  [[package]]
390
390
  name = "flowent"
391
- version = "0.0.11"
391
+ version = "0.0.13"
392
392
  source = { editable = "." }
393
393
  dependencies = [
394
394
  { name = "fastapi", extra = ["standard"] },