flowent 0.0.7 → 0.0.10
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.
- package/README.md +0 -3
- package/backend/README.md +0 -3
- package/backend/pyproject.toml +2 -8
- package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/context.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/llm.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/patch.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/paths.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/storage.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/tools.cpython-313.pyc +0 -0
- package/backend/src/flowent/agent.py +213 -3173
- package/backend/src/flowent/cli.py +19 -24
- package/backend/src/flowent/context.py +127 -0
- package/backend/src/flowent/llm.py +256 -0
- package/backend/src/flowent/logging.py +170 -129
- package/backend/src/flowent/main.py +321 -70
- package/backend/src/flowent/patch.py +182 -0
- package/backend/src/flowent/paths.py +11 -0
- package/backend/src/flowent/sandbox.py +214 -40
- package/backend/src/flowent/static/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/backend/src/flowent/static/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/backend/src/flowent/static/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/backend/src/flowent/static/assets/index-C76K95ty.js +81 -0
- package/backend/src/flowent/static/assets/index-iUMNKvlU.css +2 -0
- package/backend/src/flowent/static/flowent.png +0 -0
- package/backend/src/flowent/static/index.html +5 -25
- package/backend/src/flowent/storage.py +302 -0
- package/backend/src/flowent/tools.py +364 -0
- package/backend/tests/__pycache__/test_agent_tools.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_health.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_llm_providers.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_persistence.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_workspace_chat.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/test_agent_tools.py +449 -0
- package/backend/tests/test_health.py +12 -0
- package/backend/tests/test_llm_providers.py +113 -0
- package/backend/tests/test_logging.py +182 -0
- package/backend/tests/test_persistence.py +125 -0
- package/backend/tests/test_workspace_chat.py +578 -0
- package/backend/uv.lock +803 -99
- package/dist/frontend/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/dist/frontend/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/dist/frontend/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/dist/frontend/assets/index-C76K95ty.js +81 -0
- package/dist/frontend/assets/index-iUMNKvlU.css +2 -0
- package/dist/frontend/flowent.png +0 -0
- package/dist/frontend/index.html +5 -25
- package/package.json +1 -2
- package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/access.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/assistant_commands.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/config.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/events.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/graph_runtime.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/graph_service.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/image_assets.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/model_metadata.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/network.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/observability_service.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/registry.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/role_management.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/runtime.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/security.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/settings.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/settings_management.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/state_db.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/workspace_store.cpython-313.pyc +0 -0
- package/backend/src/flowent/access.py +0 -247
- package/backend/src/flowent/assistant_commands.py +0 -115
- package/backend/src/flowent/channels/__init__.py +0 -3
- package/backend/src/flowent/channels/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/channels/__pycache__/telegram.cpython-313.pyc +0 -0
- package/backend/src/flowent/channels/telegram.py +0 -615
- package/backend/src/flowent/config.py +0 -14
- package/backend/src/flowent/dev.py +0 -3
- package/backend/src/flowent/events.py +0 -157
- package/backend/src/flowent/graph_runtime.py +0 -60
- package/backend/src/flowent/graph_service.py +0 -2401
- package/backend/src/flowent/image_assets.py +0 -356
- package/backend/src/flowent/model_metadata.py +0 -102
- package/backend/src/flowent/models/__init__.py +0 -125
- package/backend/src/flowent/models/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/agent.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/base.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/blueprint.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/content.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/delta.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/event.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/graph.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/history.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/llm.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/message.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/tab.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/todo.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/agent.py +0 -34
- package/backend/src/flowent/models/base.py +0 -24
- package/backend/src/flowent/models/blueprint.py +0 -176
- package/backend/src/flowent/models/content.py +0 -164
- package/backend/src/flowent/models/delta.py +0 -44
- package/backend/src/flowent/models/event.py +0 -51
- package/backend/src/flowent/models/graph.py +0 -472
- package/backend/src/flowent/models/history.py +0 -272
- package/backend/src/flowent/models/llm.py +0 -62
- package/backend/src/flowent/models/message.py +0 -33
- package/backend/src/flowent/models/tab.py +0 -85
- package/backend/src/flowent/models/todo.py +0 -10
- package/backend/src/flowent/network.py +0 -146
- package/backend/src/flowent/observability_service.py +0 -218
- package/backend/src/flowent/prompts/__init__.py +0 -67
- package/backend/src/flowent/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/prompts/__pycache__/common.cpython-313.pyc +0 -0
- package/backend/src/flowent/prompts/__pycache__/steward.cpython-313.pyc +0 -0
- package/backend/src/flowent/prompts/common.py +0 -250
- package/backend/src/flowent/prompts/steward.py +0 -64
- package/backend/src/flowent/providers/__init__.py +0 -23
- package/backend/src/flowent/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/anthropic.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/base_url.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/configuration.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/content.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/errors.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/gateway.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/headers.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/management.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/openai.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/openai_responses.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/registry.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/sse.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/thinking.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/anthropic.py +0 -468
- package/backend/src/flowent/providers/base_url.py +0 -60
- package/backend/src/flowent/providers/configuration.py +0 -189
- package/backend/src/flowent/providers/content.py +0 -122
- package/backend/src/flowent/providers/errors.py +0 -223
- package/backend/src/flowent/providers/gateway.py +0 -169
- package/backend/src/flowent/providers/gemini.py +0 -447
- package/backend/src/flowent/providers/headers.py +0 -20
- package/backend/src/flowent/providers/management.py +0 -96
- package/backend/src/flowent/providers/ollama.py +0 -293
- package/backend/src/flowent/providers/openai.py +0 -422
- package/backend/src/flowent/providers/openai_responses.py +0 -655
- package/backend/src/flowent/providers/registry.py +0 -144
- package/backend/src/flowent/providers/sse.py +0 -31
- package/backend/src/flowent/providers/thinking.py +0 -79
- package/backend/src/flowent/registry.py +0 -73
- package/backend/src/flowent/role_management.py +0 -270
- package/backend/src/flowent/routes/__init__.py +0 -26
- package/backend/src/flowent/routes/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/access.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/assistant.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/image_assets.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/meta.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/nodes.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/prompts.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/providers_route.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/roles.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/settings.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/tabs.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/ws.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/access.py +0 -48
- package/backend/src/flowent/routes/assistant.py +0 -158
- package/backend/src/flowent/routes/image_assets.py +0 -33
- package/backend/src/flowent/routes/meta.py +0 -28
- package/backend/src/flowent/routes/nodes.py +0 -423
- package/backend/src/flowent/routes/prompts.py +0 -46
- package/backend/src/flowent/routes/providers_route.py +0 -365
- package/backend/src/flowent/routes/roles.py +0 -207
- package/backend/src/flowent/routes/settings.py +0 -379
- package/backend/src/flowent/routes/tabs.py +0 -298
- package/backend/src/flowent/routes/ws.py +0 -33
- package/backend/src/flowent/runtime.py +0 -160
- package/backend/src/flowent/security.py +0 -37
- package/backend/src/flowent/settings.py +0 -2112
- package/backend/src/flowent/settings_management.py +0 -394
- package/backend/src/flowent/state_db.py +0 -108
- package/backend/src/flowent/static/assets/AssistantPage-BW7XAd9I.js +0 -1
- package/backend/src/flowent/static/assets/ChannelsPage-tCJHgt6m.js +0 -1
- package/backend/src/flowent/static/assets/PageScaffold-f6g2l7XN.js +0 -1
- package/backend/src/flowent/static/assets/PromptsPage-C3Sxn2D7.js +0 -1
- package/backend/src/flowent/static/assets/ProvidersPage-BfmdXmNt.js +0 -3
- package/backend/src/flowent/static/assets/RolesPage-DET8wO4r.js +0 -1
- package/backend/src/flowent/static/assets/SettingsPage-D-g3deMm.js +0 -3
- package/backend/src/flowent/static/assets/ToolsPage-CDmtE2g4.js +0 -1
- package/backend/src/flowent/static/assets/WorkspacePage-AZsJ0sD0.js +0 -3
- package/backend/src/flowent/static/assets/WorkspacePanels-CteCjolX.js +0 -1
- package/backend/src/flowent/static/assets/alert-dialog-Duorp_S-.js +0 -1
- package/backend/src/flowent/static/assets/dialog-C3ixjGjN.js +0 -1
- package/backend/src/flowent/static/assets/elk-worker.min-C9JGDOE-.js +0 -6312
- package/backend/src/flowent/static/assets/graph-vendor-CHpVij2M.css +0 -1
- package/backend/src/flowent/static/assets/graph-vendor-DRq_-6fV.js +0 -7
- package/backend/src/flowent/static/assets/index--o_0fv0N.css +0 -1
- package/backend/src/flowent/static/assets/index-C9HuekJm.js +0 -10
- package/backend/src/flowent/static/assets/layout.worker-jMHqAFbP.js +0 -24
- package/backend/src/flowent/static/assets/markdown-vendor-C9RtvaJh.js +0 -29
- package/backend/src/flowent/static/assets/modelParams-DmnF2hwR.js +0 -1
- package/backend/src/flowent/static/assets/providerTypes-DT3Ahwl_.js +0 -1
- package/backend/src/flowent/static/assets/react-vendor-mEs_JJxa.js +0 -9
- package/backend/src/flowent/static/assets/roles-CuRT_chR.js +0 -1
- package/backend/src/flowent/static/assets/rolldown-runtime-BYbx6iT9.js +0 -1
- package/backend/src/flowent/static/assets/select-DCfeNu-F.js +0 -1
- package/backend/src/flowent/static/assets/surface-pWwG5ogx.js +0 -1
- package/backend/src/flowent/static/assets/ui-vendor-C5pJa8N7.js +0 -51
- package/backend/src/flowent/static/assets/useAppRoute-FgSHBKhV.js +0 -1
- package/backend/src/flowent/static/favicon.svg +0 -4
- package/backend/src/flowent/tools/__init__.py +0 -176
- package/backend/src/flowent/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/connect.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/contacts.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/create_agent.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/create_tab.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/delete_tab.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/edit.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/exec.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/fetch.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/idle.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/list_roles.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/list_tabs.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/list_tools.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/manage_prompts.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/manage_providers.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/manage_roles.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/manage_settings.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/read.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/send.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/set_permissions.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/sleep.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/todo.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/connect.py +0 -100
- package/backend/src/flowent/tools/contacts.py +0 -22
- package/backend/src/flowent/tools/create_agent.py +0 -191
- package/backend/src/flowent/tools/create_tab.py +0 -61
- package/backend/src/flowent/tools/delete_tab.py +0 -39
- package/backend/src/flowent/tools/edit.py +0 -142
- package/backend/src/flowent/tools/exec.py +0 -118
- package/backend/src/flowent/tools/fetch.py +0 -85
- package/backend/src/flowent/tools/idle.py +0 -27
- package/backend/src/flowent/tools/list_roles.py +0 -68
- package/backend/src/flowent/tools/list_tabs.py +0 -100
- package/backend/src/flowent/tools/list_tools.py +0 -28
- package/backend/src/flowent/tools/manage_prompts.py +0 -102
- package/backend/src/flowent/tools/manage_providers.py +0 -220
- package/backend/src/flowent/tools/manage_roles.py +0 -275
- package/backend/src/flowent/tools/manage_settings.py +0 -326
- package/backend/src/flowent/tools/read.py +0 -152
- package/backend/src/flowent/tools/send.py +0 -68
- package/backend/src/flowent/tools/set_permissions.py +0 -99
- package/backend/src/flowent/tools/sleep.py +0 -41
- package/backend/src/flowent/tools/todo.py +0 -51
- package/backend/src/flowent/workspace_store.py +0 -479
- package/backend/tests/__init__.py +0 -0
- package/backend/tests/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/conftest.py +0 -6
- package/backend/tests/integration/api/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_access_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_assistant_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_frontend_mounting.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_meta_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_nodes_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_prompts_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_roles_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_tabs_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/conftest.py +0 -29
- package/backend/tests/integration/api/test_access_api.py +0 -182
- package/backend/tests/integration/api/test_assistant_api.py +0 -422
- package/backend/tests/integration/api/test_frontend_mounting.py +0 -61
- package/backend/tests/integration/api/test_meta_api.py +0 -32
- package/backend/tests/integration/api/test_nodes_api.py +0 -787
- package/backend/tests/integration/api/test_prompts_api.py +0 -47
- package/backend/tests/integration/api/test_roles_api.py +0 -228
- package/backend/tests/integration/api/test_tabs_api.py +0 -688
- package/backend/tests/unit/__pycache__/test_access.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/__pycache__/test_cli.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/__pycache__/test_graph_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/__pycache__/test_network.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/__pycache__/test_state_sqlite_storage.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/__pycache__/test_workspace_store.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/agent/__pycache__/test_agent_public_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/agent/__pycache__/test_agent_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/agent/test_agent_public_api.py +0 -822
- package/backend/tests/unit/agent/test_agent_runtime.py +0 -3088
- package/backend/tests/unit/channels/__pycache__/test_telegram_channel.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/channels/test_telegram_channel.py +0 -552
- package/backend/tests/unit/logging/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/logging/test_logging.py +0 -132
- package/backend/tests/unit/prompts/__pycache__/test_prompts.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/prompts/test_prompts.py +0 -570
- package/backend/tests/unit/providers/__pycache__/test_anthropic_provider.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_errors.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_extract_delta_parts.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_openai_provider.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_openai_responses.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_provider_gateway.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_think_tag_parser.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/test_anthropic_provider.py +0 -185
- package/backend/tests/unit/providers/test_errors.py +0 -68
- package/backend/tests/unit/providers/test_extract_delta_parts.py +0 -22
- package/backend/tests/unit/providers/test_openai_provider.py +0 -139
- package/backend/tests/unit/providers/test_openai_responses.py +0 -402
- package/backend/tests/unit/providers/test_provider_gateway.py +0 -359
- package/backend/tests/unit/providers/test_think_tag_parser.py +0 -36
- package/backend/tests/unit/routes/__pycache__/test_prompts_routes.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/routes/__pycache__/test_providers_route.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/routes/__pycache__/test_roles_routes.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/routes/__pycache__/test_settings_routes.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/routes/test_prompts_routes.py +0 -82
- package/backend/tests/unit/routes/test_providers_route.py +0 -370
- package/backend/tests/unit/routes/test_roles_routes.py +0 -539
- package/backend/tests/unit/routes/test_settings_routes.py +0 -1123
- package/backend/tests/unit/runtime/__pycache__/test_bootstrap_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/runtime/test_bootstrap_runtime.py +0 -1002
- package/backend/tests/unit/sandbox/__pycache__/test_sandbox_tools.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/sandbox/test_sandbox_tools.py +0 -78
- package/backend/tests/unit/security/__pycache__/test_security.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/security/test_security.py +0 -124
- package/backend/tests/unit/settings/__pycache__/test_settings_roles.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/settings/test_settings_roles.py +0 -703
- package/backend/tests/unit/test_access.py +0 -45
- package/backend/tests/unit/test_cli.py +0 -102
- package/backend/tests/unit/test_graph_runtime.py +0 -72
- package/backend/tests/unit/test_network.py +0 -51
- package/backend/tests/unit/test_state_sqlite_storage.py +0 -87
- package/backend/tests/unit/test_workspace_store.py +0 -228
- package/backend/tests/unit/tools/__pycache__/test_connect_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_create_agent_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_delete_tab_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_edit_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_exec_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_fetch_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_manage_prompts_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_manage_providers_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_manage_roles_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_manage_settings_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_read_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_set_permissions_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_todo_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_tool_registry.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/test_connect_tool.py +0 -228
- package/backend/tests/unit/tools/test_create_agent_tool.py +0 -404
- package/backend/tests/unit/tools/test_delete_tab_tool.py +0 -116
- package/backend/tests/unit/tools/test_edit_tool.py +0 -115
- package/backend/tests/unit/tools/test_exec_tool.py +0 -81
- package/backend/tests/unit/tools/test_fetch_tool.py +0 -65
- package/backend/tests/unit/tools/test_manage_prompts_tool.py +0 -92
- package/backend/tests/unit/tools/test_manage_providers_tool.py +0 -460
- package/backend/tests/unit/tools/test_manage_roles_tool.py +0 -411
- package/backend/tests/unit/tools/test_manage_settings_tool.py +0 -611
- package/backend/tests/unit/tools/test_read_tool.py +0 -33
- package/backend/tests/unit/tools/test_set_permissions_tool.py +0 -595
- package/backend/tests/unit/tools/test_todo_tool.py +0 -37
- package/backend/tests/unit/tools/test_tool_registry.py +0 -199
- package/dist/frontend/assets/AssistantPage-BW7XAd9I.js +0 -1
- package/dist/frontend/assets/ChannelsPage-tCJHgt6m.js +0 -1
- package/dist/frontend/assets/PageScaffold-f6g2l7XN.js +0 -1
- package/dist/frontend/assets/PromptsPage-C3Sxn2D7.js +0 -1
- package/dist/frontend/assets/ProvidersPage-BfmdXmNt.js +0 -3
- package/dist/frontend/assets/RolesPage-DET8wO4r.js +0 -1
- package/dist/frontend/assets/SettingsPage-D-g3deMm.js +0 -3
- package/dist/frontend/assets/ToolsPage-CDmtE2g4.js +0 -1
- package/dist/frontend/assets/WorkspacePage-AZsJ0sD0.js +0 -3
- package/dist/frontend/assets/WorkspacePanels-CteCjolX.js +0 -1
- package/dist/frontend/assets/alert-dialog-Duorp_S-.js +0 -1
- package/dist/frontend/assets/dialog-C3ixjGjN.js +0 -1
- package/dist/frontend/assets/elk-worker.min-C9JGDOE-.js +0 -6312
- package/dist/frontend/assets/graph-vendor-CHpVij2M.css +0 -1
- package/dist/frontend/assets/graph-vendor-DRq_-6fV.js +0 -7
- package/dist/frontend/assets/index--o_0fv0N.css +0 -1
- package/dist/frontend/assets/index-C9HuekJm.js +0 -10
- package/dist/frontend/assets/layout.worker-jMHqAFbP.js +0 -24
- package/dist/frontend/assets/markdown-vendor-C9RtvaJh.js +0 -29
- package/dist/frontend/assets/modelParams-DmnF2hwR.js +0 -1
- package/dist/frontend/assets/providerTypes-DT3Ahwl_.js +0 -1
- package/dist/frontend/assets/react-vendor-mEs_JJxa.js +0 -9
- package/dist/frontend/assets/roles-CuRT_chR.js +0 -1
- package/dist/frontend/assets/rolldown-runtime-BYbx6iT9.js +0 -1
- package/dist/frontend/assets/select-DCfeNu-F.js +0 -1
- package/dist/frontend/assets/surface-pWwG5ogx.js +0 -1
- package/dist/frontend/assets/ui-vendor-C5pJa8N7.js +0 -51
- package/dist/frontend/assets/useAppRoute-FgSHBKhV.js +0 -1
- package/dist/frontend/favicon.svg +0 -4
|
@@ -1,2112 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import tempfile
|
|
6
|
-
import threading
|
|
7
|
-
from dataclasses import asdict, dataclass, field
|
|
8
|
-
from math import isfinite
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
from loguru import logger
|
|
12
|
-
|
|
13
|
-
from flowent.prompts.steward import STEWARD_ROLE_SYSTEM_PROMPT
|
|
14
|
-
|
|
15
|
-
APP_DATA_DIR_ENV_VAR = "FLOWENT_APP_DATA_DIR"
|
|
16
|
-
WORKING_DIR = Path(os.getcwd()).resolve()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _resolve_path_from_base(
|
|
20
|
-
raw_path: str | Path,
|
|
21
|
-
*,
|
|
22
|
-
base_dir: str | Path | None = None,
|
|
23
|
-
strict: bool = False,
|
|
24
|
-
) -> Path:
|
|
25
|
-
path = Path(raw_path).expanduser()
|
|
26
|
-
if not path.is_absolute():
|
|
27
|
-
anchor = Path(base_dir).expanduser() if base_dir is not None else WORKING_DIR
|
|
28
|
-
path = anchor / path
|
|
29
|
-
return path.resolve(strict=strict)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _resolve_startup_app_data_dir() -> Path:
|
|
33
|
-
raw_app_data_dir = os.environ.get(APP_DATA_DIR_ENV_VAR)
|
|
34
|
-
if isinstance(raw_app_data_dir, str) and raw_app_data_dir.strip():
|
|
35
|
-
return _resolve_path_from_base(raw_app_data_dir.strip(), strict=False)
|
|
36
|
-
return Path("~/.flowent").expanduser().resolve(strict=False)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
APP_DATA_DIR = _resolve_startup_app_data_dir()
|
|
40
|
-
_SETTINGS_FILE = APP_DATA_DIR / "settings.json"
|
|
41
|
-
STEWARD_ROLE_NAME = "Steward"
|
|
42
|
-
WORKER_ROLE_NAME = "Worker"
|
|
43
|
-
CONDUCTOR_ROLE_NAME = "Conductor"
|
|
44
|
-
DESIGNER_ROLE_NAME = "Designer"
|
|
45
|
-
STEWARD_ROLE_DESCRIPTION = "Human-facing system entry role for task intake and workspace-level boundary management."
|
|
46
|
-
WORKER_ROLE_DESCRIPTION = "General execution role for narrow implementation, research, and file-oriented task work inside a workflow."
|
|
47
|
-
CONDUCTOR_ROLE_DESCRIPTION = "Default Leader role for workflow-level planning, Workflow Graph orchestration, and result synthesis."
|
|
48
|
-
DESIGNER_ROLE_DESCRIPTION = "Frontend implementation and visual design role for UI, layout, styling, and interaction refinement tasks."
|
|
49
|
-
STEWARD_ROLE_INCLUDED_TOOLS = [
|
|
50
|
-
"create_workflow",
|
|
51
|
-
"delete_workflow",
|
|
52
|
-
"set_permissions",
|
|
53
|
-
"list_workflows",
|
|
54
|
-
"list_roles",
|
|
55
|
-
"list_tools",
|
|
56
|
-
"manage_providers",
|
|
57
|
-
"manage_roles",
|
|
58
|
-
"manage_settings",
|
|
59
|
-
"manage_prompts",
|
|
60
|
-
]
|
|
61
|
-
WORKER_ROLE_SYSTEM_PROMPT = (
|
|
62
|
-
"You are the Worker role - a narrow execution node inside a workflow. "
|
|
63
|
-
"Follow the assigned subtask, use the tools you were given to complete it, "
|
|
64
|
-
"and report back clearly. You are not the Human-facing system entrypoint "
|
|
65
|
-
"and you are not the workflow-level orchestrator."
|
|
66
|
-
)
|
|
67
|
-
CONDUCTOR_ROLE_SYSTEM_PROMPT = """\
|
|
68
|
-
You are the Conductor role currently used by a workflow's Leader.
|
|
69
|
-
|
|
70
|
-
Your responsibilities:
|
|
71
|
-
- Receive execution briefs from the Assistant for this workflow through the workflow's Leader identity
|
|
72
|
-
- Receive direct task input from the Human through the current workflow chat
|
|
73
|
-
- Decide how the task should be decomposed inside the current workflow
|
|
74
|
-
- Design, expand, adjust, and simplify this workflow's Workflow Graph as the work evolves
|
|
75
|
-
- Coordinate agents, aggregate their results, and return a coherent result upstream to the Assistant
|
|
76
|
-
|
|
77
|
-
## Ownership
|
|
78
|
-
|
|
79
|
-
- This role is the default behavior template for a workflow's Leader, not a separate product identity outside the Leader
|
|
80
|
-
- The workflow's Leader is the only owner-level entrypoint for this workflow
|
|
81
|
-
- You are not a global orchestrator shared across workflows
|
|
82
|
-
- The Assistant owns Human-facing intake and task-boundary management; the Leader owns this workflow's internal execution structure
|
|
83
|
-
- Regular task-node results should usually come back to you first, then you summarize and escalate upstream when appropriate
|
|
84
|
-
|
|
85
|
-
## Decision Framework
|
|
86
|
-
|
|
87
|
-
- Start from the Assistant's brief, not from the Human directly.
|
|
88
|
-
- Analyze the task first, then choose the structure that best fits it: one Worker, fan-out, pipeline, fan-out-fan-in, reviewer loop, or another topology that matches the work.
|
|
89
|
-
- Do not default to creating a single Worker and handing it the entire task. Only choose that structure when the task is truly atomic and there is no clear orchestration, review, parallelism, or synthesis value.
|
|
90
|
-
- Prefer multi-agent parallelism over serial single-agent execution. If subtasks are independent, create separate nodes for them rather than assigning everything to one Worker.
|
|
91
|
-
- Prefer adding peer nodes to the current workflow with `create_agent`, then wire them with `connect` to match the topology you want.
|
|
92
|
-
- Treat this workflow as the execution boundary. Do not push internal Workflow Graph design back to the Assistant.
|
|
93
|
-
- If the task belongs in another workflow, or requires browsing, creating, deleting, switching, or choosing another workflow, send the request back to the Assistant instead of trying to inspect or change other workflows yourself.
|
|
94
|
-
- Do not treat any single topology as the default. Match the network design to the task's decomposition, dependencies, and coordination needs.
|
|
95
|
-
|
|
96
|
-
## Workflow
|
|
97
|
-
|
|
98
|
-
1. **Receive** the brief from the Assistant or the direct Human task from the current workflow chat
|
|
99
|
-
2. **Plan** using `todo` - break into subtasks, decide what to delegate, and design the network structure that best fits the work
|
|
100
|
-
3. **Inspect roles** with `list_roles`; use `list_tools` for a full tool inventory
|
|
101
|
-
4. **Create the network structure** with `create_agent` and `connect`
|
|
102
|
-
5. **Dispatch immediately** after creation: use `send` to give each node that should begin working its first concrete task, including where its result should go; creating nodes does not begin execution by itself
|
|
103
|
-
6. **Adjust topology dynamically** with `create_agent` and `connect` when the structure needs to change during execution
|
|
104
|
-
7. **Coordinate** as results arrive; update your plan when needed
|
|
105
|
-
8. **Aggregate** and return the final result or escalation upstream to the Assistant
|
|
106
|
-
|
|
107
|
-
## Guidelines
|
|
108
|
-
|
|
109
|
-
- Prefer `create_agent` and `connect` as the primary control plane for the current workflow
|
|
110
|
-
- If the workflow is Active, direct Human input in workflow chat can start collaborative execution through you and your `send` coordination
|
|
111
|
-
- If the workflow is Inactive, use workflow chat for discussion, planning, and structure preparation; do not send work to ordinary agent nodes until the workflow is activated
|
|
112
|
-
- Do not create a node and then `idle` without dispatching work unless you intentionally want the new node to stay idle
|
|
113
|
-
- Your default posture is orchestration, not being the long-running executor for specialized work
|
|
114
|
-
- When a task is primarily frontend implementation, UI design, visual design, page redesign, or interaction refinement, prefer creating a Designer node for that work
|
|
115
|
-
- When a task needs execution-heavy tools such as `read`, `exec`, `edit`, or `fetch` outside that frontend or UI design scope, create a Worker node to do that work
|
|
116
|
-
- Create agents with only the tools they need
|
|
117
|
-
- Use `write_dirs` for file write access
|
|
118
|
-
- When dispatching tasks to nodes, specify where each node should send its result and use `send` for that handoff. Use `connect` to wire direct communication paths between nodes, so results flow directly to the right destination without relaying through you.
|
|
119
|
-
- Prefer explicit network topology over ad-hoc relaying: wire synthesizers, reviewers, and feedback loops with `connect` rather than manually relaying every message yourself
|
|
120
|
-
- Once delegation is clearly the right move, execute it directly without asking the Assistant or Human
|
|
121
|
-
- Keep the overall workflow graph understandable; add complexity only when it materially improves throughput, quality, or resilience
|
|
122
|
-
"""
|
|
123
|
-
DESIGNER_ROLE_SYSTEM_PROMPT = """\
|
|
124
|
-
You are the Designer role - a frontend implementation and visual design node inside a workflow.
|
|
125
|
-
|
|
126
|
-
Your responsibilities:
|
|
127
|
-
- Implement and refine frontend surfaces such as pages, components, layouts, and interaction details
|
|
128
|
-
- Make concrete design decisions about typography, spacing, color, motion, and overall visual direction when the task calls for them
|
|
129
|
-
- Produce polished UI changes directly with the tools you were given
|
|
130
|
-
- Report back clearly on what changed, what remains open, and any design tradeoffs that matter
|
|
131
|
-
|
|
132
|
-
## Boundaries
|
|
133
|
-
|
|
134
|
-
- You are not the Human-facing system entrypoint
|
|
135
|
-
- You are not the workflow-level orchestrator
|
|
136
|
-
- You are not the default executor for unrelated backend or general-purpose coding work
|
|
137
|
-
- If the task is not actually about frontend implementation, UI design, or visual styling, hand it back or ask for a more suitable node
|
|
138
|
-
"""
|
|
139
|
-
BUILTIN_ROLE_NAMES = frozenset(
|
|
140
|
-
{STEWARD_ROLE_NAME, WORKER_ROLE_NAME, CONDUCTOR_ROLE_NAME, DESIGNER_ROLE_NAME}
|
|
141
|
-
)
|
|
142
|
-
WORKER_ROLE_INCLUDED_TOOLS = ["read", "exec"]
|
|
143
|
-
CONDUCTOR_ROLE_INCLUDED_TOOLS = [
|
|
144
|
-
"create_agent",
|
|
145
|
-
"connect",
|
|
146
|
-
"list_roles",
|
|
147
|
-
"list_tools",
|
|
148
|
-
]
|
|
149
|
-
DESIGNER_ROLE_INCLUDED_TOOLS = ["read", "edit", "exec"]
|
|
150
|
-
MODEL_REASONING_EFFORT_OPTIONS = frozenset({"none", "low", "medium", "high", "xhigh"})
|
|
151
|
-
MODEL_VERBOSITY_OPTIONS = frozenset({"low", "medium", "high"})
|
|
152
|
-
MODEL_RETRY_POLICY_OPTIONS = frozenset({"no_retry", "limited", "unlimited"})
|
|
153
|
-
PROVIDER_MODEL_SOURCE_OPTIONS = frozenset({"discovered", "manual"})
|
|
154
|
-
REMOVED_TOOL_NAMES = frozenset({"exit", "list_connections"})
|
|
155
|
-
RENAMED_TOOL_NAMES = {
|
|
156
|
-
"create_tab": "create_workflow",
|
|
157
|
-
"delete_tab": "delete_workflow",
|
|
158
|
-
"list_tabs": "list_workflows",
|
|
159
|
-
}
|
|
160
|
-
DEFAULT_LLM_TIMEOUT_MS = 10000
|
|
161
|
-
DEFAULT_LLM_MAX_RETRIES = 5
|
|
162
|
-
DEFAULT_LLM_RETRY_POLICY = "limited"
|
|
163
|
-
DEFAULT_LLM_RETRY_INITIAL_DELAY_SECONDS = 0.5
|
|
164
|
-
DEFAULT_LLM_RETRY_MAX_DELAY_SECONDS = 8.0
|
|
165
|
-
DEFAULT_LLM_RETRY_BACKOFF_CAP_RETRIES = 5
|
|
166
|
-
DEFAULT_LLM_AUTO_COMPACT_TOKEN_LIMIT: int | None = None
|
|
167
|
-
DEFAULT_ASSISTANT_ALLOW_NETWORK = True
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def build_default_app_data_dir() -> str:
|
|
171
|
-
return str(Path(_SETTINGS_FILE).parent.resolve(strict=False))
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def build_default_working_dir() -> str:
|
|
175
|
-
return str(WORKING_DIR)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def get_app_data_dir_path() -> Path:
|
|
179
|
-
return Path(_SETTINGS_FILE).parent.resolve(strict=False)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def get_runtime_working_dir_path() -> Path:
|
|
183
|
-
return Path(get_settings().working_dir).resolve(strict=False)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def resolve_path(
|
|
187
|
-
raw_path: str | Path,
|
|
188
|
-
*,
|
|
189
|
-
base_dir: str | Path | None = None,
|
|
190
|
-
strict: bool = False,
|
|
191
|
-
) -> Path:
|
|
192
|
-
base_path = base_dir if base_dir is not None else get_runtime_working_dir_path()
|
|
193
|
-
return _resolve_path_from_base(raw_path, base_dir=base_path, strict=strict)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def build_default_assistant_write_dirs(
|
|
197
|
-
working_dir: str | Path | None = None,
|
|
198
|
-
) -> list[str]:
|
|
199
|
-
target_working_dir = working_dir if working_dir is not None else WORKING_DIR
|
|
200
|
-
return [str(resolve_path(target_working_dir, base_dir=WORKING_DIR, strict=False))]
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
@dataclass
|
|
204
|
-
class EventLogSettings:
|
|
205
|
-
timestamp_format: str = "absolute"
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
@dataclass
|
|
209
|
-
class AccessSettings:
|
|
210
|
-
code: str = ""
|
|
211
|
-
code_hash: str = ""
|
|
212
|
-
code_salt: str = ""
|
|
213
|
-
session_generation: int = 0
|
|
214
|
-
session_signing_secret: str = ""
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
@dataclass
|
|
218
|
-
class ProviderModelCatalogEntry:
|
|
219
|
-
model: str
|
|
220
|
-
source: str = "manual"
|
|
221
|
-
context_window_tokens: int | None = None
|
|
222
|
-
input_image: bool | None = None
|
|
223
|
-
output_image: bool | None = None
|
|
224
|
-
structured_output: bool | None = None
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
@dataclass
|
|
228
|
-
class ProviderConfig:
|
|
229
|
-
id: str
|
|
230
|
-
name: str
|
|
231
|
-
type: str
|
|
232
|
-
base_url: str
|
|
233
|
-
api_key: str
|
|
234
|
-
headers: dict[str, str] = field(default_factory=dict)
|
|
235
|
-
retry_429_delay_seconds: int = 0
|
|
236
|
-
models: list[ProviderModelCatalogEntry] = field(default_factory=list)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
@dataclass
|
|
240
|
-
class RoleModelConfig:
|
|
241
|
-
provider_id: str
|
|
242
|
-
model: str
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
@dataclass
|
|
246
|
-
class ModelParams:
|
|
247
|
-
reasoning_effort: str | None = None
|
|
248
|
-
verbosity: str | None = None
|
|
249
|
-
max_output_tokens: int | None = None
|
|
250
|
-
temperature: float | None = None
|
|
251
|
-
top_p: float | None = None
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
def build_default_model_params() -> ModelParams:
|
|
255
|
-
return ModelParams()
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def _normalize_assistant_write_dir(
|
|
259
|
-
raw_write_dir: str,
|
|
260
|
-
*,
|
|
261
|
-
base_dir: str | Path | None = None,
|
|
262
|
-
) -> str:
|
|
263
|
-
return str(resolve_path(raw_write_dir, base_dir=base_dir, strict=False))
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def build_working_dir(
|
|
267
|
-
raw_working_dir: object,
|
|
268
|
-
*,
|
|
269
|
-
field_name: str = "working_dir",
|
|
270
|
-
) -> str:
|
|
271
|
-
if not isinstance(raw_working_dir, str):
|
|
272
|
-
raise ValueError(f"{field_name} must be a string")
|
|
273
|
-
stripped = raw_working_dir.strip()
|
|
274
|
-
if not stripped:
|
|
275
|
-
raise ValueError(f"{field_name} must not be empty")
|
|
276
|
-
try:
|
|
277
|
-
normalized = str(_resolve_path_from_base(stripped, strict=True))
|
|
278
|
-
except FileNotFoundError as exc:
|
|
279
|
-
raise ValueError(f"{field_name} must be an existing directory") from exc
|
|
280
|
-
except OSError as exc:
|
|
281
|
-
raise ValueError(f"{field_name} must be an accessible directory") from exc
|
|
282
|
-
path = Path(normalized)
|
|
283
|
-
if not path.is_dir():
|
|
284
|
-
raise ValueError(f"{field_name} must be an existing directory")
|
|
285
|
-
if not os.access(path, os.R_OK | os.X_OK):
|
|
286
|
-
raise ValueError(f"{field_name} must be an accessible directory")
|
|
287
|
-
return normalized
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
@dataclass
|
|
291
|
-
class RoleConfig:
|
|
292
|
-
name: str
|
|
293
|
-
system_prompt: str
|
|
294
|
-
description: str = ""
|
|
295
|
-
model: RoleModelConfig | None = None
|
|
296
|
-
model_params: ModelParams | None = None
|
|
297
|
-
included_tools: list[str] = field(default_factory=list)
|
|
298
|
-
excluded_tools: list[str] = field(default_factory=list)
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
@dataclass
|
|
302
|
-
class ModelSettings:
|
|
303
|
-
active_provider_id: str = ""
|
|
304
|
-
active_model: str = ""
|
|
305
|
-
input_image: bool | None = None
|
|
306
|
-
output_image: bool | None = None
|
|
307
|
-
structured_output: bool | None = None
|
|
308
|
-
context_window_tokens: int | None = None
|
|
309
|
-
params: ModelParams = field(default_factory=build_default_model_params)
|
|
310
|
-
timeout_ms: int = DEFAULT_LLM_TIMEOUT_MS
|
|
311
|
-
retry_policy: str = DEFAULT_LLM_RETRY_POLICY
|
|
312
|
-
max_retries: int = DEFAULT_LLM_MAX_RETRIES
|
|
313
|
-
retry_initial_delay_seconds: float = DEFAULT_LLM_RETRY_INITIAL_DELAY_SECONDS
|
|
314
|
-
retry_max_delay_seconds: float = DEFAULT_LLM_RETRY_MAX_DELAY_SECONDS
|
|
315
|
-
retry_backoff_cap_retries: int = DEFAULT_LLM_RETRY_BACKOFF_CAP_RETRIES
|
|
316
|
-
auto_compact_token_limit: int | None = DEFAULT_LLM_AUTO_COMPACT_TOKEN_LIMIT
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
@dataclass
|
|
320
|
-
class AssistantSettings:
|
|
321
|
-
role_name: str = STEWARD_ROLE_NAME
|
|
322
|
-
allow_network: bool = DEFAULT_ASSISTANT_ALLOW_NETWORK
|
|
323
|
-
write_dirs: list[str] = field(default_factory=build_default_assistant_write_dirs)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
@dataclass
|
|
327
|
-
class LeaderSettings:
|
|
328
|
-
role_name: str = CONDUCTOR_ROLE_NAME
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
@dataclass
|
|
332
|
-
class TelegramPendingChat:
|
|
333
|
-
chat_id: int
|
|
334
|
-
username: str | None = None
|
|
335
|
-
display_name: str = ""
|
|
336
|
-
first_seen_at: float = 0.0
|
|
337
|
-
last_seen_at: float = 0.0
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
@dataclass
|
|
341
|
-
class TelegramApprovedChat:
|
|
342
|
-
chat_id: int
|
|
343
|
-
username: str | None = None
|
|
344
|
-
display_name: str = ""
|
|
345
|
-
approved_at: float = 0.0
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
@dataclass
|
|
349
|
-
class TelegramSettings:
|
|
350
|
-
bot_token: str = ""
|
|
351
|
-
pending_chats: list[TelegramPendingChat] = field(default_factory=list)
|
|
352
|
-
approved_chats: list[TelegramApprovedChat] = field(default_factory=list)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
@dataclass
|
|
356
|
-
class Settings:
|
|
357
|
-
app_data_dir: str = field(default_factory=build_default_app_data_dir)
|
|
358
|
-
working_dir: str = field(default_factory=build_default_working_dir)
|
|
359
|
-
event_log: EventLogSettings = field(default_factory=EventLogSettings)
|
|
360
|
-
access: AccessSettings = field(default_factory=AccessSettings)
|
|
361
|
-
assistant: AssistantSettings = field(default_factory=AssistantSettings)
|
|
362
|
-
leader: LeaderSettings = field(default_factory=LeaderSettings)
|
|
363
|
-
telegram: TelegramSettings = field(default_factory=TelegramSettings)
|
|
364
|
-
model: ModelSettings = field(default_factory=ModelSettings)
|
|
365
|
-
custom_prompt: str = ""
|
|
366
|
-
custom_post_prompt: str = ""
|
|
367
|
-
providers: list[ProviderConfig] = field(default_factory=list)
|
|
368
|
-
roles: list[RoleConfig] = field(default_factory=list)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
_cached_settings: Settings | None = None
|
|
372
|
-
_cached_settings_file_signature: tuple[int, int] | None = None
|
|
373
|
-
_settings_lock = threading.Lock()
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
def normalize_tool_names(tool_names: list[str]) -> list[str]:
|
|
377
|
-
normalized: list[str] = []
|
|
378
|
-
seen: set[str] = set()
|
|
379
|
-
for tool_name in tool_names:
|
|
380
|
-
stripped = tool_name.strip()
|
|
381
|
-
name = RENAMED_TOOL_NAMES.get(stripped, stripped)
|
|
382
|
-
if not name or name in seen or name in REMOVED_TOOL_NAMES:
|
|
383
|
-
continue
|
|
384
|
-
normalized.append(name)
|
|
385
|
-
seen.add(name)
|
|
386
|
-
return normalized
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
def validate_role_tool_config(
|
|
390
|
-
included_tools: list[str],
|
|
391
|
-
excluded_tools: list[str],
|
|
392
|
-
) -> None:
|
|
393
|
-
overlap = sorted(set(included_tools) & set(excluded_tools))
|
|
394
|
-
if overlap:
|
|
395
|
-
raise ValueError(
|
|
396
|
-
"included_tools and excluded_tools cannot overlap: " + ", ".join(overlap)
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
def serialize_role_model(
|
|
401
|
-
role_model: RoleModelConfig | None,
|
|
402
|
-
) -> dict[str, str] | None:
|
|
403
|
-
if role_model is None:
|
|
404
|
-
return None
|
|
405
|
-
return {
|
|
406
|
-
"provider_id": role_model.provider_id,
|
|
407
|
-
"model": role_model.model,
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
def serialize_model_params(
|
|
412
|
-
model_params: ModelParams | None,
|
|
413
|
-
) -> dict[str, object] | None:
|
|
414
|
-
if model_params is None:
|
|
415
|
-
return None
|
|
416
|
-
return {
|
|
417
|
-
"reasoning_effort": model_params.reasoning_effort,
|
|
418
|
-
"verbosity": model_params.verbosity,
|
|
419
|
-
"max_output_tokens": model_params.max_output_tokens,
|
|
420
|
-
"temperature": model_params.temperature,
|
|
421
|
-
"top_p": model_params.top_p,
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
def is_empty_model_params(model_params: ModelParams | None) -> bool:
|
|
426
|
-
return model_params is None or all(
|
|
427
|
-
value is None for value in asdict(model_params).values()
|
|
428
|
-
)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
def merge_model_params(
|
|
432
|
-
defaults: ModelParams | None,
|
|
433
|
-
override: ModelParams | None,
|
|
434
|
-
) -> ModelParams | None:
|
|
435
|
-
merged = asdict(defaults) if defaults is not None else asdict(ModelParams())
|
|
436
|
-
if override is not None:
|
|
437
|
-
for key, value in asdict(override).items():
|
|
438
|
-
if value is not None:
|
|
439
|
-
merged[key] = value
|
|
440
|
-
params = ModelParams(**merged)
|
|
441
|
-
return None if is_empty_model_params(params) else params
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
def build_model_params_from_mapping(raw_model_params: object) -> ModelParams | None:
|
|
445
|
-
if raw_model_params is None:
|
|
446
|
-
return None
|
|
447
|
-
if not isinstance(raw_model_params, dict):
|
|
448
|
-
raise ValueError("model_params must be an object or null")
|
|
449
|
-
|
|
450
|
-
raw_reasoning_effort = raw_model_params.get("reasoning_effort")
|
|
451
|
-
raw_verbosity = raw_model_params.get("verbosity")
|
|
452
|
-
raw_max_output_tokens = raw_model_params.get("max_output_tokens")
|
|
453
|
-
raw_temperature = raw_model_params.get("temperature")
|
|
454
|
-
raw_top_p = raw_model_params.get("top_p")
|
|
455
|
-
|
|
456
|
-
if raw_reasoning_effort is not None:
|
|
457
|
-
if not isinstance(raw_reasoning_effort, str):
|
|
458
|
-
raise ValueError("model_params.reasoning_effort must be a string")
|
|
459
|
-
reasoning_effort = raw_reasoning_effort.strip().lower()
|
|
460
|
-
if reasoning_effort and reasoning_effort not in MODEL_REASONING_EFFORT_OPTIONS:
|
|
461
|
-
raise ValueError(
|
|
462
|
-
"model_params.reasoning_effort must be one of: "
|
|
463
|
-
+ ", ".join(sorted(MODEL_REASONING_EFFORT_OPTIONS))
|
|
464
|
-
)
|
|
465
|
-
else:
|
|
466
|
-
reasoning_effort = None
|
|
467
|
-
|
|
468
|
-
if raw_verbosity is not None:
|
|
469
|
-
if not isinstance(raw_verbosity, str):
|
|
470
|
-
raise ValueError("model_params.verbosity must be a string")
|
|
471
|
-
verbosity = raw_verbosity.strip().lower()
|
|
472
|
-
if verbosity and verbosity not in MODEL_VERBOSITY_OPTIONS:
|
|
473
|
-
raise ValueError(
|
|
474
|
-
"model_params.verbosity must be one of: "
|
|
475
|
-
+ ", ".join(sorted(MODEL_VERBOSITY_OPTIONS))
|
|
476
|
-
)
|
|
477
|
-
else:
|
|
478
|
-
verbosity = None
|
|
479
|
-
|
|
480
|
-
if raw_max_output_tokens is not None:
|
|
481
|
-
if isinstance(raw_max_output_tokens, bool) or not isinstance(
|
|
482
|
-
raw_max_output_tokens, int
|
|
483
|
-
):
|
|
484
|
-
raise ValueError("model_params.max_output_tokens must be an integer")
|
|
485
|
-
if raw_max_output_tokens <= 0:
|
|
486
|
-
raise ValueError("model_params.max_output_tokens must be greater than 0")
|
|
487
|
-
max_output_tokens = raw_max_output_tokens
|
|
488
|
-
else:
|
|
489
|
-
max_output_tokens = None
|
|
490
|
-
|
|
491
|
-
if raw_temperature is not None:
|
|
492
|
-
if isinstance(raw_temperature, bool) or not isinstance(
|
|
493
|
-
raw_temperature, (int, float)
|
|
494
|
-
):
|
|
495
|
-
raise ValueError("model_params.temperature must be a number")
|
|
496
|
-
temperature = float(raw_temperature)
|
|
497
|
-
if not isfinite(temperature) or temperature < 0 or temperature > 2:
|
|
498
|
-
raise ValueError("model_params.temperature must be between 0 and 2")
|
|
499
|
-
else:
|
|
500
|
-
temperature = None
|
|
501
|
-
|
|
502
|
-
if raw_top_p is not None:
|
|
503
|
-
if isinstance(raw_top_p, bool) or not isinstance(raw_top_p, (int, float)):
|
|
504
|
-
raise ValueError("model_params.top_p must be a number")
|
|
505
|
-
top_p = float(raw_top_p)
|
|
506
|
-
if not isfinite(top_p) or top_p <= 0 or top_p > 1:
|
|
507
|
-
raise ValueError("model_params.top_p must be greater than 0 and at most 1")
|
|
508
|
-
else:
|
|
509
|
-
top_p = None
|
|
510
|
-
|
|
511
|
-
params = ModelParams(
|
|
512
|
-
reasoning_effort=reasoning_effort or None,
|
|
513
|
-
verbosity=verbosity or None,
|
|
514
|
-
max_output_tokens=max_output_tokens,
|
|
515
|
-
temperature=temperature,
|
|
516
|
-
top_p=top_p,
|
|
517
|
-
)
|
|
518
|
-
return None if is_empty_model_params(params) else params
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
def build_model_max_retries(
|
|
522
|
-
raw_max_retries: object,
|
|
523
|
-
*,
|
|
524
|
-
field_name: str = "model.max_retries",
|
|
525
|
-
) -> int:
|
|
526
|
-
if isinstance(raw_max_retries, bool) or not isinstance(raw_max_retries, int):
|
|
527
|
-
raise ValueError(f"{field_name} must be an integer")
|
|
528
|
-
if raw_max_retries <= 0:
|
|
529
|
-
raise ValueError(f"{field_name} must be greater than 0")
|
|
530
|
-
return raw_max_retries
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
def build_model_input_image(
|
|
534
|
-
raw_input_image: object,
|
|
535
|
-
*,
|
|
536
|
-
field_name: str = "model.input_image",
|
|
537
|
-
) -> bool | None:
|
|
538
|
-
if raw_input_image is None:
|
|
539
|
-
return None
|
|
540
|
-
if not isinstance(raw_input_image, bool):
|
|
541
|
-
raise ValueError(f"{field_name} must be a boolean or null")
|
|
542
|
-
return raw_input_image
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
def build_model_output_image(
|
|
546
|
-
raw_output_image: object,
|
|
547
|
-
*,
|
|
548
|
-
field_name: str = "model.output_image",
|
|
549
|
-
) -> bool | None:
|
|
550
|
-
if raw_output_image is None:
|
|
551
|
-
return None
|
|
552
|
-
if not isinstance(raw_output_image, bool):
|
|
553
|
-
raise ValueError(f"{field_name} must be a boolean or null")
|
|
554
|
-
return raw_output_image
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
def build_model_structured_output(
|
|
558
|
-
raw_structured_output: object,
|
|
559
|
-
*,
|
|
560
|
-
field_name: str = "model.structured_output",
|
|
561
|
-
) -> bool | None:
|
|
562
|
-
if raw_structured_output is None:
|
|
563
|
-
return None
|
|
564
|
-
if not isinstance(raw_structured_output, bool):
|
|
565
|
-
raise ValueError(f"{field_name} must be a boolean or null")
|
|
566
|
-
return raw_structured_output
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
def build_model_context_window_tokens(
|
|
570
|
-
raw_context_window_tokens: object,
|
|
571
|
-
*,
|
|
572
|
-
field_name: str = "model.context_window_tokens",
|
|
573
|
-
) -> int | None:
|
|
574
|
-
if raw_context_window_tokens is None:
|
|
575
|
-
return None
|
|
576
|
-
if isinstance(raw_context_window_tokens, bool) or not isinstance(
|
|
577
|
-
raw_context_window_tokens, int
|
|
578
|
-
):
|
|
579
|
-
raise ValueError(f"{field_name} must be an integer or null")
|
|
580
|
-
if raw_context_window_tokens <= 0:
|
|
581
|
-
raise ValueError(f"{field_name} must be greater than 0")
|
|
582
|
-
return raw_context_window_tokens
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
def build_model_auto_compact_token_limit(
|
|
586
|
-
raw_auto_compact_token_limit: object,
|
|
587
|
-
*,
|
|
588
|
-
field_name: str = "model.auto_compact_token_limit",
|
|
589
|
-
) -> int | None:
|
|
590
|
-
if raw_auto_compact_token_limit is None:
|
|
591
|
-
return None
|
|
592
|
-
if isinstance(raw_auto_compact_token_limit, bool) or not isinstance(
|
|
593
|
-
raw_auto_compact_token_limit, int
|
|
594
|
-
):
|
|
595
|
-
raise ValueError(f"{field_name} must be an integer or null")
|
|
596
|
-
if raw_auto_compact_token_limit <= 0:
|
|
597
|
-
raise ValueError(f"{field_name} must be greater than 0")
|
|
598
|
-
return raw_auto_compact_token_limit
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
def build_assistant_allow_network(
|
|
602
|
-
raw_allow_network: object,
|
|
603
|
-
*,
|
|
604
|
-
field_name: str = "assistant.allow_network",
|
|
605
|
-
) -> bool:
|
|
606
|
-
if not isinstance(raw_allow_network, bool):
|
|
607
|
-
raise ValueError(f"{field_name} must be a boolean")
|
|
608
|
-
return raw_allow_network
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
def build_assistant_write_dirs(
|
|
612
|
-
raw_write_dirs: object,
|
|
613
|
-
*,
|
|
614
|
-
field_name: str = "assistant.write_dirs",
|
|
615
|
-
base_dir: str | Path | None = None,
|
|
616
|
-
) -> list[str]:
|
|
617
|
-
if not isinstance(raw_write_dirs, list):
|
|
618
|
-
raise ValueError(f"{field_name} must be an array of strings")
|
|
619
|
-
|
|
620
|
-
normalized: list[str] = []
|
|
621
|
-
seen: set[str] = set()
|
|
622
|
-
for raw_item in raw_write_dirs:
|
|
623
|
-
if not isinstance(raw_item, str):
|
|
624
|
-
raise ValueError(f"{field_name} must be an array of strings")
|
|
625
|
-
stripped = raw_item.strip()
|
|
626
|
-
if not stripped:
|
|
627
|
-
continue
|
|
628
|
-
normalized_item = _normalize_assistant_write_dir(
|
|
629
|
-
stripped,
|
|
630
|
-
base_dir=base_dir,
|
|
631
|
-
)
|
|
632
|
-
if normalized_item in seen:
|
|
633
|
-
continue
|
|
634
|
-
seen.add(normalized_item)
|
|
635
|
-
normalized.append(normalized_item)
|
|
636
|
-
return normalized
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
def build_model_retry_policy(
|
|
640
|
-
raw_retry_policy: object,
|
|
641
|
-
*,
|
|
642
|
-
field_name: str = "model.retry_policy",
|
|
643
|
-
) -> str:
|
|
644
|
-
if not isinstance(raw_retry_policy, str):
|
|
645
|
-
raise ValueError(f"{field_name} must be a string")
|
|
646
|
-
retry_policy = raw_retry_policy.strip().lower()
|
|
647
|
-
if retry_policy not in MODEL_RETRY_POLICY_OPTIONS:
|
|
648
|
-
raise ValueError(
|
|
649
|
-
f"{field_name} must be one of: "
|
|
650
|
-
+ ", ".join(sorted(MODEL_RETRY_POLICY_OPTIONS))
|
|
651
|
-
)
|
|
652
|
-
return retry_policy
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
def build_model_retry_initial_delay_seconds(
|
|
656
|
-
raw_delay_seconds: object,
|
|
657
|
-
*,
|
|
658
|
-
field_name: str = "model.retry_initial_delay_seconds",
|
|
659
|
-
) -> float:
|
|
660
|
-
if isinstance(raw_delay_seconds, bool) or not isinstance(
|
|
661
|
-
raw_delay_seconds, (int, float)
|
|
662
|
-
):
|
|
663
|
-
raise ValueError(f"{field_name} must be a number")
|
|
664
|
-
delay_seconds = float(raw_delay_seconds)
|
|
665
|
-
if not isfinite(delay_seconds) or delay_seconds <= 0:
|
|
666
|
-
raise ValueError(f"{field_name} must be greater than 0")
|
|
667
|
-
return delay_seconds
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
def build_model_retry_max_delay_seconds(
|
|
671
|
-
raw_delay_seconds: object,
|
|
672
|
-
*,
|
|
673
|
-
field_name: str = "model.retry_max_delay_seconds",
|
|
674
|
-
) -> float:
|
|
675
|
-
if isinstance(raw_delay_seconds, bool) or not isinstance(
|
|
676
|
-
raw_delay_seconds, (int, float)
|
|
677
|
-
):
|
|
678
|
-
raise ValueError(f"{field_name} must be a number")
|
|
679
|
-
delay_seconds = float(raw_delay_seconds)
|
|
680
|
-
if not isfinite(delay_seconds) or delay_seconds <= 0:
|
|
681
|
-
raise ValueError(f"{field_name} must be greater than 0")
|
|
682
|
-
return delay_seconds
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
def build_model_retry_backoff_cap_retries(
|
|
686
|
-
raw_cap_retries: object,
|
|
687
|
-
*,
|
|
688
|
-
field_name: str = "model.retry_backoff_cap_retries",
|
|
689
|
-
) -> int:
|
|
690
|
-
if isinstance(raw_cap_retries, bool) or not isinstance(raw_cap_retries, int):
|
|
691
|
-
raise ValueError(f"{field_name} must be an integer")
|
|
692
|
-
if raw_cap_retries <= 0:
|
|
693
|
-
raise ValueError(f"{field_name} must be greater than 0")
|
|
694
|
-
return raw_cap_retries
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
def validate_model_retry_backoff_settings(
|
|
698
|
-
*,
|
|
699
|
-
retry_initial_delay_seconds: float,
|
|
700
|
-
retry_max_delay_seconds: float,
|
|
701
|
-
) -> None:
|
|
702
|
-
if retry_max_delay_seconds < retry_initial_delay_seconds:
|
|
703
|
-
raise ValueError(
|
|
704
|
-
"model.retry_max_delay_seconds must be greater than or equal to "
|
|
705
|
-
"model.retry_initial_delay_seconds"
|
|
706
|
-
)
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
def build_model_timeout_ms(
|
|
710
|
-
raw_timeout_ms: object,
|
|
711
|
-
*,
|
|
712
|
-
field_name: str = "model.timeout_ms",
|
|
713
|
-
) -> int:
|
|
714
|
-
if isinstance(raw_timeout_ms, bool) or not isinstance(raw_timeout_ms, int):
|
|
715
|
-
raise ValueError(f"{field_name} must be an integer")
|
|
716
|
-
if raw_timeout_ms <= 0:
|
|
717
|
-
raise ValueError(f"{field_name} must be greater than 0")
|
|
718
|
-
return raw_timeout_ms
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
def build_provider_headers(
|
|
722
|
-
raw_headers: object,
|
|
723
|
-
*,
|
|
724
|
-
field_name: str = "headers",
|
|
725
|
-
) -> dict[str, str]:
|
|
726
|
-
if raw_headers is None:
|
|
727
|
-
return {}
|
|
728
|
-
if not isinstance(raw_headers, dict):
|
|
729
|
-
raise ValueError(f"{field_name} must be a JSON object")
|
|
730
|
-
|
|
731
|
-
headers: dict[str, str] = {}
|
|
732
|
-
for key, value in raw_headers.items():
|
|
733
|
-
if not isinstance(key, str) or not isinstance(value, str):
|
|
734
|
-
raise ValueError(f"{field_name} must be a JSON object of string values")
|
|
735
|
-
headers[key] = value
|
|
736
|
-
return headers
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
def build_provider_retry_429_delay_seconds(
|
|
740
|
-
raw_delay_seconds: object,
|
|
741
|
-
*,
|
|
742
|
-
field_name: str = "retry_429_delay_seconds",
|
|
743
|
-
) -> int:
|
|
744
|
-
if isinstance(raw_delay_seconds, bool) or not isinstance(raw_delay_seconds, int):
|
|
745
|
-
raise ValueError(f"{field_name} must be an integer")
|
|
746
|
-
if raw_delay_seconds < 0:
|
|
747
|
-
raise ValueError(f"{field_name} must be greater than or equal to 0")
|
|
748
|
-
return raw_delay_seconds
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
def _normalize_provider_headers(raw_headers: object) -> tuple[dict[str, str], bool]:
|
|
752
|
-
if raw_headers is None:
|
|
753
|
-
return {}, False
|
|
754
|
-
if not isinstance(raw_headers, dict):
|
|
755
|
-
return {}, True
|
|
756
|
-
|
|
757
|
-
headers: dict[str, str] = {}
|
|
758
|
-
migrated = False
|
|
759
|
-
for key, value in raw_headers.items():
|
|
760
|
-
if not isinstance(key, str) or not isinstance(value, str):
|
|
761
|
-
migrated = True
|
|
762
|
-
continue
|
|
763
|
-
headers[key] = value
|
|
764
|
-
return headers, migrated
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
def _normalize_provider_model_source(raw_source: object) -> tuple[str, bool]:
|
|
768
|
-
if raw_source is None:
|
|
769
|
-
return "manual", True
|
|
770
|
-
if not isinstance(raw_source, str):
|
|
771
|
-
return "manual", True
|
|
772
|
-
normalized = raw_source.strip().lower()
|
|
773
|
-
if normalized not in PROVIDER_MODEL_SOURCE_OPTIONS:
|
|
774
|
-
return "manual", True
|
|
775
|
-
return normalized, normalized != raw_source
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
def _normalize_provider_model_catalog_entries(
|
|
779
|
-
raw_models: object,
|
|
780
|
-
) -> tuple[list[ProviderModelCatalogEntry], bool]:
|
|
781
|
-
if raw_models is None:
|
|
782
|
-
return [], False
|
|
783
|
-
if not isinstance(raw_models, list):
|
|
784
|
-
return [], True
|
|
785
|
-
|
|
786
|
-
entries_by_model: dict[str, ProviderModelCatalogEntry] = {}
|
|
787
|
-
migrated = False
|
|
788
|
-
for raw_entry in raw_models:
|
|
789
|
-
if not isinstance(raw_entry, dict):
|
|
790
|
-
migrated = True
|
|
791
|
-
continue
|
|
792
|
-
raw_model = raw_entry.get("model")
|
|
793
|
-
if not isinstance(raw_model, str) or not raw_model.strip():
|
|
794
|
-
migrated = True
|
|
795
|
-
continue
|
|
796
|
-
model = raw_model.strip()
|
|
797
|
-
source, source_migrated = _normalize_provider_model_source(
|
|
798
|
-
raw_entry.get("source")
|
|
799
|
-
)
|
|
800
|
-
input_image, input_image_migrated = _normalize_nullable_bool(
|
|
801
|
-
raw_entry.get("input_image")
|
|
802
|
-
)
|
|
803
|
-
output_image, output_image_migrated = _normalize_nullable_bool(
|
|
804
|
-
raw_entry.get("output_image")
|
|
805
|
-
)
|
|
806
|
-
structured_output, structured_output_migrated = _normalize_nullable_bool(
|
|
807
|
-
raw_entry.get("structured_output")
|
|
808
|
-
)
|
|
809
|
-
context_window_tokens, context_window_tokens_migrated = _normalize_positive_int(
|
|
810
|
-
raw_entry.get("context_window_tokens")
|
|
811
|
-
)
|
|
812
|
-
migrated = (
|
|
813
|
-
migrated
|
|
814
|
-
or source_migrated
|
|
815
|
-
or input_image_migrated
|
|
816
|
-
or output_image_migrated
|
|
817
|
-
or structured_output_migrated
|
|
818
|
-
or context_window_tokens_migrated
|
|
819
|
-
or model != raw_model
|
|
820
|
-
or model in entries_by_model
|
|
821
|
-
)
|
|
822
|
-
entries_by_model[model] = ProviderModelCatalogEntry(
|
|
823
|
-
model=model,
|
|
824
|
-
source=source,
|
|
825
|
-
context_window_tokens=context_window_tokens,
|
|
826
|
-
input_image=input_image,
|
|
827
|
-
output_image=output_image,
|
|
828
|
-
structured_output=structured_output,
|
|
829
|
-
)
|
|
830
|
-
return list(entries_by_model.values()), migrated
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
def _fallback_role_description(role_name: str, system_prompt: str) -> str:
|
|
834
|
-
for line in system_prompt.splitlines():
|
|
835
|
-
stripped = " ".join(line.split())
|
|
836
|
-
if stripped:
|
|
837
|
-
return stripped[:160]
|
|
838
|
-
normalized_role_name = " ".join(role_name.split())
|
|
839
|
-
if normalized_role_name:
|
|
840
|
-
return f"{normalized_role_name} role."
|
|
841
|
-
return "Custom role."
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
def _normalize_role_description(
|
|
845
|
-
raw_description: object,
|
|
846
|
-
*,
|
|
847
|
-
role_name: str,
|
|
848
|
-
system_prompt: str,
|
|
849
|
-
) -> tuple[str, bool]:
|
|
850
|
-
if isinstance(raw_description, str):
|
|
851
|
-
stripped = " ".join(raw_description.split())
|
|
852
|
-
if stripped:
|
|
853
|
-
return stripped, stripped != raw_description
|
|
854
|
-
return _fallback_role_description(role_name, system_prompt), True
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
def serialize_provider_model_catalog_entry(
|
|
858
|
-
entry: ProviderModelCatalogEntry,
|
|
859
|
-
) -> dict[str, object]:
|
|
860
|
-
return {
|
|
861
|
-
"model": entry.model,
|
|
862
|
-
"source": entry.source,
|
|
863
|
-
"context_window_tokens": entry.context_window_tokens,
|
|
864
|
-
"input_image": entry.input_image,
|
|
865
|
-
"output_image": entry.output_image,
|
|
866
|
-
"structured_output": entry.structured_output,
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
def serialize_provider(provider: ProviderConfig) -> dict[str, object]:
|
|
871
|
-
return {
|
|
872
|
-
"id": provider.id,
|
|
873
|
-
"name": provider.name,
|
|
874
|
-
"type": provider.type,
|
|
875
|
-
"base_url": provider.base_url,
|
|
876
|
-
"api_key": provider.api_key,
|
|
877
|
-
"headers": dict(provider.headers),
|
|
878
|
-
"retry_429_delay_seconds": provider.retry_429_delay_seconds,
|
|
879
|
-
"models": [
|
|
880
|
-
serialize_provider_model_catalog_entry(entry) for entry in provider.models
|
|
881
|
-
],
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
def serialize_role(role: RoleConfig) -> dict[str, object]:
|
|
886
|
-
return {
|
|
887
|
-
"name": role.name,
|
|
888
|
-
"description": role.description,
|
|
889
|
-
"system_prompt": role.system_prompt,
|
|
890
|
-
"model": serialize_role_model(role.model),
|
|
891
|
-
"model_params": serialize_model_params(role.model_params),
|
|
892
|
-
"included_tools": list(role.included_tools),
|
|
893
|
-
"excluded_tools": list(role.excluded_tools),
|
|
894
|
-
"is_builtin": is_builtin_role_name(role.name),
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
def mask_secret(secret: str) -> str:
|
|
899
|
-
if not secret:
|
|
900
|
-
return ""
|
|
901
|
-
return f"sk-...{secret[-4:]}"
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
def serialize_telegram_settings(
|
|
905
|
-
telegram: TelegramSettings,
|
|
906
|
-
*,
|
|
907
|
-
mask_token: bool = True,
|
|
908
|
-
) -> dict[str, object]:
|
|
909
|
-
return {
|
|
910
|
-
"bot_token": mask_secret(telegram.bot_token)
|
|
911
|
-
if mask_token
|
|
912
|
-
else telegram.bot_token,
|
|
913
|
-
"pending_chats": [asdict(chat) for chat in telegram.pending_chats],
|
|
914
|
-
"approved_chats": [asdict(chat) for chat in telegram.approved_chats],
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
def serialize_settings(
|
|
919
|
-
settings: Settings,
|
|
920
|
-
*,
|
|
921
|
-
mask_telegram_token: bool = True,
|
|
922
|
-
) -> dict[str, object]:
|
|
923
|
-
data = asdict(settings)
|
|
924
|
-
provider = find_provider(settings, settings.model.active_provider_id)
|
|
925
|
-
if provider is None or not settings.model.active_model.strip():
|
|
926
|
-
model_info = None
|
|
927
|
-
else:
|
|
928
|
-
model_info = resolve_model_info(
|
|
929
|
-
provider=provider,
|
|
930
|
-
model_id=settings.model.active_model,
|
|
931
|
-
input_image=settings.model.input_image,
|
|
932
|
-
output_image=settings.model.output_image,
|
|
933
|
-
structured_output=settings.model.structured_output,
|
|
934
|
-
context_window_tokens=settings.model.context_window_tokens,
|
|
935
|
-
)
|
|
936
|
-
data["model"]["capabilities"] = (
|
|
937
|
-
asdict(model_info.capabilities) if model_info is not None else None
|
|
938
|
-
)
|
|
939
|
-
data["model"]["resolved_context_window_tokens"] = (
|
|
940
|
-
model_info.context_window_tokens if model_info is not None else None
|
|
941
|
-
)
|
|
942
|
-
data["telegram"] = serialize_telegram_settings(
|
|
943
|
-
settings.telegram,
|
|
944
|
-
mask_token=mask_telegram_token,
|
|
945
|
-
)
|
|
946
|
-
data["access"] = {
|
|
947
|
-
"configured": bool(
|
|
948
|
-
settings.access.code.strip()
|
|
949
|
-
and settings.access.code_hash.strip()
|
|
950
|
-
and settings.access.code_salt.strip()
|
|
951
|
-
)
|
|
952
|
-
}
|
|
953
|
-
return data
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
def find_provider_model_catalog_entry(
|
|
957
|
-
provider: ProviderConfig,
|
|
958
|
-
model_id: str,
|
|
959
|
-
) -> ProviderModelCatalogEntry | None:
|
|
960
|
-
normalized_model_id = model_id.strip()
|
|
961
|
-
if not normalized_model_id:
|
|
962
|
-
return None
|
|
963
|
-
for entry in provider.models:
|
|
964
|
-
if entry.model.strip() == normalized_model_id:
|
|
965
|
-
return entry
|
|
966
|
-
return None
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
def resolve_model_info(
|
|
970
|
-
*,
|
|
971
|
-
provider: ProviderConfig,
|
|
972
|
-
model_id: str,
|
|
973
|
-
input_image: bool | None = None,
|
|
974
|
-
output_image: bool | None = None,
|
|
975
|
-
structured_output: bool | None = None,
|
|
976
|
-
context_window_tokens: int | None = None,
|
|
977
|
-
):
|
|
978
|
-
from flowent.model_metadata import build_model_info
|
|
979
|
-
|
|
980
|
-
catalog_entry = find_provider_model_catalog_entry(provider, model_id)
|
|
981
|
-
return build_model_info(
|
|
982
|
-
provider_type=provider.type,
|
|
983
|
-
model_id=model_id,
|
|
984
|
-
input_image=(
|
|
985
|
-
input_image
|
|
986
|
-
if input_image is not None
|
|
987
|
-
else catalog_entry.input_image
|
|
988
|
-
if catalog_entry is not None
|
|
989
|
-
else None
|
|
990
|
-
),
|
|
991
|
-
output_image=(
|
|
992
|
-
output_image
|
|
993
|
-
if output_image is not None
|
|
994
|
-
else catalog_entry.output_image
|
|
995
|
-
if catalog_entry is not None
|
|
996
|
-
else None
|
|
997
|
-
),
|
|
998
|
-
structured_output=(
|
|
999
|
-
structured_output
|
|
1000
|
-
if structured_output is not None
|
|
1001
|
-
else catalog_entry.structured_output
|
|
1002
|
-
if catalog_entry is not None
|
|
1003
|
-
else None
|
|
1004
|
-
),
|
|
1005
|
-
context_window_tokens=(
|
|
1006
|
-
context_window_tokens
|
|
1007
|
-
if context_window_tokens is not None
|
|
1008
|
-
else catalog_entry.context_window_tokens
|
|
1009
|
-
if catalog_entry is not None
|
|
1010
|
-
else None
|
|
1011
|
-
),
|
|
1012
|
-
)
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
def _normalize_role_model(
|
|
1016
|
-
raw_role_model: object,
|
|
1017
|
-
*,
|
|
1018
|
-
default_provider_id: str,
|
|
1019
|
-
) -> tuple[RoleModelConfig | None, bool]:
|
|
1020
|
-
if raw_role_model is None:
|
|
1021
|
-
return None, False
|
|
1022
|
-
|
|
1023
|
-
if isinstance(raw_role_model, dict):
|
|
1024
|
-
provider_id = str(raw_role_model.get("provider_id", "")).strip()
|
|
1025
|
-
model = str(raw_role_model.get("model", "")).strip()
|
|
1026
|
-
if provider_id and model:
|
|
1027
|
-
return RoleModelConfig(provider_id=provider_id, model=model), False
|
|
1028
|
-
if model and default_provider_id:
|
|
1029
|
-
return (
|
|
1030
|
-
RoleModelConfig(provider_id=default_provider_id, model=model),
|
|
1031
|
-
True,
|
|
1032
|
-
)
|
|
1033
|
-
return None, bool(provider_id or model)
|
|
1034
|
-
|
|
1035
|
-
if isinstance(raw_role_model, str):
|
|
1036
|
-
model = raw_role_model.strip()
|
|
1037
|
-
if model and default_provider_id:
|
|
1038
|
-
return (
|
|
1039
|
-
RoleModelConfig(provider_id=default_provider_id, model=model),
|
|
1040
|
-
True,
|
|
1041
|
-
)
|
|
1042
|
-
return None, True
|
|
1043
|
-
|
|
1044
|
-
return None, True
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
def _normalize_model_param_choice(
|
|
1048
|
-
raw_value: object,
|
|
1049
|
-
*,
|
|
1050
|
-
allowed: frozenset[str],
|
|
1051
|
-
) -> tuple[str | None, bool]:
|
|
1052
|
-
if raw_value is None:
|
|
1053
|
-
return None, False
|
|
1054
|
-
if not isinstance(raw_value, str):
|
|
1055
|
-
return None, True
|
|
1056
|
-
|
|
1057
|
-
value = raw_value.strip().lower()
|
|
1058
|
-
if not value:
|
|
1059
|
-
return None, raw_value != ""
|
|
1060
|
-
if value not in allowed:
|
|
1061
|
-
return None, True
|
|
1062
|
-
return value, value != raw_value
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
def _normalize_positive_int(raw_value: object) -> tuple[int | None, bool]:
|
|
1066
|
-
if raw_value is None:
|
|
1067
|
-
return None, False
|
|
1068
|
-
if isinstance(raw_value, bool):
|
|
1069
|
-
return None, True
|
|
1070
|
-
if isinstance(raw_value, int):
|
|
1071
|
-
return (raw_value, False) if raw_value > 0 else (None, True)
|
|
1072
|
-
if isinstance(raw_value, float) and raw_value.is_integer():
|
|
1073
|
-
value = int(raw_value)
|
|
1074
|
-
return (value, True) if value > 0 else (None, True)
|
|
1075
|
-
return None, True
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
def _normalize_nullable_bool(raw_value: object) -> tuple[bool | None, bool]:
|
|
1079
|
-
if raw_value is None:
|
|
1080
|
-
return None, False
|
|
1081
|
-
if isinstance(raw_value, bool):
|
|
1082
|
-
return raw_value, False
|
|
1083
|
-
return None, True
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
def _normalize_temperature(raw_value: object) -> tuple[float | None, bool]:
|
|
1087
|
-
if raw_value is None:
|
|
1088
|
-
return None, False
|
|
1089
|
-
if isinstance(raw_value, bool):
|
|
1090
|
-
return None, True
|
|
1091
|
-
if not isinstance(raw_value, (int, float)):
|
|
1092
|
-
return None, True
|
|
1093
|
-
|
|
1094
|
-
value = float(raw_value)
|
|
1095
|
-
if not isfinite(value) or value < 0 or value > 2:
|
|
1096
|
-
return None, True
|
|
1097
|
-
return value, False
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
def _normalize_top_p(raw_value: object) -> tuple[float | None, bool]:
|
|
1101
|
-
if raw_value is None:
|
|
1102
|
-
return None, False
|
|
1103
|
-
if isinstance(raw_value, bool):
|
|
1104
|
-
return None, True
|
|
1105
|
-
if not isinstance(raw_value, (int, float)):
|
|
1106
|
-
return None, True
|
|
1107
|
-
|
|
1108
|
-
value = float(raw_value)
|
|
1109
|
-
if not isfinite(value) or value <= 0 or value > 1:
|
|
1110
|
-
return None, True
|
|
1111
|
-
return value, False
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
def _normalize_optional_model_params(
|
|
1115
|
-
raw_model_params: object,
|
|
1116
|
-
) -> tuple[ModelParams | None, bool]:
|
|
1117
|
-
if raw_model_params is None:
|
|
1118
|
-
return None, False
|
|
1119
|
-
if not isinstance(raw_model_params, dict):
|
|
1120
|
-
return None, True
|
|
1121
|
-
|
|
1122
|
-
reasoning_effort, migrated_reasoning = _normalize_model_param_choice(
|
|
1123
|
-
raw_model_params.get("reasoning_effort"),
|
|
1124
|
-
allowed=MODEL_REASONING_EFFORT_OPTIONS,
|
|
1125
|
-
)
|
|
1126
|
-
verbosity, migrated_verbosity = _normalize_model_param_choice(
|
|
1127
|
-
raw_model_params.get("verbosity"),
|
|
1128
|
-
allowed=MODEL_VERBOSITY_OPTIONS,
|
|
1129
|
-
)
|
|
1130
|
-
max_output_tokens, migrated_max_output_tokens = _normalize_positive_int(
|
|
1131
|
-
raw_model_params.get("max_output_tokens")
|
|
1132
|
-
)
|
|
1133
|
-
temperature, migrated_temperature = _normalize_temperature(
|
|
1134
|
-
raw_model_params.get("temperature")
|
|
1135
|
-
)
|
|
1136
|
-
top_p, migrated_top_p = _normalize_top_p(raw_model_params.get("top_p"))
|
|
1137
|
-
|
|
1138
|
-
params = ModelParams(
|
|
1139
|
-
reasoning_effort=reasoning_effort,
|
|
1140
|
-
verbosity=verbosity,
|
|
1141
|
-
max_output_tokens=max_output_tokens,
|
|
1142
|
-
temperature=temperature,
|
|
1143
|
-
top_p=top_p,
|
|
1144
|
-
)
|
|
1145
|
-
migrated = (
|
|
1146
|
-
migrated_reasoning
|
|
1147
|
-
or migrated_verbosity
|
|
1148
|
-
or migrated_max_output_tokens
|
|
1149
|
-
or migrated_temperature
|
|
1150
|
-
or migrated_top_p
|
|
1151
|
-
)
|
|
1152
|
-
|
|
1153
|
-
if is_empty_model_params(params):
|
|
1154
|
-
return None, migrated or bool(raw_model_params)
|
|
1155
|
-
|
|
1156
|
-
return params, migrated
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
def _normalize_model_params_with_defaults(
|
|
1160
|
-
raw_model_params: object,
|
|
1161
|
-
) -> tuple[ModelParams, bool]:
|
|
1162
|
-
params, migrated = _normalize_optional_model_params(raw_model_params)
|
|
1163
|
-
if params is not None:
|
|
1164
|
-
return params, migrated
|
|
1165
|
-
return build_default_model_params(), migrated or raw_model_params is None
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
def _normalize_int_list(raw_values: object) -> tuple[list[int], bool]:
|
|
1169
|
-
if raw_values is None:
|
|
1170
|
-
return [], False
|
|
1171
|
-
if not isinstance(raw_values, list):
|
|
1172
|
-
return [], True
|
|
1173
|
-
|
|
1174
|
-
normalized: list[int] = []
|
|
1175
|
-
migrated = False
|
|
1176
|
-
for raw_value in raw_values:
|
|
1177
|
-
if isinstance(raw_value, bool):
|
|
1178
|
-
migrated = True
|
|
1179
|
-
continue
|
|
1180
|
-
if isinstance(raw_value, int):
|
|
1181
|
-
value = raw_value
|
|
1182
|
-
elif isinstance(raw_value, float) and raw_value.is_integer():
|
|
1183
|
-
value = int(raw_value)
|
|
1184
|
-
migrated = True
|
|
1185
|
-
elif isinstance(raw_value, str):
|
|
1186
|
-
stripped = raw_value.strip()
|
|
1187
|
-
if not stripped:
|
|
1188
|
-
migrated = True
|
|
1189
|
-
continue
|
|
1190
|
-
try:
|
|
1191
|
-
value = int(stripped)
|
|
1192
|
-
except ValueError:
|
|
1193
|
-
migrated = True
|
|
1194
|
-
continue
|
|
1195
|
-
migrated = True
|
|
1196
|
-
else:
|
|
1197
|
-
migrated = True
|
|
1198
|
-
continue
|
|
1199
|
-
|
|
1200
|
-
if value in normalized:
|
|
1201
|
-
migrated = True
|
|
1202
|
-
continue
|
|
1203
|
-
normalized.append(value)
|
|
1204
|
-
|
|
1205
|
-
return normalized, migrated
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
def _normalize_float(raw_value: object) -> tuple[float, bool]:
|
|
1209
|
-
if isinstance(raw_value, bool):
|
|
1210
|
-
return 0.0, True
|
|
1211
|
-
if isinstance(raw_value, (int, float)):
|
|
1212
|
-
value = float(raw_value)
|
|
1213
|
-
return value, False
|
|
1214
|
-
if isinstance(raw_value, str):
|
|
1215
|
-
stripped = raw_value.strip()
|
|
1216
|
-
if not stripped:
|
|
1217
|
-
return 0.0, True
|
|
1218
|
-
try:
|
|
1219
|
-
return float(stripped), True
|
|
1220
|
-
except ValueError:
|
|
1221
|
-
return 0.0, True
|
|
1222
|
-
return 0.0, raw_value is not None
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
def _normalize_optional_string(raw_value: object) -> tuple[str | None, bool]:
|
|
1226
|
-
if raw_value is None:
|
|
1227
|
-
return None, False
|
|
1228
|
-
if not isinstance(raw_value, str):
|
|
1229
|
-
return None, True
|
|
1230
|
-
stripped = raw_value.strip()
|
|
1231
|
-
if not stripped:
|
|
1232
|
-
return None, raw_value != ""
|
|
1233
|
-
return stripped, stripped != raw_value
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
def _normalize_required_string(raw_value: object) -> tuple[str, bool]:
|
|
1237
|
-
if not isinstance(raw_value, str):
|
|
1238
|
-
return "", raw_value is not None
|
|
1239
|
-
stripped = raw_value.strip()
|
|
1240
|
-
return stripped, stripped != raw_value
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
def _build_pending_chat(raw_chat: object) -> tuple[TelegramPendingChat | None, bool]:
|
|
1244
|
-
if not isinstance(raw_chat, dict):
|
|
1245
|
-
return None, True
|
|
1246
|
-
|
|
1247
|
-
chat_id_value = raw_chat.get("chat_id")
|
|
1248
|
-
if isinstance(chat_id_value, bool) or not isinstance(chat_id_value, int):
|
|
1249
|
-
return None, True
|
|
1250
|
-
|
|
1251
|
-
username, username_migrated = _normalize_optional_string(raw_chat.get("username"))
|
|
1252
|
-
display_name, display_name_migrated = _normalize_required_string(
|
|
1253
|
-
raw_chat.get("display_name")
|
|
1254
|
-
)
|
|
1255
|
-
first_seen_at, first_seen_migrated = _normalize_float(raw_chat.get("first_seen_at"))
|
|
1256
|
-
last_seen_at, last_seen_migrated = _normalize_float(raw_chat.get("last_seen_at"))
|
|
1257
|
-
|
|
1258
|
-
pending_chat = TelegramPendingChat(
|
|
1259
|
-
chat_id=chat_id_value,
|
|
1260
|
-
username=username,
|
|
1261
|
-
display_name=display_name,
|
|
1262
|
-
first_seen_at=first_seen_at,
|
|
1263
|
-
last_seen_at=last_seen_at,
|
|
1264
|
-
)
|
|
1265
|
-
migrated = (
|
|
1266
|
-
username_migrated
|
|
1267
|
-
or display_name_migrated
|
|
1268
|
-
or first_seen_migrated
|
|
1269
|
-
or last_seen_migrated
|
|
1270
|
-
)
|
|
1271
|
-
return pending_chat, migrated
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
def _build_approved_chat(
|
|
1275
|
-
raw_chat: object,
|
|
1276
|
-
) -> tuple[TelegramApprovedChat | None, bool]:
|
|
1277
|
-
if not isinstance(raw_chat, dict):
|
|
1278
|
-
return None, True
|
|
1279
|
-
|
|
1280
|
-
chat_id_value = raw_chat.get("chat_id")
|
|
1281
|
-
if isinstance(chat_id_value, bool) or not isinstance(chat_id_value, int):
|
|
1282
|
-
return None, True
|
|
1283
|
-
|
|
1284
|
-
username, username_migrated = _normalize_optional_string(raw_chat.get("username"))
|
|
1285
|
-
display_name, display_name_migrated = _normalize_required_string(
|
|
1286
|
-
raw_chat.get("display_name")
|
|
1287
|
-
)
|
|
1288
|
-
approved_at, approved_at_migrated = _normalize_float(raw_chat.get("approved_at"))
|
|
1289
|
-
|
|
1290
|
-
approved_chat = TelegramApprovedChat(
|
|
1291
|
-
chat_id=chat_id_value,
|
|
1292
|
-
username=username,
|
|
1293
|
-
display_name=display_name,
|
|
1294
|
-
approved_at=approved_at,
|
|
1295
|
-
)
|
|
1296
|
-
migrated = username_migrated or display_name_migrated or approved_at_migrated
|
|
1297
|
-
return approved_chat, migrated
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
def _normalize_pending_chats(
|
|
1301
|
-
raw_chats: object,
|
|
1302
|
-
) -> tuple[list[TelegramPendingChat], bool]:
|
|
1303
|
-
if raw_chats is None:
|
|
1304
|
-
return [], False
|
|
1305
|
-
if not isinstance(raw_chats, list):
|
|
1306
|
-
return [], True
|
|
1307
|
-
|
|
1308
|
-
normalized: list[TelegramPendingChat] = []
|
|
1309
|
-
seen_chat_ids: set[int] = set()
|
|
1310
|
-
migrated = False
|
|
1311
|
-
for raw_chat in raw_chats:
|
|
1312
|
-
chat, chat_migrated = _build_pending_chat(raw_chat)
|
|
1313
|
-
migrated = migrated or chat_migrated
|
|
1314
|
-
if chat is None:
|
|
1315
|
-
continue
|
|
1316
|
-
if chat.chat_id in seen_chat_ids:
|
|
1317
|
-
migrated = True
|
|
1318
|
-
continue
|
|
1319
|
-
seen_chat_ids.add(chat.chat_id)
|
|
1320
|
-
normalized.append(chat)
|
|
1321
|
-
return normalized, migrated
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
def _normalize_approved_chats(
|
|
1325
|
-
raw_chats: object,
|
|
1326
|
-
) -> tuple[list[TelegramApprovedChat], bool]:
|
|
1327
|
-
if raw_chats is None:
|
|
1328
|
-
return [], False
|
|
1329
|
-
if not isinstance(raw_chats, list):
|
|
1330
|
-
return [], True
|
|
1331
|
-
|
|
1332
|
-
normalized: list[TelegramApprovedChat] = []
|
|
1333
|
-
seen_chat_ids: set[int] = set()
|
|
1334
|
-
migrated = False
|
|
1335
|
-
for raw_chat in raw_chats:
|
|
1336
|
-
chat, chat_migrated = _build_approved_chat(raw_chat)
|
|
1337
|
-
migrated = migrated or chat_migrated
|
|
1338
|
-
if chat is None:
|
|
1339
|
-
continue
|
|
1340
|
-
if chat.chat_id in seen_chat_ids:
|
|
1341
|
-
migrated = True
|
|
1342
|
-
continue
|
|
1343
|
-
seen_chat_ids.add(chat.chat_id)
|
|
1344
|
-
normalized.append(chat)
|
|
1345
|
-
return normalized, migrated
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
def _build_settings(data: dict[str, object]) -> tuple[Settings, bool]:
|
|
1349
|
-
migrated = False
|
|
1350
|
-
|
|
1351
|
-
event_log_data = data.get("event_log", {})
|
|
1352
|
-
if not isinstance(event_log_data, dict):
|
|
1353
|
-
event_log_data = {}
|
|
1354
|
-
event_log = EventLogSettings(**event_log_data)
|
|
1355
|
-
|
|
1356
|
-
app_data_dir = build_default_app_data_dir()
|
|
1357
|
-
raw_app_data_dir = data.get("app_data_dir")
|
|
1358
|
-
if (
|
|
1359
|
-
raw_app_data_dir is None
|
|
1360
|
-
or not isinstance(raw_app_data_dir, str)
|
|
1361
|
-
or raw_app_data_dir.strip() != app_data_dir
|
|
1362
|
-
):
|
|
1363
|
-
migrated = True
|
|
1364
|
-
|
|
1365
|
-
raw_working_dir = data.get("working_dir")
|
|
1366
|
-
if raw_working_dir is None:
|
|
1367
|
-
working_dir = build_default_working_dir()
|
|
1368
|
-
migrated = True
|
|
1369
|
-
else:
|
|
1370
|
-
try:
|
|
1371
|
-
working_dir = build_working_dir(raw_working_dir)
|
|
1372
|
-
if not isinstance(raw_working_dir, str) or working_dir != raw_working_dir:
|
|
1373
|
-
migrated = True
|
|
1374
|
-
except ValueError:
|
|
1375
|
-
working_dir = build_default_working_dir()
|
|
1376
|
-
migrated = True
|
|
1377
|
-
|
|
1378
|
-
access_data = data.get("access", {})
|
|
1379
|
-
if access_data is None:
|
|
1380
|
-
access_data = {}
|
|
1381
|
-
if not isinstance(access_data, dict):
|
|
1382
|
-
access_data = {}
|
|
1383
|
-
migrated = True
|
|
1384
|
-
raw_access_code_hash = access_data.get("code_hash", "")
|
|
1385
|
-
raw_access_code_salt = access_data.get("code_salt", "")
|
|
1386
|
-
raw_access_code = access_data.get("code", "")
|
|
1387
|
-
raw_access_session_generation = access_data.get("session_generation", 0)
|
|
1388
|
-
raw_access_session_signing_secret = access_data.get("session_signing_secret", "")
|
|
1389
|
-
access_code = raw_access_code.strip() if isinstance(raw_access_code, str) else ""
|
|
1390
|
-
access_code_hash = (
|
|
1391
|
-
raw_access_code_hash.strip() if isinstance(raw_access_code_hash, str) else ""
|
|
1392
|
-
)
|
|
1393
|
-
access_code_salt = (
|
|
1394
|
-
raw_access_code_salt.strip() if isinstance(raw_access_code_salt, str) else ""
|
|
1395
|
-
)
|
|
1396
|
-
access_session_signing_secret = (
|
|
1397
|
-
raw_access_session_signing_secret.strip()
|
|
1398
|
-
if isinstance(raw_access_session_signing_secret, str)
|
|
1399
|
-
else ""
|
|
1400
|
-
)
|
|
1401
|
-
if raw_access_code is not None and not isinstance(raw_access_code, str):
|
|
1402
|
-
migrated = True
|
|
1403
|
-
if raw_access_code_hash is not None and not isinstance(raw_access_code_hash, str):
|
|
1404
|
-
migrated = True
|
|
1405
|
-
if raw_access_code_salt is not None and not isinstance(raw_access_code_salt, str):
|
|
1406
|
-
migrated = True
|
|
1407
|
-
if raw_access_session_signing_secret is not None and not isinstance(
|
|
1408
|
-
raw_access_session_signing_secret,
|
|
1409
|
-
str,
|
|
1410
|
-
):
|
|
1411
|
-
migrated = True
|
|
1412
|
-
if isinstance(raw_access_session_generation, bool) or not isinstance(
|
|
1413
|
-
raw_access_session_generation,
|
|
1414
|
-
int,
|
|
1415
|
-
):
|
|
1416
|
-
access_session_generation = 0
|
|
1417
|
-
if "session_generation" in access_data:
|
|
1418
|
-
migrated = True
|
|
1419
|
-
else:
|
|
1420
|
-
access_session_generation = max(raw_access_session_generation, 0)
|
|
1421
|
-
if access_session_generation != raw_access_session_generation:
|
|
1422
|
-
migrated = True
|
|
1423
|
-
if not access_code or not access_code_hash or not access_code_salt:
|
|
1424
|
-
if access_code or access_code_hash or access_code_salt:
|
|
1425
|
-
migrated = True
|
|
1426
|
-
access_code = ""
|
|
1427
|
-
access_code_hash = ""
|
|
1428
|
-
access_code_salt = ""
|
|
1429
|
-
access = AccessSettings(
|
|
1430
|
-
code=access_code,
|
|
1431
|
-
code_hash=access_code_hash,
|
|
1432
|
-
code_salt=access_code_salt,
|
|
1433
|
-
session_generation=access_session_generation,
|
|
1434
|
-
session_signing_secret=access_session_signing_secret,
|
|
1435
|
-
)
|
|
1436
|
-
|
|
1437
|
-
assistant_data = data.get("assistant", {})
|
|
1438
|
-
if not isinstance(assistant_data, dict):
|
|
1439
|
-
assistant_data = {}
|
|
1440
|
-
migrated = True
|
|
1441
|
-
if "assistant" not in data:
|
|
1442
|
-
migrated = True
|
|
1443
|
-
assistant_role_name = assistant_data.get("role_name")
|
|
1444
|
-
raw_assistant_allow_network = assistant_data.get("allow_network")
|
|
1445
|
-
if raw_assistant_allow_network is None:
|
|
1446
|
-
assistant_allow_network = DEFAULT_ASSISTANT_ALLOW_NETWORK
|
|
1447
|
-
migrated = True
|
|
1448
|
-
else:
|
|
1449
|
-
try:
|
|
1450
|
-
assistant_allow_network = build_assistant_allow_network(
|
|
1451
|
-
raw_assistant_allow_network
|
|
1452
|
-
)
|
|
1453
|
-
except ValueError:
|
|
1454
|
-
assistant_allow_network = DEFAULT_ASSISTANT_ALLOW_NETWORK
|
|
1455
|
-
migrated = True
|
|
1456
|
-
raw_assistant_write_dirs = assistant_data.get("write_dirs")
|
|
1457
|
-
if raw_assistant_write_dirs is None or not isinstance(
|
|
1458
|
-
raw_assistant_write_dirs, list
|
|
1459
|
-
):
|
|
1460
|
-
assistant_write_dirs = build_default_assistant_write_dirs(working_dir)
|
|
1461
|
-
migrated = True
|
|
1462
|
-
else:
|
|
1463
|
-
assistant_write_dirs = []
|
|
1464
|
-
seen_assistant_write_dirs: set[str] = set()
|
|
1465
|
-
for raw_item in raw_assistant_write_dirs:
|
|
1466
|
-
if not isinstance(raw_item, str):
|
|
1467
|
-
migrated = True
|
|
1468
|
-
continue
|
|
1469
|
-
stripped = raw_item.strip()
|
|
1470
|
-
if not stripped:
|
|
1471
|
-
migrated = True
|
|
1472
|
-
continue
|
|
1473
|
-
normalized_item = _normalize_assistant_write_dir(
|
|
1474
|
-
stripped,
|
|
1475
|
-
base_dir=working_dir,
|
|
1476
|
-
)
|
|
1477
|
-
if normalized_item != raw_item:
|
|
1478
|
-
migrated = True
|
|
1479
|
-
if normalized_item in seen_assistant_write_dirs:
|
|
1480
|
-
migrated = True
|
|
1481
|
-
continue
|
|
1482
|
-
seen_assistant_write_dirs.add(normalized_item)
|
|
1483
|
-
assistant_write_dirs.append(normalized_item)
|
|
1484
|
-
if "mcp_servers" in assistant_data:
|
|
1485
|
-
migrated = True
|
|
1486
|
-
assistant = AssistantSettings(
|
|
1487
|
-
role_name=assistant_role_name.strip()
|
|
1488
|
-
if isinstance(assistant_role_name, str) and assistant_role_name.strip()
|
|
1489
|
-
else STEWARD_ROLE_NAME,
|
|
1490
|
-
allow_network=assistant_allow_network,
|
|
1491
|
-
write_dirs=assistant_write_dirs,
|
|
1492
|
-
)
|
|
1493
|
-
if assistant.role_name == STEWARD_ROLE_NAME and (
|
|
1494
|
-
not isinstance(assistant_role_name, str) or not assistant_role_name.strip()
|
|
1495
|
-
):
|
|
1496
|
-
migrated = True
|
|
1497
|
-
|
|
1498
|
-
leader_data = data.get("leader", {})
|
|
1499
|
-
if not isinstance(leader_data, dict):
|
|
1500
|
-
leader_data = {}
|
|
1501
|
-
migrated = True
|
|
1502
|
-
if "leader" not in data:
|
|
1503
|
-
migrated = True
|
|
1504
|
-
leader_role_name = leader_data.get("role_name")
|
|
1505
|
-
leader = LeaderSettings(
|
|
1506
|
-
role_name=leader_role_name.strip()
|
|
1507
|
-
if isinstance(leader_role_name, str) and leader_role_name.strip()
|
|
1508
|
-
else CONDUCTOR_ROLE_NAME
|
|
1509
|
-
)
|
|
1510
|
-
if leader.role_name == CONDUCTOR_ROLE_NAME and (
|
|
1511
|
-
not isinstance(leader_role_name, str) or not leader_role_name.strip()
|
|
1512
|
-
):
|
|
1513
|
-
migrated = True
|
|
1514
|
-
|
|
1515
|
-
telegram_data = data.get("telegram", {})
|
|
1516
|
-
if not isinstance(telegram_data, dict):
|
|
1517
|
-
telegram_data = {}
|
|
1518
|
-
migrated = True
|
|
1519
|
-
if "telegram" not in data:
|
|
1520
|
-
migrated = True
|
|
1521
|
-
bot_token = telegram_data.get("bot_token", "")
|
|
1522
|
-
pending_chats, pending_chats_migrated = _normalize_pending_chats(
|
|
1523
|
-
telegram_data.get("pending_chats")
|
|
1524
|
-
)
|
|
1525
|
-
approved_chats, approved_chats_migrated = _normalize_approved_chats(
|
|
1526
|
-
telegram_data.get("approved_chats")
|
|
1527
|
-
)
|
|
1528
|
-
migrated = migrated or pending_chats_migrated or approved_chats_migrated
|
|
1529
|
-
if "pending_links" in telegram_data:
|
|
1530
|
-
migrated = True
|
|
1531
|
-
if "allowed_user_ids" in telegram_data or "registered_chat_ids" in telegram_data:
|
|
1532
|
-
migrated = True
|
|
1533
|
-
legacy_registered_chat_ids, _ = _normalize_int_list(
|
|
1534
|
-
telegram_data.get("registered_chat_ids")
|
|
1535
|
-
)
|
|
1536
|
-
for chat_id in legacy_registered_chat_ids:
|
|
1537
|
-
if any(chat.chat_id == chat_id for chat in approved_chats):
|
|
1538
|
-
continue
|
|
1539
|
-
approved_chats.append(
|
|
1540
|
-
TelegramApprovedChat(
|
|
1541
|
-
chat_id=chat_id,
|
|
1542
|
-
approved_at=0.0,
|
|
1543
|
-
)
|
|
1544
|
-
)
|
|
1545
|
-
telegram = TelegramSettings(
|
|
1546
|
-
bot_token=bot_token.strip() if isinstance(bot_token, str) else "",
|
|
1547
|
-
pending_chats=pending_chats,
|
|
1548
|
-
approved_chats=approved_chats,
|
|
1549
|
-
)
|
|
1550
|
-
if bot_token is not None and not isinstance(bot_token, str):
|
|
1551
|
-
migrated = True
|
|
1552
|
-
|
|
1553
|
-
model_data = data.get("model", {})
|
|
1554
|
-
if not isinstance(model_data, dict):
|
|
1555
|
-
model_data = {}
|
|
1556
|
-
migrated = True
|
|
1557
|
-
model_params, model_params_migrated = _normalize_model_params_with_defaults(
|
|
1558
|
-
model_data.get("params")
|
|
1559
|
-
)
|
|
1560
|
-
migrated = migrated or model_params_migrated
|
|
1561
|
-
raw_model_retry_policy = model_data.get("retry_policy")
|
|
1562
|
-
if raw_model_retry_policy is None:
|
|
1563
|
-
model_retry_policy = DEFAULT_LLM_RETRY_POLICY
|
|
1564
|
-
migrated = True
|
|
1565
|
-
else:
|
|
1566
|
-
try:
|
|
1567
|
-
model_retry_policy = build_model_retry_policy(raw_model_retry_policy)
|
|
1568
|
-
except ValueError:
|
|
1569
|
-
model_retry_policy = DEFAULT_LLM_RETRY_POLICY
|
|
1570
|
-
migrated = True
|
|
1571
|
-
raw_model_max_retries = model_data.get("max_retries")
|
|
1572
|
-
if raw_model_max_retries is None:
|
|
1573
|
-
model_max_retries = DEFAULT_LLM_MAX_RETRIES
|
|
1574
|
-
migrated = True
|
|
1575
|
-
else:
|
|
1576
|
-
try:
|
|
1577
|
-
model_max_retries = build_model_max_retries(raw_model_max_retries)
|
|
1578
|
-
except ValueError:
|
|
1579
|
-
model_max_retries = DEFAULT_LLM_MAX_RETRIES
|
|
1580
|
-
migrated = True
|
|
1581
|
-
raw_retry_initial_delay_seconds = model_data.get("retry_initial_delay_seconds")
|
|
1582
|
-
if raw_retry_initial_delay_seconds is None:
|
|
1583
|
-
retry_initial_delay_seconds = DEFAULT_LLM_RETRY_INITIAL_DELAY_SECONDS
|
|
1584
|
-
migrated = True
|
|
1585
|
-
else:
|
|
1586
|
-
try:
|
|
1587
|
-
retry_initial_delay_seconds = build_model_retry_initial_delay_seconds(
|
|
1588
|
-
raw_retry_initial_delay_seconds
|
|
1589
|
-
)
|
|
1590
|
-
except ValueError:
|
|
1591
|
-
retry_initial_delay_seconds = DEFAULT_LLM_RETRY_INITIAL_DELAY_SECONDS
|
|
1592
|
-
migrated = True
|
|
1593
|
-
raw_retry_max_delay_seconds = model_data.get("retry_max_delay_seconds")
|
|
1594
|
-
if raw_retry_max_delay_seconds is None:
|
|
1595
|
-
retry_max_delay_seconds = DEFAULT_LLM_RETRY_MAX_DELAY_SECONDS
|
|
1596
|
-
migrated = True
|
|
1597
|
-
else:
|
|
1598
|
-
try:
|
|
1599
|
-
retry_max_delay_seconds = build_model_retry_max_delay_seconds(
|
|
1600
|
-
raw_retry_max_delay_seconds
|
|
1601
|
-
)
|
|
1602
|
-
except ValueError:
|
|
1603
|
-
retry_max_delay_seconds = DEFAULT_LLM_RETRY_MAX_DELAY_SECONDS
|
|
1604
|
-
migrated = True
|
|
1605
|
-
raw_retry_backoff_cap_retries = model_data.get("retry_backoff_cap_retries")
|
|
1606
|
-
if raw_retry_backoff_cap_retries is None:
|
|
1607
|
-
retry_backoff_cap_retries = DEFAULT_LLM_RETRY_BACKOFF_CAP_RETRIES
|
|
1608
|
-
migrated = True
|
|
1609
|
-
else:
|
|
1610
|
-
try:
|
|
1611
|
-
retry_backoff_cap_retries = build_model_retry_backoff_cap_retries(
|
|
1612
|
-
raw_retry_backoff_cap_retries
|
|
1613
|
-
)
|
|
1614
|
-
except ValueError:
|
|
1615
|
-
retry_backoff_cap_retries = DEFAULT_LLM_RETRY_BACKOFF_CAP_RETRIES
|
|
1616
|
-
migrated = True
|
|
1617
|
-
input_image, migrated_input_image = _normalize_nullable_bool(
|
|
1618
|
-
model_data.get("input_image")
|
|
1619
|
-
)
|
|
1620
|
-
output_image, migrated_output_image = _normalize_nullable_bool(
|
|
1621
|
-
model_data.get("output_image")
|
|
1622
|
-
)
|
|
1623
|
-
structured_output, migrated_structured_output = _normalize_nullable_bool(
|
|
1624
|
-
model_data.get("structured_output")
|
|
1625
|
-
)
|
|
1626
|
-
context_window_tokens, migrated_context_window_tokens = _normalize_positive_int(
|
|
1627
|
-
model_data.get("context_window_tokens")
|
|
1628
|
-
)
|
|
1629
|
-
auto_compact_token_limit, migrated_auto_compact_token_limit = (
|
|
1630
|
-
_normalize_positive_int(model_data.get("auto_compact_token_limit"))
|
|
1631
|
-
)
|
|
1632
|
-
if "auto_compact" in model_data or "auto_compact_threshold" in model_data:
|
|
1633
|
-
migrated = True
|
|
1634
|
-
try:
|
|
1635
|
-
validate_model_retry_backoff_settings(
|
|
1636
|
-
retry_initial_delay_seconds=retry_initial_delay_seconds,
|
|
1637
|
-
retry_max_delay_seconds=retry_max_delay_seconds,
|
|
1638
|
-
)
|
|
1639
|
-
except ValueError:
|
|
1640
|
-
retry_initial_delay_seconds = DEFAULT_LLM_RETRY_INITIAL_DELAY_SECONDS
|
|
1641
|
-
retry_max_delay_seconds = DEFAULT_LLM_RETRY_MAX_DELAY_SECONDS
|
|
1642
|
-
migrated = True
|
|
1643
|
-
raw_model_timeout_ms = model_data.get("timeout_ms")
|
|
1644
|
-
if raw_model_timeout_ms is None:
|
|
1645
|
-
model_timeout_ms = DEFAULT_LLM_TIMEOUT_MS
|
|
1646
|
-
migrated = True
|
|
1647
|
-
else:
|
|
1648
|
-
try:
|
|
1649
|
-
model_timeout_ms = build_model_timeout_ms(raw_model_timeout_ms)
|
|
1650
|
-
except ValueError:
|
|
1651
|
-
model_timeout_ms = DEFAULT_LLM_TIMEOUT_MS
|
|
1652
|
-
migrated = True
|
|
1653
|
-
model_settings = ModelSettings(
|
|
1654
|
-
active_provider_id=str(model_data.get("active_provider_id", "")),
|
|
1655
|
-
active_model=str(model_data.get("active_model", "")),
|
|
1656
|
-
input_image=input_image,
|
|
1657
|
-
output_image=output_image,
|
|
1658
|
-
structured_output=structured_output,
|
|
1659
|
-
context_window_tokens=context_window_tokens,
|
|
1660
|
-
params=model_params,
|
|
1661
|
-
timeout_ms=model_timeout_ms,
|
|
1662
|
-
retry_policy=model_retry_policy,
|
|
1663
|
-
max_retries=model_max_retries,
|
|
1664
|
-
retry_initial_delay_seconds=retry_initial_delay_seconds,
|
|
1665
|
-
retry_max_delay_seconds=retry_max_delay_seconds,
|
|
1666
|
-
retry_backoff_cap_retries=retry_backoff_cap_retries,
|
|
1667
|
-
auto_compact_token_limit=auto_compact_token_limit,
|
|
1668
|
-
)
|
|
1669
|
-
migrated = (
|
|
1670
|
-
migrated
|
|
1671
|
-
or migrated_input_image
|
|
1672
|
-
or migrated_output_image
|
|
1673
|
-
or migrated_structured_output
|
|
1674
|
-
or migrated_context_window_tokens
|
|
1675
|
-
or migrated_auto_compact_token_limit
|
|
1676
|
-
)
|
|
1677
|
-
custom_prompt = str(data.get("custom_prompt", ""))
|
|
1678
|
-
if "custom_post_prompt" in data:
|
|
1679
|
-
custom_post_prompt = str(data.get("custom_post_prompt", ""))
|
|
1680
|
-
else:
|
|
1681
|
-
custom_post_prompt = str(data.get("post_prompt", ""))
|
|
1682
|
-
if "post_prompt" in data:
|
|
1683
|
-
migrated = True
|
|
1684
|
-
|
|
1685
|
-
providers_raw = data.get("providers", [])
|
|
1686
|
-
if not isinstance(providers_raw, list):
|
|
1687
|
-
providers_raw = []
|
|
1688
|
-
providers = []
|
|
1689
|
-
for provider in providers_raw:
|
|
1690
|
-
if not isinstance(provider, dict):
|
|
1691
|
-
continue
|
|
1692
|
-
headers, headers_migrated = _normalize_provider_headers(provider.get("headers"))
|
|
1693
|
-
models, models_migrated = _normalize_provider_model_catalog_entries(
|
|
1694
|
-
provider.get("models")
|
|
1695
|
-
)
|
|
1696
|
-
migrated = migrated or headers_migrated
|
|
1697
|
-
migrated = migrated or models_migrated
|
|
1698
|
-
raw_retry_429_delay_seconds = provider.get("retry_429_delay_seconds")
|
|
1699
|
-
if raw_retry_429_delay_seconds is None:
|
|
1700
|
-
retry_429_delay_seconds = 0
|
|
1701
|
-
migrated = True
|
|
1702
|
-
else:
|
|
1703
|
-
try:
|
|
1704
|
-
retry_429_delay_seconds = build_provider_retry_429_delay_seconds(
|
|
1705
|
-
raw_retry_429_delay_seconds
|
|
1706
|
-
)
|
|
1707
|
-
except ValueError:
|
|
1708
|
-
retry_429_delay_seconds = 0
|
|
1709
|
-
migrated = True
|
|
1710
|
-
providers.append(
|
|
1711
|
-
ProviderConfig(
|
|
1712
|
-
id=str(provider.get("id", "")),
|
|
1713
|
-
name=str(provider.get("name", "")),
|
|
1714
|
-
type=str(provider.get("type", "openai_compatible")),
|
|
1715
|
-
base_url=str(provider.get("base_url", "")),
|
|
1716
|
-
api_key=str(provider.get("api_key", "")),
|
|
1717
|
-
headers=headers,
|
|
1718
|
-
retry_429_delay_seconds=retry_429_delay_seconds,
|
|
1719
|
-
models=models,
|
|
1720
|
-
)
|
|
1721
|
-
)
|
|
1722
|
-
|
|
1723
|
-
roles_raw = data.get("roles", [])
|
|
1724
|
-
if not isinstance(roles_raw, list):
|
|
1725
|
-
roles_raw = []
|
|
1726
|
-
roles = []
|
|
1727
|
-
for role in roles_raw:
|
|
1728
|
-
if not isinstance(role, dict):
|
|
1729
|
-
continue
|
|
1730
|
-
role_name = str(role.get("name", ""))
|
|
1731
|
-
if "id" in role:
|
|
1732
|
-
migrated = True
|
|
1733
|
-
if not role_name:
|
|
1734
|
-
role_name = str(role.get("id", ""))
|
|
1735
|
-
if "included_tools" in role:
|
|
1736
|
-
included_tools_raw = role.get("included_tools", [])
|
|
1737
|
-
else:
|
|
1738
|
-
included_tools_raw = role.get("required_tools", [])
|
|
1739
|
-
if "required_tools" in role:
|
|
1740
|
-
migrated = True
|
|
1741
|
-
if not isinstance(included_tools_raw, list):
|
|
1742
|
-
included_tools_raw = []
|
|
1743
|
-
excluded_tools_raw = role.get("excluded_tools", [])
|
|
1744
|
-
if not isinstance(excluded_tools_raw, list):
|
|
1745
|
-
excluded_tools_raw = []
|
|
1746
|
-
|
|
1747
|
-
role_model: RoleModelConfig | None = None
|
|
1748
|
-
role_model_params: ModelParams | None = None
|
|
1749
|
-
if "model" in role:
|
|
1750
|
-
role_model, role_model_migrated = _normalize_role_model(
|
|
1751
|
-
role.get("model"),
|
|
1752
|
-
default_provider_id=model_settings.active_provider_id.strip(),
|
|
1753
|
-
)
|
|
1754
|
-
migrated = migrated or role_model_migrated
|
|
1755
|
-
elif "model_override" in role:
|
|
1756
|
-
role_model, role_model_migrated = _normalize_role_model(
|
|
1757
|
-
role.get("model_override"),
|
|
1758
|
-
default_provider_id=model_settings.active_provider_id.strip(),
|
|
1759
|
-
)
|
|
1760
|
-
migrated = migrated or role_model_migrated or True
|
|
1761
|
-
if "model_params" in role:
|
|
1762
|
-
role_model_params, role_model_params_migrated = (
|
|
1763
|
-
_normalize_optional_model_params(role.get("model_params"))
|
|
1764
|
-
)
|
|
1765
|
-
migrated = migrated or role_model_params_migrated
|
|
1766
|
-
role_system_prompt = str(role.get("system_prompt", ""))
|
|
1767
|
-
role_description, role_description_migrated = _normalize_role_description(
|
|
1768
|
-
role.get("description"),
|
|
1769
|
-
role_name=role_name,
|
|
1770
|
-
system_prompt=role_system_prompt,
|
|
1771
|
-
)
|
|
1772
|
-
migrated = migrated or role_description_migrated
|
|
1773
|
-
|
|
1774
|
-
included_tools = normalize_tool_names(
|
|
1775
|
-
[name for name in included_tools_raw if isinstance(name, str)]
|
|
1776
|
-
)
|
|
1777
|
-
from flowent.tools import is_assistant_only_tool_name
|
|
1778
|
-
|
|
1779
|
-
filtered_included_tools = [
|
|
1780
|
-
tool_name
|
|
1781
|
-
for tool_name in included_tools
|
|
1782
|
-
if not tool_name.startswith("mcp__")
|
|
1783
|
-
and not is_assistant_only_tool_name(tool_name)
|
|
1784
|
-
]
|
|
1785
|
-
migrated = migrated or filtered_included_tools != included_tools
|
|
1786
|
-
excluded_tools = normalize_tool_names(
|
|
1787
|
-
[name for name in excluded_tools_raw if isinstance(name, str)]
|
|
1788
|
-
)
|
|
1789
|
-
filtered_excluded_tools = [
|
|
1790
|
-
tool_name
|
|
1791
|
-
for tool_name in excluded_tools
|
|
1792
|
-
if not tool_name.startswith("mcp__")
|
|
1793
|
-
]
|
|
1794
|
-
migrated = migrated or filtered_excluded_tools != excluded_tools
|
|
1795
|
-
|
|
1796
|
-
roles.append(
|
|
1797
|
-
RoleConfig(
|
|
1798
|
-
name=role_name,
|
|
1799
|
-
system_prompt=role_system_prompt,
|
|
1800
|
-
description=role_description,
|
|
1801
|
-
model=role_model,
|
|
1802
|
-
model_params=role_model_params,
|
|
1803
|
-
included_tools=filtered_included_tools,
|
|
1804
|
-
excluded_tools=filtered_excluded_tools,
|
|
1805
|
-
)
|
|
1806
|
-
)
|
|
1807
|
-
|
|
1808
|
-
if "mcp_servers" in data:
|
|
1809
|
-
migrated = True
|
|
1810
|
-
|
|
1811
|
-
return (
|
|
1812
|
-
Settings(
|
|
1813
|
-
app_data_dir=app_data_dir,
|
|
1814
|
-
working_dir=working_dir,
|
|
1815
|
-
event_log=event_log,
|
|
1816
|
-
access=access,
|
|
1817
|
-
assistant=assistant,
|
|
1818
|
-
leader=leader,
|
|
1819
|
-
telegram=telegram,
|
|
1820
|
-
model=model_settings,
|
|
1821
|
-
custom_prompt=custom_prompt,
|
|
1822
|
-
custom_post_prompt=custom_post_prompt,
|
|
1823
|
-
providers=providers,
|
|
1824
|
-
roles=roles,
|
|
1825
|
-
),
|
|
1826
|
-
migrated,
|
|
1827
|
-
)
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
def _read_settings_file() -> tuple[Settings, bool]:
|
|
1831
|
-
with _SETTINGS_FILE.open(encoding="utf-8") as settings_file:
|
|
1832
|
-
data = json.load(settings_file)
|
|
1833
|
-
if not isinstance(data, dict):
|
|
1834
|
-
raise ValueError("settings file must contain a JSON object")
|
|
1835
|
-
return _build_settings(data)
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
def _get_settings_file_signature() -> tuple[int, int] | None:
|
|
1839
|
-
try:
|
|
1840
|
-
stat_result = _SETTINGS_FILE.stat()
|
|
1841
|
-
except FileNotFoundError:
|
|
1842
|
-
return None
|
|
1843
|
-
return (stat_result.st_mtime_ns, stat_result.st_size)
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
def _preserve_newer_live_access(settings: Settings) -> None:
|
|
1847
|
-
if _get_settings_file_signature() is None:
|
|
1848
|
-
return
|
|
1849
|
-
try:
|
|
1850
|
-
live_settings, _ = _read_settings_file()
|
|
1851
|
-
except Exception as exc:
|
|
1852
|
-
logger.warning(
|
|
1853
|
-
"Failed to read live settings from {} while preserving access: {}",
|
|
1854
|
-
_SETTINGS_FILE,
|
|
1855
|
-
exc,
|
|
1856
|
-
)
|
|
1857
|
-
return
|
|
1858
|
-
if live_settings.access.session_generation > settings.access.session_generation:
|
|
1859
|
-
settings.access = live_settings.access
|
|
1860
|
-
return
|
|
1861
|
-
if (
|
|
1862
|
-
live_settings.access.session_signing_secret.strip()
|
|
1863
|
-
and not settings.access.session_signing_secret.strip()
|
|
1864
|
-
):
|
|
1865
|
-
settings.access.session_signing_secret = (
|
|
1866
|
-
live_settings.access.session_signing_secret
|
|
1867
|
-
)
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
def load_settings() -> Settings:
|
|
1871
|
-
global _cached_settings, _cached_settings_file_signature
|
|
1872
|
-
current_signature = _get_settings_file_signature()
|
|
1873
|
-
with _settings_lock:
|
|
1874
|
-
if (
|
|
1875
|
-
_cached_settings is not None
|
|
1876
|
-
and _cached_settings_file_signature == current_signature
|
|
1877
|
-
):
|
|
1878
|
-
return _cached_settings
|
|
1879
|
-
|
|
1880
|
-
if current_signature is None:
|
|
1881
|
-
loaded_settings = Settings()
|
|
1882
|
-
loaded_signature = None
|
|
1883
|
-
with _settings_lock:
|
|
1884
|
-
_cached_settings = loaded_settings
|
|
1885
|
-
_cached_settings_file_signature = loaded_signature
|
|
1886
|
-
return _cached_settings
|
|
1887
|
-
|
|
1888
|
-
try:
|
|
1889
|
-
loaded_settings, migrated = _read_settings_file()
|
|
1890
|
-
loaded_signature = _get_settings_file_signature()
|
|
1891
|
-
except Exception as exc:
|
|
1892
|
-
logger.warning(
|
|
1893
|
-
"Failed to load settings from {}: {}. Falling back to defaults.",
|
|
1894
|
-
_SETTINGS_FILE,
|
|
1895
|
-
exc,
|
|
1896
|
-
)
|
|
1897
|
-
loaded_settings = Settings()
|
|
1898
|
-
loaded_signature = current_signature
|
|
1899
|
-
migrated = False
|
|
1900
|
-
|
|
1901
|
-
if migrated:
|
|
1902
|
-
try:
|
|
1903
|
-
save_settings(loaded_settings)
|
|
1904
|
-
loaded_signature = _get_settings_file_signature()
|
|
1905
|
-
except Exception as exc:
|
|
1906
|
-
logger.warning(
|
|
1907
|
-
"Failed to persist migrated settings to {}: {}",
|
|
1908
|
-
_SETTINGS_FILE,
|
|
1909
|
-
exc,
|
|
1910
|
-
)
|
|
1911
|
-
|
|
1912
|
-
with _settings_lock:
|
|
1913
|
-
_cached_settings = loaded_settings
|
|
1914
|
-
_cached_settings_file_signature = loaded_signature
|
|
1915
|
-
return _cached_settings
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
def save_settings(settings: Settings) -> None:
|
|
1919
|
-
global _cached_settings, _cached_settings_file_signature
|
|
1920
|
-
temp_path: Path | None = None
|
|
1921
|
-
_SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
1922
|
-
_preserve_newer_live_access(settings)
|
|
1923
|
-
|
|
1924
|
-
try:
|
|
1925
|
-
with tempfile.NamedTemporaryFile(
|
|
1926
|
-
mode="w",
|
|
1927
|
-
encoding="utf-8",
|
|
1928
|
-
dir=_SETTINGS_FILE.parent,
|
|
1929
|
-
prefix=f"{_SETTINGS_FILE.name}.",
|
|
1930
|
-
suffix=".tmp",
|
|
1931
|
-
delete=False,
|
|
1932
|
-
) as temp_file:
|
|
1933
|
-
temp_path = Path(temp_file.name)
|
|
1934
|
-
json.dump(asdict(settings), temp_file, indent=2)
|
|
1935
|
-
temp_file.flush()
|
|
1936
|
-
os.fsync(temp_file.fileno())
|
|
1937
|
-
|
|
1938
|
-
os.replace(temp_path, _SETTINGS_FILE)
|
|
1939
|
-
except Exception:
|
|
1940
|
-
if temp_path is not None:
|
|
1941
|
-
temp_path.unlink(missing_ok=True)
|
|
1942
|
-
raise
|
|
1943
|
-
|
|
1944
|
-
persisted_signature = _get_settings_file_signature()
|
|
1945
|
-
with _settings_lock:
|
|
1946
|
-
_cached_settings = settings
|
|
1947
|
-
_cached_settings_file_signature = persisted_signature
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
def get_settings() -> Settings:
|
|
1951
|
-
return load_settings()
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
def find_provider(settings: Settings, provider_id: str) -> ProviderConfig | None:
|
|
1955
|
-
for p in settings.providers:
|
|
1956
|
-
if p.id == provider_id:
|
|
1957
|
-
return p
|
|
1958
|
-
return None
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
def find_role(settings: Settings, role_name: str) -> RoleConfig | None:
|
|
1962
|
-
for r in settings.roles:
|
|
1963
|
-
if r.name == role_name:
|
|
1964
|
-
return r
|
|
1965
|
-
return None
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
def clear_provider_references(settings: Settings, provider_id: str) -> bool:
|
|
1969
|
-
changed = False
|
|
1970
|
-
|
|
1971
|
-
if settings.model.active_provider_id == provider_id:
|
|
1972
|
-
settings.model.active_provider_id = ""
|
|
1973
|
-
settings.model.active_model = ""
|
|
1974
|
-
changed = True
|
|
1975
|
-
|
|
1976
|
-
for role in settings.roles:
|
|
1977
|
-
if role.model is None or role.model.provider_id != provider_id:
|
|
1978
|
-
continue
|
|
1979
|
-
role.model = None
|
|
1980
|
-
changed = True
|
|
1981
|
-
return changed
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
def build_steward_role() -> RoleConfig:
|
|
1985
|
-
return RoleConfig(
|
|
1986
|
-
name=STEWARD_ROLE_NAME,
|
|
1987
|
-
system_prompt=STEWARD_ROLE_SYSTEM_PROMPT,
|
|
1988
|
-
description=STEWARD_ROLE_DESCRIPTION,
|
|
1989
|
-
included_tools=list(STEWARD_ROLE_INCLUDED_TOOLS),
|
|
1990
|
-
excluded_tools=[],
|
|
1991
|
-
)
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
def build_worker_role() -> RoleConfig:
|
|
1995
|
-
return RoleConfig(
|
|
1996
|
-
name=WORKER_ROLE_NAME,
|
|
1997
|
-
system_prompt=WORKER_ROLE_SYSTEM_PROMPT,
|
|
1998
|
-
description=WORKER_ROLE_DESCRIPTION,
|
|
1999
|
-
included_tools=list(WORKER_ROLE_INCLUDED_TOOLS),
|
|
2000
|
-
excluded_tools=[],
|
|
2001
|
-
)
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
def build_conductor_role() -> RoleConfig:
|
|
2005
|
-
return RoleConfig(
|
|
2006
|
-
name=CONDUCTOR_ROLE_NAME,
|
|
2007
|
-
system_prompt=CONDUCTOR_ROLE_SYSTEM_PROMPT,
|
|
2008
|
-
description=CONDUCTOR_ROLE_DESCRIPTION,
|
|
2009
|
-
included_tools=list(CONDUCTOR_ROLE_INCLUDED_TOOLS),
|
|
2010
|
-
excluded_tools=[],
|
|
2011
|
-
)
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
def build_designer_role() -> RoleConfig:
|
|
2015
|
-
return RoleConfig(
|
|
2016
|
-
name=DESIGNER_ROLE_NAME,
|
|
2017
|
-
system_prompt=DESIGNER_ROLE_SYSTEM_PROMPT,
|
|
2018
|
-
description=DESIGNER_ROLE_DESCRIPTION,
|
|
2019
|
-
included_tools=list(DESIGNER_ROLE_INCLUDED_TOOLS),
|
|
2020
|
-
excluded_tools=[],
|
|
2021
|
-
)
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
def rename_role_references(
|
|
2025
|
-
settings: Settings,
|
|
2026
|
-
old_role_name: str,
|
|
2027
|
-
new_role_name: str,
|
|
2028
|
-
) -> bool:
|
|
2029
|
-
changed = False
|
|
2030
|
-
if settings.assistant.role_name == old_role_name:
|
|
2031
|
-
settings.assistant.role_name = new_role_name
|
|
2032
|
-
changed = True
|
|
2033
|
-
if settings.leader.role_name == old_role_name:
|
|
2034
|
-
settings.leader.role_name = new_role_name
|
|
2035
|
-
changed = True
|
|
2036
|
-
return changed
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
def clear_role_references(settings: Settings, role_name: str) -> bool:
|
|
2040
|
-
changed = False
|
|
2041
|
-
if settings.assistant.role_name == role_name:
|
|
2042
|
-
settings.assistant.role_name = STEWARD_ROLE_NAME
|
|
2043
|
-
changed = True
|
|
2044
|
-
if settings.leader.role_name == role_name:
|
|
2045
|
-
settings.leader.role_name = CONDUCTOR_ROLE_NAME
|
|
2046
|
-
changed = True
|
|
2047
|
-
return changed
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
def ensure_assistant_role(settings: Settings) -> bool:
|
|
2051
|
-
if find_role(settings, settings.assistant.role_name) is not None:
|
|
2052
|
-
return False
|
|
2053
|
-
settings.assistant.role_name = STEWARD_ROLE_NAME
|
|
2054
|
-
return True
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
def ensure_leader_role(settings: Settings) -> bool:
|
|
2058
|
-
if find_role(settings, settings.leader.role_name) is not None:
|
|
2059
|
-
return False
|
|
2060
|
-
settings.leader.role_name = CONDUCTOR_ROLE_NAME
|
|
2061
|
-
return True
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
def _ensure_builtin_role(settings: Settings, standard_role: RoleConfig) -> bool:
|
|
2065
|
-
current_role = find_role(settings, standard_role.name)
|
|
2066
|
-
if current_role is None:
|
|
2067
|
-
settings.roles.append(standard_role)
|
|
2068
|
-
return True
|
|
2069
|
-
if (
|
|
2070
|
-
current_role.description != standard_role.description
|
|
2071
|
-
or current_role.system_prompt != standard_role.system_prompt
|
|
2072
|
-
or current_role.included_tools != standard_role.included_tools
|
|
2073
|
-
or current_role.excluded_tools != standard_role.excluded_tools
|
|
2074
|
-
):
|
|
2075
|
-
current_role.description = standard_role.description
|
|
2076
|
-
current_role.system_prompt = standard_role.system_prompt
|
|
2077
|
-
current_role.included_tools = list(standard_role.included_tools)
|
|
2078
|
-
current_role.excluded_tools = list(standard_role.excluded_tools)
|
|
2079
|
-
return True
|
|
2080
|
-
return False
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
def ensure_builtin_roles(settings: Settings) -> bool:
|
|
2084
|
-
changed = False
|
|
2085
|
-
builtin_role_order = [
|
|
2086
|
-
build_steward_role(),
|
|
2087
|
-
build_worker_role(),
|
|
2088
|
-
build_conductor_role(),
|
|
2089
|
-
build_designer_role(),
|
|
2090
|
-
]
|
|
2091
|
-
for standard_role in builtin_role_order:
|
|
2092
|
-
changed = _ensure_builtin_role(settings, standard_role) or changed
|
|
2093
|
-
changed = ensure_assistant_role(settings) or changed
|
|
2094
|
-
changed = ensure_leader_role(settings) or changed
|
|
2095
|
-
|
|
2096
|
-
ordered_roles: list[RoleConfig] = []
|
|
2097
|
-
builtin_role_names = {role.name for role in builtin_role_order}
|
|
2098
|
-
for standard_role in builtin_role_order:
|
|
2099
|
-
current_role = find_role(settings, standard_role.name)
|
|
2100
|
-
if current_role is not None:
|
|
2101
|
-
ordered_roles.append(current_role)
|
|
2102
|
-
for role in settings.roles:
|
|
2103
|
-
if role.name not in builtin_role_names:
|
|
2104
|
-
ordered_roles.append(role)
|
|
2105
|
-
if ordered_roles != settings.roles:
|
|
2106
|
-
settings.roles = ordered_roles
|
|
2107
|
-
changed = True
|
|
2108
|
-
return changed
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
def is_builtin_role_name(role_name: str) -> bool:
|
|
2112
|
-
return role_name in BUILTIN_ROLE_NAMES
|