flowent 0.2.4 → 0.3.0

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. package/README.md +3 -3
  2. package/backend/README.md +3 -3
  3. package/backend/pyproject.toml +1 -1
  4. package/backend/src/flowent/agent.py +1 -1
  5. package/backend/src/flowent/api_models.py +103 -0
  6. package/backend/src/flowent/app.py +151 -0
  7. package/backend/src/flowent/cli.py +13 -4
  8. package/backend/src/flowent/compact.py +34 -13
  9. package/backend/src/flowent/llm.py +2 -0
  10. package/backend/src/flowent/main.py +18 -1994
  11. package/backend/src/flowent/mcp.py +100 -2
  12. package/backend/src/flowent/network.py +5 -0
  13. package/backend/src/flowent/provider_connections.py +42 -0
  14. package/backend/src/flowent/routes/__init__.py +0 -0
  15. package/backend/src/flowent/routes/integrations.py +105 -0
  16. package/backend/src/flowent/routes/permissions.py +36 -0
  17. package/backend/src/flowent/routes/providers.py +30 -0
  18. package/backend/src/flowent/routes/system.py +49 -0
  19. package/backend/src/flowent/routes/workflow_routes.py +63 -0
  20. package/backend/src/flowent/routes/workspace.py +105 -0
  21. package/backend/src/flowent/state/__init__.py +53 -0
  22. package/backend/src/flowent/state/models.py +257 -0
  23. package/backend/src/flowent/state/schema.py +186 -0
  24. package/backend/src/flowent/state/store.py +1013 -0
  25. package/backend/src/flowent/static/assets/index-CvWZZMtK.css +2 -0
  26. package/backend/src/flowent/static/assets/index-ma2v8oW7.js +90 -0
  27. package/backend/src/flowent/static/index.html +2 -2
  28. package/backend/src/flowent/storage.py +52 -1318
  29. package/backend/src/flowent/system_tools.py +25 -0
  30. package/backend/src/flowent/tools.py +4 -2
  31. package/backend/src/flowent/usage.py +9 -4
  32. package/backend/src/flowent/workflows.py +282 -0
  33. package/backend/src/flowent/workspace/__init__.py +0 -0
  34. package/backend/src/flowent/workspace/context.py +249 -0
  35. package/backend/src/flowent/workspace/events.py +180 -0
  36. package/backend/src/flowent/workspace/output.py +274 -0
  37. package/backend/src/flowent/workspace/runtime.py +1041 -0
  38. package/backend/uv.lock +1 -1
  39. package/dist/frontend/assets/index-CvWZZMtK.css +2 -0
  40. package/dist/frontend/assets/index-ma2v8oW7.js +90 -0
  41. package/dist/frontend/index.html +2 -2
  42. package/package.json +1 -1
  43. package/backend/src/flowent/static/assets/index-BH30iLzb.css +0 -2
  44. package/backend/src/flowent/static/assets/index-sBFt3ORj.js +0 -84
  45. package/dist/frontend/assets/index-BH30iLzb.css +0 -2
  46. package/dist/frontend/assets/index-sBFt3ORj.js +0 -84
@@ -5,17 +5,23 @@ import json
5
5
  import logging
6
6
  import os
7
7
  import re
8
+ import sys
8
9
  from collections.abc import Callable
9
- from contextlib import AsyncExitStack
10
+ from contextlib import AsyncExitStack, suppress
10
11
  from dataclasses import dataclass
11
12
  from importlib import import_module
12
- from typing import Any, Protocol
13
+ from typing import Any, Protocol, TextIO, cast
13
14
 
14
15
  from flowent.storage import StateStore, StoredMcpServer, StoredMcpTool
15
16
  from flowent.tools import ToolResult
16
17
 
17
18
  logger = logging.getLogger("flowent.mcp")
18
19
  MCP_CONNECT_TIMEOUT_SECONDS = 10
20
+ PYTHON_TRACEBACK_START = "Traceback (most recent call last):"
21
+ PYTHON_TRACEBACK_TERMINAL_PATTERN = re.compile(
22
+ r"^(?:[A-Za-z_][A-Za-z0-9_.]*(?:Error|Exception|Interrupt|Warning)|"
23
+ r"BaseExceptionGroup|ExceptionGroup)(?::|$)"
24
+ )
19
25
 
20
26
 
21
27
  class McpTransport(Protocol):
@@ -128,6 +134,96 @@ class _McpConnection:
128
134
  session: Any = None
129
135
 
130
136
 
137
+ class _McpStdioErrorFilter:
138
+ def __init__(self, target: TextIO) -> None:
139
+ self.target = target
140
+ self.line_buffer = ""
141
+ self.traceback_lines: list[str] | None = None
142
+
143
+ def feed(self, text: str) -> None:
144
+ self.line_buffer += text
145
+ while "\n" in self.line_buffer:
146
+ line, self.line_buffer = self.line_buffer.split("\n", 1)
147
+ self.feed_line(f"{line}\n")
148
+
149
+ def finish(self) -> None:
150
+ if self.line_buffer:
151
+ self.feed_line(self.line_buffer)
152
+ self.line_buffer = ""
153
+ if self.traceback_lines is not None:
154
+ self.write("".join(self.traceback_lines))
155
+ self.traceback_lines = None
156
+
157
+ def feed_line(self, line: str) -> None:
158
+ stripped_line = line.rstrip("\r\n")
159
+ if self.traceback_lines is not None:
160
+ self.traceback_lines.append(line)
161
+ if stripped_line == "KeyboardInterrupt":
162
+ self.traceback_lines = None
163
+ return
164
+ if PYTHON_TRACEBACK_TERMINAL_PATTERN.match(stripped_line):
165
+ self.write("".join(self.traceback_lines))
166
+ self.traceback_lines = None
167
+ return
168
+ if stripped_line == PYTHON_TRACEBACK_START:
169
+ self.traceback_lines = [line]
170
+ return
171
+ self.write(line)
172
+
173
+ def write(self, text: str) -> None:
174
+ self.target.write(text)
175
+ self.target.flush()
176
+
177
+
178
+ class _McpStdioErrorLog:
179
+ def __init__(self, target: TextIO | None = None) -> None:
180
+ self.target = target or sys.stderr
181
+ self.filter = _McpStdioErrorFilter(self.target)
182
+ self.read_fd, write_fd = os.pipe()
183
+ self.write_file = os.fdopen(write_fd, "wb", buffering=0)
184
+ self.drain_task: asyncio.Task[None] | None = None
185
+
186
+ async def __aenter__(self) -> _McpStdioErrorLog:
187
+ self.drain_task = asyncio.create_task(self.drain())
188
+ return self
189
+
190
+ async def __aexit__(
191
+ self,
192
+ exc_type: type[BaseException] | None,
193
+ exc: BaseException | None,
194
+ traceback: object,
195
+ ) -> None:
196
+ self.close_write_file()
197
+ if self.drain_task is not None:
198
+ await asyncio.gather(self.drain_task, return_exceptions=True)
199
+ else:
200
+ self.close_read_fd()
201
+
202
+ def fileno(self) -> int:
203
+ return self.write_file.fileno()
204
+
205
+ async def drain(self) -> None:
206
+ try:
207
+ while True:
208
+ chunk = await asyncio.to_thread(os.read, self.read_fd, 4096)
209
+ if not chunk:
210
+ break
211
+ self.filter.feed(chunk.decode("utf-8", errors="replace"))
212
+ except OSError:
213
+ pass
214
+ finally:
215
+ self.filter.finish()
216
+ self.close_read_fd()
217
+
218
+ def close_write_file(self) -> None:
219
+ with suppress(OSError, ValueError):
220
+ self.write_file.close()
221
+
222
+ def close_read_fd(self) -> None:
223
+ with suppress(OSError):
224
+ os.close(self.read_fd)
225
+
226
+
131
227
  class DefaultMcpTransport:
132
228
  def __init__(self) -> None:
133
229
  self._connections: dict[str, _McpConnection] = {}
@@ -197,9 +293,11 @@ class DefaultMcpTransport:
197
293
  )
198
294
  )
199
295
  else:
296
+ stdio_errlog = await stack.enter_async_context(_McpStdioErrorLog())
200
297
  read_stream, write_stream = await stack.enter_async_context(
201
298
  stdio_client(
202
299
  self._stdio_parameters(server, config),
300
+ errlog=cast(TextIO, stdio_errlog),
203
301
  )
204
302
  )
205
303
  session = await stack.enter_async_context(
@@ -0,0 +1,5 @@
1
+ from flowent._version import __version__
2
+
3
+
4
+ def flowent_user_agent() -> str:
5
+ return f"Flowent/{__version__}"
@@ -0,0 +1,42 @@
1
+ import logging
2
+
3
+ from fastapi import HTTPException
4
+
5
+ from flowent.llm import ProviderConnection
6
+ from flowent.storage import StoredState
7
+
8
+ logger = logging.getLogger("flowent.provider_connections")
9
+
10
+
11
+ def selected_connection(state: StoredState) -> ProviderConnection:
12
+ provider = next(
13
+ (
14
+ stored_provider
15
+ for stored_provider in state.providers
16
+ if stored_provider.id == state.settings.selected_provider_id
17
+ ),
18
+ None,
19
+ )
20
+ if provider is None or not state.settings.selected_model:
21
+ logger.warning("Workspace request blocked because provider or model is missing")
22
+ raise HTTPException(
23
+ status_code=400,
24
+ detail="Choose a provider and model before sending.",
25
+ )
26
+ if not provider.api_key:
27
+ logger.warning("Workspace request blocked because selected provider has no key")
28
+ raise HTTPException(status_code=400, detail="Add a key before sending.")
29
+
30
+ logger.debug(
31
+ "Workspace request using provider=%s model=%s",
32
+ provider.name,
33
+ state.settings.selected_model,
34
+ )
35
+ return ProviderConnection(
36
+ base_url=provider.base_url or None,
37
+ model=state.settings.selected_model,
38
+ name=provider.name,
39
+ provider=provider.type,
40
+ reasoning_effort=state.settings.reasoning_effort,
41
+ secret_reference=provider.api_key,
42
+ )
File without changes
@@ -0,0 +1,105 @@
1
+ from pathlib import Path
2
+
3
+ from fastapi import FastAPI, HTTPException
4
+
5
+ from flowent.api_models import (
6
+ McpImportPreviewRequest,
7
+ McpImportRequest,
8
+ SkillSettingsRequest,
9
+ TelegramSessionApproveRequest,
10
+ )
11
+ from flowent.channels import TelegramBotManager
12
+ from flowent.mcp import McpManager
13
+ from flowent.mcp_import import McpImportDiscovery, discover_imported_mcp_servers
14
+ from flowent.skills import discover_skills, update_skill_enabled
15
+ from flowent.storage import (
16
+ StateStore,
17
+ StoredMcpServer,
18
+ StoredSkill,
19
+ StoredTelegramBot,
20
+ StoredTelegramSession,
21
+ )
22
+
23
+
24
+ def register_integration_routes(
25
+ app: FastAPI,
26
+ *,
27
+ cwd: Path,
28
+ mcp_manager: McpManager,
29
+ store: StateStore,
30
+ telegram_bot_manager: TelegramBotManager,
31
+ ) -> None:
32
+ @app.put("/api/mcp/servers")
33
+ async def save_mcp_server(server: StoredMcpServer) -> StoredMcpServer:
34
+ saved_server = store.save_mcp_server(server)
35
+ return await mcp_manager.sync_server(saved_server)
36
+
37
+ @app.post("/api/mcp/import/preview")
38
+ async def preview_mcp_import(
39
+ request: McpImportPreviewRequest,
40
+ ) -> McpImportDiscovery:
41
+ return discover_imported_mcp_servers(cwd, source=request.source)
42
+
43
+ @app.post("/api/mcp/import")
44
+ async def import_mcp_servers(request: McpImportRequest) -> list[StoredMcpServer]:
45
+ imported_servers = discover_imported_mcp_servers(
46
+ cwd,
47
+ source=request.source,
48
+ ).servers
49
+ existing_servers = {server.id for server in store.read_mcp_servers()}
50
+ for server in imported_servers:
51
+ if server.id != request.server_id:
52
+ continue
53
+ if server.id in existing_servers:
54
+ continue
55
+ store.save_mcp_server(server)
56
+ existing_servers.add(server.id)
57
+ return mcp_manager.servers_with_status(store.read_mcp_servers())
58
+
59
+ @app.delete("/api/mcp/servers/{server_id}")
60
+ async def delete_mcp_server(server_id: str) -> dict[str, bool]:
61
+ await mcp_manager.delete_server(server_id)
62
+ return {"ok": True}
63
+
64
+ @app.post("/api/mcp/servers/{server_id}/reconnect")
65
+ async def reconnect_mcp_server(server_id: str) -> StoredMcpServer:
66
+ try:
67
+ return await mcp_manager.reconnect_server(server_id)
68
+ except KeyError as error:
69
+ raise HTTPException(status_code=404, detail="Server not found.") from error
70
+
71
+ @app.post("/api/mcp/reload")
72
+ async def reload_mcp_servers() -> list[StoredMcpServer]:
73
+ return await mcp_manager.reload()
74
+
75
+ @app.post("/api/skills/reload")
76
+ async def reload_skills() -> list[StoredSkill]:
77
+ return discover_skills(cwd, store)
78
+
79
+ @app.put("/api/skills/{skill_id:path}")
80
+ async def save_skill_settings(
81
+ skill_id: str,
82
+ request: SkillSettingsRequest,
83
+ ) -> StoredSkill:
84
+ try:
85
+ return update_skill_enabled(cwd, store, skill_id, request.enabled)
86
+ except KeyError as error:
87
+ raise HTTPException(status_code=404, detail="Skill not found.") from error
88
+
89
+ @app.put("/api/telegram-bot")
90
+ async def save_telegram_bot(telegram_bot: StoredTelegramBot) -> StoredTelegramBot:
91
+ saved_bot = store.save_telegram_bot(telegram_bot)
92
+ await telegram_bot_manager.sync_bot(saved_bot)
93
+ return telegram_bot_manager.bot_with_status(saved_bot)
94
+
95
+ @app.post("/api/telegram-bot/approve")
96
+ async def approve_telegram_session(
97
+ request: TelegramSessionApproveRequest,
98
+ ) -> StoredTelegramSession:
99
+ try:
100
+ return store.approve_telegram_session(request.chat_id)
101
+ except KeyError as error:
102
+ raise HTTPException(
103
+ status_code=404,
104
+ detail="Conversation not found.",
105
+ ) from error
@@ -0,0 +1,36 @@
1
+ from pathlib import Path
2
+
3
+ from fastapi import FastAPI
4
+
5
+ from flowent.api_models import WritablePathListResponse, WritablePathRequest
6
+ from flowent.storage import StateStore, StoredWritablePath
7
+
8
+
9
+ def normalized_request_path(path: str, cwd: Path) -> Path:
10
+ raw_path = Path(path).expanduser()
11
+ if not raw_path.is_absolute():
12
+ raw_path = cwd / raw_path
13
+ return raw_path.resolve(strict=False)
14
+
15
+
16
+ def register_permission_routes(
17
+ app: FastAPI,
18
+ *,
19
+ cwd: Path,
20
+ store: StateStore,
21
+ ) -> None:
22
+ @app.post("/api/permissions/writable-paths")
23
+ async def save_writable_path(
24
+ request: WritablePathRequest,
25
+ ) -> StoredWritablePath:
26
+ return store.save_writable_path(normalized_request_path(request.path, cwd))
27
+
28
+ @app.delete("/api/permissions/writable-paths")
29
+ async def delete_writable_path(
30
+ request: WritablePathRequest,
31
+ ) -> WritablePathListResponse:
32
+ return WritablePathListResponse(
33
+ writable_paths=store.delete_writable_path(
34
+ normalized_request_path(request.path, cwd)
35
+ )
36
+ )
@@ -0,0 +1,30 @@
1
+ from fastapi import FastAPI
2
+
3
+ from flowent.api_models import ProviderModelsRequest, ProviderModelsResponse
4
+ from flowent.llm import list_provider_models
5
+ from flowent.storage import StateStore, StoredProvider, StoredSettings
6
+
7
+
8
+ def register_provider_routes(app: FastAPI, *, store: StateStore) -> None:
9
+ @app.post("/api/providers")
10
+ async def save_provider(provider: StoredProvider) -> StoredProvider:
11
+ return store.save_provider(provider)
12
+
13
+ @app.delete("/api/providers/{provider_id}")
14
+ async def delete_provider(provider_id: str) -> dict[str, bool]:
15
+ store.delete_provider(provider_id)
16
+ return {"ok": True}
17
+
18
+ @app.post("/api/providers/models")
19
+ async def provider_models(request: ProviderModelsRequest) -> ProviderModelsResponse:
20
+ return ProviderModelsResponse(
21
+ models=list_provider_models(
22
+ base_url=request.base_url,
23
+ provider=request.provider,
24
+ secret_reference=request.secret_reference,
25
+ ),
26
+ )
27
+
28
+ @app.put("/api/settings")
29
+ async def save_settings(settings: StoredSettings) -> StoredSettings:
30
+ return store.save_settings(settings)
@@ -0,0 +1,49 @@
1
+ from pathlib import Path
2
+
3
+ from fastapi import FastAPI
4
+
5
+ from flowent._version import __version__
6
+ from flowent.api_models import AboutResponse
7
+ from flowent.channels import TelegramBotManager
8
+ from flowent.mcp import McpManager
9
+ from flowent.skills import discover_skills
10
+ from flowent.storage import StateStore, StoredState
11
+ from flowent.workspace.context import state_with_current_model_context_window
12
+ from flowent.workspace.runtime import WorkspaceRuntime
13
+
14
+
15
+ def register_system_routes(
16
+ app: FastAPI,
17
+ *,
18
+ cwd: Path,
19
+ mcp_manager: McpManager,
20
+ runtime: WorkspaceRuntime,
21
+ store: StateStore,
22
+ telegram_bot_manager: TelegramBotManager,
23
+ ) -> None:
24
+ @app.get("/api/health")
25
+ async def health() -> dict[str, str]:
26
+ return {"status": "ok"}
27
+
28
+ @app.get("/api/state")
29
+ async def app_state() -> StoredState:
30
+ state = state_with_current_model_context_window(store.read_state())
31
+ active_run = runtime.active_run()
32
+ update: dict[str, object] = {
33
+ "active_run_event_index": active_run.latest_event_index
34
+ if active_run
35
+ else 0,
36
+ "active_run_id": active_run.id
37
+ if active_run and not active_run.is_done
38
+ else None,
39
+ "mcp_servers": mcp_manager.servers_with_status(state.mcp_servers),
40
+ "skills": discover_skills(cwd, store),
41
+ }
42
+ update["telegram_bot"] = telegram_bot_manager.bot_with_status(
43
+ state.telegram_bot
44
+ )
45
+ return state.model_copy(update=update)
46
+
47
+ @app.get("/api/about")
48
+ async def about() -> AboutResponse:
49
+ return AboutResponse(version=__version__)
@@ -0,0 +1,63 @@
1
+ from fastapi import FastAPI, HTTPException
2
+
3
+ from flowent.llm import CompletionCallable
4
+ from flowent.provider_connections import selected_connection
5
+ from flowent.storage import StateStore, StoredWorkflow
6
+ from flowent.workflows import (
7
+ WorkflowRunResponse,
8
+ run_workflow_definition,
9
+ validate_workflow_draft,
10
+ workflow_requires_connection,
11
+ )
12
+
13
+
14
+ def register_workflow_routes(
15
+ app: FastAPI,
16
+ *,
17
+ chat_completion: CompletionCallable | None,
18
+ store: StateStore,
19
+ ) -> None:
20
+ @app.put("/api/workflows")
21
+ async def save_workflow(workflow: StoredWorkflow) -> StoredWorkflow:
22
+ try:
23
+ return store.save_workflow(
24
+ validate_workflow_draft(
25
+ workflow.model_copy(
26
+ update={"name": workflow.name.strip() or "Untitled Workflow"}
27
+ )
28
+ )
29
+ )
30
+ except ValueError as error:
31
+ raise HTTPException(status_code=400, detail=str(error)) from error
32
+
33
+ @app.delete("/api/workflows/{workflow_id}")
34
+ async def delete_workflow(workflow_id: str) -> dict[str, bool]:
35
+ store.delete_workflow(workflow_id)
36
+ return {"ok": True}
37
+
38
+ @app.post("/api/workflows/{workflow_id}/run")
39
+ async def run_workflow(workflow_id: str) -> WorkflowRunResponse:
40
+ workflow = next(
41
+ (
42
+ current_workflow
43
+ for current_workflow in store.read_workflows()
44
+ if current_workflow.id == workflow_id
45
+ ),
46
+ None,
47
+ )
48
+ if workflow is None:
49
+ raise HTTPException(status_code=404, detail="Workflow not found.")
50
+ try:
51
+ connection = (
52
+ selected_connection(store.read_state())
53
+ if workflow_requires_connection(workflow.definition)
54
+ else None
55
+ )
56
+ return await run_workflow_definition(
57
+ completion=chat_completion,
58
+ connection=connection,
59
+ definition=workflow.definition,
60
+ workflow_id=workflow.id,
61
+ )
62
+ except ValueError as error:
63
+ raise HTTPException(status_code=400, detail=str(error)) from error
@@ -0,0 +1,105 @@
1
+ import logging
2
+
3
+ from fastapi import FastAPI, Query
4
+ from fastapi.responses import StreamingResponse
5
+
6
+ from flowent.api_models import (
7
+ WorkspaceClearResponse,
8
+ WorkspaceMessageEditRequest,
9
+ WorkspaceMessageEditResponse,
10
+ WorkspaceMessagesRequest,
11
+ WorkspaceRespondRequest,
12
+ WorkspaceRunResponse,
13
+ )
14
+ from flowent.logging import TRACE_LEVEL
15
+ from flowent.storage import StateStore
16
+ from flowent.workspace.runtime import WorkspaceRuntime
17
+
18
+ logger = logging.getLogger("flowent.routes.workspace")
19
+
20
+
21
+ def register_workspace_routes(
22
+ app: FastAPI,
23
+ *,
24
+ runtime: WorkspaceRuntime,
25
+ store: StateStore,
26
+ ) -> None:
27
+ @app.put("/api/workspace/messages")
28
+ async def save_workspace_messages(
29
+ request: WorkspaceMessagesRequest,
30
+ ) -> WorkspaceMessagesRequest:
31
+ return WorkspaceMessagesRequest(messages=store.save_messages(request.messages))
32
+
33
+ @app.post("/api/workspace/messages/{message_id}/edit")
34
+ async def edit_workspace_message(
35
+ message_id: str,
36
+ request: WorkspaceMessageEditRequest,
37
+ ) -> WorkspaceMessageEditResponse:
38
+ logger.info(
39
+ "Workspace message edit requested action=%s message_id=%s content_length=%s",
40
+ request.action,
41
+ message_id,
42
+ len(request.content),
43
+ )
44
+ logger.log(TRACE_LEVEL, "Workspace edited user content=%r", request.content)
45
+ messages, run = runtime.edit_message(
46
+ message_id,
47
+ action=request.action,
48
+ content=request.content,
49
+ )
50
+ return WorkspaceMessageEditResponse(
51
+ messages=messages,
52
+ run_id=run.id if run else None,
53
+ )
54
+
55
+ @app.post("/api/workspace/clear")
56
+ async def clear_workspace() -> WorkspaceClearResponse:
57
+ messages = runtime.clear()
58
+ await runtime.notify_cleared_runs()
59
+ return WorkspaceClearResponse(messages=messages)
60
+
61
+ @app.post("/api/workspace/runs")
62
+ async def start_workspace_run(
63
+ request: WorkspaceRespondRequest,
64
+ ) -> WorkspaceRunResponse:
65
+ logger.info("Workspace run requested content_length=%s", len(request.content))
66
+ logger.log(TRACE_LEVEL, "Workspace user content=%r", request.content)
67
+ run = runtime.create_run(request.content, message_id=request.message_id)
68
+ return WorkspaceRunResponse(run_id=run.id)
69
+
70
+ @app.get("/api/workspace/runs/{run_id}/stream")
71
+ async def stream_workspace_run(
72
+ run_id: str,
73
+ after: int = Query(default=0, ge=0),
74
+ ) -> StreamingResponse:
75
+ run = runtime.run_by_id(run_id)
76
+ return StreamingResponse(
77
+ runtime.run_stream(run, after),
78
+ media_type="text/event-stream",
79
+ )
80
+
81
+ @app.post("/api/workspace/runs/{run_id}/stop")
82
+ async def stop_workspace_run(run_id: str) -> dict[str, bool]:
83
+ runtime.stop_run(run_id)
84
+ return {"ok": True}
85
+
86
+ @app.post("/api/workspace/compact", response_class=StreamingResponse)
87
+ async def compact_workspace() -> StreamingResponse:
88
+ return StreamingResponse(
89
+ runtime.compact_stream(),
90
+ media_type="text/event-stream",
91
+ )
92
+
93
+ @app.post("/api/workspace/respond")
94
+ async def respond_to_workspace(
95
+ request: WorkspaceRespondRequest,
96
+ ) -> StreamingResponse:
97
+ logger.info(
98
+ "Workspace response requested content_length=%s", len(request.content)
99
+ )
100
+ logger.log(TRACE_LEVEL, "Workspace user content=%r", request.content)
101
+ run = runtime.create_run(request.content, message_id=request.message_id)
102
+ return StreamingResponse(
103
+ runtime.run_stream(run, include_snapshots=False),
104
+ media_type="text/event-stream",
105
+ )
@@ -0,0 +1,53 @@
1
+ from flowent.state.models import (
2
+ StoredAssistantOutputGroup,
3
+ StoredCompactionCheckpoint,
4
+ StoredErrorOutputItem,
5
+ StoredMcpServer,
6
+ StoredMcpTool,
7
+ StoredMessage,
8
+ StoredOutputItem,
9
+ StoredProvider,
10
+ StoredSettings,
11
+ StoredSkill,
12
+ StoredState,
13
+ StoredTelegramBot,
14
+ StoredTelegramSession,
15
+ StoredTextOutputItem,
16
+ StoredThinkingOutputItem,
17
+ StoredToolItem,
18
+ StoredToolOutputItem,
19
+ StoredWorkflow,
20
+ StoredWorkflowDefinition,
21
+ StoredWorkflowEdge,
22
+ StoredWorkflowNode,
23
+ StoredWorkflowNodePosition,
24
+ StoredWritablePath,
25
+ )
26
+ from flowent.state.store import StateStore
27
+
28
+ __all__ = [
29
+ "StateStore",
30
+ "StoredAssistantOutputGroup",
31
+ "StoredCompactionCheckpoint",
32
+ "StoredErrorOutputItem",
33
+ "StoredMcpServer",
34
+ "StoredMcpTool",
35
+ "StoredMessage",
36
+ "StoredOutputItem",
37
+ "StoredProvider",
38
+ "StoredSettings",
39
+ "StoredSkill",
40
+ "StoredState",
41
+ "StoredTelegramBot",
42
+ "StoredTelegramSession",
43
+ "StoredTextOutputItem",
44
+ "StoredThinkingOutputItem",
45
+ "StoredToolItem",
46
+ "StoredToolOutputItem",
47
+ "StoredWorkflow",
48
+ "StoredWorkflowDefinition",
49
+ "StoredWorkflowEdge",
50
+ "StoredWorkflowNode",
51
+ "StoredWorkflowNodePosition",
52
+ "StoredWritablePath",
53
+ ]