flowent 0.0.0 → 0.0.4
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 +70 -10
- package/assets/flowent-banner.png +0 -0
- package/backend/.python-version +1 -0
- package/backend/pyproject.toml +57 -0
- package/backend/src/flowent/__init__.py +3 -0
- package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
- 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__/agent.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/assistant_commands.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/cli.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__/logging.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/mcp_service.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__/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__/sandbox.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__/stats_service.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/workspace_store.cpython-313.pyc +0 -0
- package/backend/src/flowent/_version.py +7 -0
- package/backend/src/flowent/access.py +247 -0
- package/backend/src/flowent/agent.py +2808 -0
- package/backend/src/flowent/assistant_commands.py +106 -0
- package/backend/src/flowent/channels/__init__.py +3 -0
- 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 +615 -0
- package/backend/src/flowent/cli.py +85 -0
- package/backend/src/flowent/config.py +14 -0
- package/backend/src/flowent/dev.py +3 -0
- package/backend/src/flowent/events.py +157 -0
- package/backend/src/flowent/graph_runtime.py +60 -0
- package/backend/src/flowent/graph_service.py +1346 -0
- package/backend/src/flowent/image_assets.py +356 -0
- package/backend/src/flowent/logging.py +155 -0
- package/backend/src/flowent/main.py +124 -0
- package/backend/src/flowent/mcp_service.py +1904 -0
- package/backend/src/flowent/model_metadata.py +98 -0
- package/backend/src/flowent/models/__init__.py +121 -0
- 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 +33 -0
- package/backend/src/flowent/models/base.py +24 -0
- package/backend/src/flowent/models/blueprint.py +176 -0
- package/backend/src/flowent/models/content.py +164 -0
- package/backend/src/flowent/models/delta.py +44 -0
- package/backend/src/flowent/models/event.py +51 -0
- package/backend/src/flowent/models/graph.py +437 -0
- package/backend/src/flowent/models/history.py +214 -0
- package/backend/src/flowent/models/llm.py +61 -0
- package/backend/src/flowent/models/message.py +27 -0
- package/backend/src/flowent/models/tab.py +48 -0
- package/backend/src/flowent/models/todo.py +10 -0
- package/backend/src/flowent/network.py +146 -0
- package/backend/src/flowent/prompts/__init__.py +67 -0
- 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 +250 -0
- package/backend/src/flowent/prompts/steward.py +64 -0
- package/backend/src/flowent/providers/__init__.py +23 -0
- 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 +468 -0
- package/backend/src/flowent/providers/base_url.py +60 -0
- package/backend/src/flowent/providers/configuration.py +182 -0
- package/backend/src/flowent/providers/content.py +122 -0
- package/backend/src/flowent/providers/errors.py +223 -0
- package/backend/src/flowent/providers/gateway.py +169 -0
- package/backend/src/flowent/providers/gemini.py +447 -0
- package/backend/src/flowent/providers/headers.py +20 -0
- package/backend/src/flowent/providers/management.py +96 -0
- package/backend/src/flowent/providers/ollama.py +293 -0
- package/backend/src/flowent/providers/openai.py +422 -0
- package/backend/src/flowent/providers/openai_responses.py +655 -0
- package/backend/src/flowent/providers/registry.py +144 -0
- package/backend/src/flowent/providers/sse.py +31 -0
- package/backend/src/flowent/providers/thinking.py +79 -0
- package/backend/src/flowent/registry.py +73 -0
- package/backend/src/flowent/role_management.py +255 -0
- package/backend/src/flowent/routes/__init__.py +30 -0
- 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__/mcp.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__/stats.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 +48 -0
- package/backend/src/flowent/routes/assistant.py +155 -0
- package/backend/src/flowent/routes/image_assets.py +33 -0
- package/backend/src/flowent/routes/mcp.py +125 -0
- package/backend/src/flowent/routes/meta.py +28 -0
- package/backend/src/flowent/routes/nodes.py +365 -0
- package/backend/src/flowent/routes/prompts.py +46 -0
- package/backend/src/flowent/routes/providers_route.py +364 -0
- package/backend/src/flowent/routes/roles.py +207 -0
- package/backend/src/flowent/routes/settings.py +324 -0
- package/backend/src/flowent/routes/stats.py +229 -0
- package/backend/src/flowent/routes/tabs.py +292 -0
- package/backend/src/flowent/routes/ws.py +33 -0
- package/backend/src/flowent/runtime.py +188 -0
- package/backend/src/flowent/sandbox.py +45 -0
- package/backend/src/flowent/security.py +42 -0
- package/backend/src/flowent/settings.py +2467 -0
- package/backend/src/flowent/settings_management.py +286 -0
- package/backend/src/flowent/state_db.py +120 -0
- package/backend/src/flowent/static/assets/AssistantPage-B3Xc08AS.js +1 -0
- package/backend/src/flowent/static/assets/ChannelsPage-ByLd28xk.js +1 -0
- package/backend/src/flowent/static/assets/HomePage-C0hAx9_l.js +3 -0
- package/backend/src/flowent/static/assets/McpPage-DkrYLvBv.js +7 -0
- package/backend/src/flowent/static/assets/PageScaffold-D4jO9ooX.js +1 -0
- package/backend/src/flowent/static/assets/PromptsPage-DWA7rRJd.js +1 -0
- package/backend/src/flowent/static/assets/ProvidersPage-PUWT8seJ.js +3 -0
- package/backend/src/flowent/static/assets/RolesPage-CqcclGRw.js +1 -0
- package/backend/src/flowent/static/assets/SettingsPage-8tS2cJgX.js +3 -0
- package/backend/src/flowent/static/assets/StatsPage-BX9khYzu.js +1 -0
- package/backend/src/flowent/static/assets/ToolsPage-9Tl9FdeD.js +1 -0
- package/backend/src/flowent/static/assets/WorkspaceCommandDialog-CCXxjDL8.js +1 -0
- package/backend/src/flowent/static/assets/WorkspacePanels-aMdJ7ZH7.js +1 -0
- package/backend/src/flowent/static/assets/alert-dialog-kFYVQ7oX.js +1 -0
- package/backend/src/flowent/static/assets/badge-74-3jsCg.js +1 -0
- package/backend/src/flowent/static/assets/constants-XUzFf6i1.js +1 -0
- package/backend/src/flowent/static/assets/datetime-m6_O_Ci9.js +1 -0
- package/backend/src/flowent/static/assets/dialog-BeGSweF6.js +1 -0
- package/backend/src/flowent/static/assets/elk-worker.min-C9JGDOE-.js +6312 -0
- package/backend/src/flowent/static/assets/graph-vendor-CHpVij2M.css +1 -0
- package/backend/src/flowent/static/assets/graph-vendor-DRq_-6fV.js +7 -0
- package/backend/src/flowent/static/assets/index-BHC1Vhy8.css +1 -0
- package/backend/src/flowent/static/assets/index-CL1ALZ3r.js +10 -0
- package/backend/src/flowent/static/assets/layout.worker-jMHqAFbP.js +24 -0
- package/backend/src/flowent/static/assets/markdown-vendor-DVdy_w12.js +29 -0
- package/backend/src/flowent/static/assets/modelParams-CaHd0903.js +1 -0
- package/backend/src/flowent/static/assets/react-vendor-mEs_JJxa.js +9 -0
- package/backend/src/flowent/static/assets/roles-2OLDeTc5.js +1 -0
- package/backend/src/flowent/static/assets/rolldown-runtime-BYbx6iT9.js +1 -0
- package/backend/src/flowent/static/assets/select-DL_LPeDj.js +1 -0
- package/backend/src/flowent/static/assets/shared-CMxbpLeQ.js +1 -0
- package/backend/src/flowent/static/assets/triState-DEr3NkXV.js +1 -0
- package/backend/src/flowent/static/assets/ui-vendor-Dg9NNnWX.js +51 -0
- package/backend/src/flowent/static/index.html +36 -0
- package/backend/src/flowent/stats_service.py +218 -0
- package/backend/src/flowent/tools/__init__.py +201 -0
- 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__/mcp.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 +156 -0
- package/backend/src/flowent/tools/contacts.py +22 -0
- package/backend/src/flowent/tools/create_agent.py +270 -0
- package/backend/src/flowent/tools/create_tab.py +59 -0
- package/backend/src/flowent/tools/delete_tab.py +39 -0
- package/backend/src/flowent/tools/edit.py +142 -0
- package/backend/src/flowent/tools/exec.py +117 -0
- package/backend/src/flowent/tools/fetch.py +85 -0
- package/backend/src/flowent/tools/idle.py +27 -0
- package/backend/src/flowent/tools/list_roles.py +50 -0
- package/backend/src/flowent/tools/list_tabs.py +96 -0
- package/backend/src/flowent/tools/list_tools.py +24 -0
- package/backend/src/flowent/tools/manage_prompts.py +102 -0
- package/backend/src/flowent/tools/manage_providers.py +220 -0
- package/backend/src/flowent/tools/manage_roles.py +275 -0
- package/backend/src/flowent/tools/manage_settings.py +346 -0
- package/backend/src/flowent/tools/mcp.py +199 -0
- package/backend/src/flowent/tools/read.py +152 -0
- package/backend/src/flowent/tools/send.py +50 -0
- package/backend/src/flowent/tools/set_permissions.py +84 -0
- package/backend/src/flowent/tools/sleep.py +41 -0
- package/backend/src/flowent/tools/todo.py +51 -0
- package/backend/src/flowent/workspace_store.py +479 -0
- 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 +6 -0
- 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_mcp_api.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 +29 -0
- package/backend/tests/integration/api/test_access_api.py +182 -0
- package/backend/tests/integration/api/test_assistant_api.py +354 -0
- package/backend/tests/integration/api/test_frontend_mounting.py +61 -0
- package/backend/tests/integration/api/test_mcp_api.py +116 -0
- package/backend/tests/integration/api/test_meta_api.py +33 -0
- package/backend/tests/integration/api/test_nodes_api.py +486 -0
- package/backend/tests/integration/api/test_prompts_api.py +47 -0
- package/backend/tests/integration/api/test_roles_api.py +227 -0
- package/backend/tests/integration/api/test_tabs_api.py +501 -0
- 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 +746 -0
- package/backend/tests/unit/agent/test_agent_runtime.py +2726 -0
- 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 +552 -0
- 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 +132 -0
- 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 +569 -0
- 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 +185 -0
- package/backend/tests/unit/providers/test_errors.py +68 -0
- package/backend/tests/unit/providers/test_extract_delta_parts.py +22 -0
- package/backend/tests/unit/providers/test_openai_provider.py +139 -0
- package/backend/tests/unit/providers/test_openai_responses.py +402 -0
- package/backend/tests/unit/providers/test_provider_gateway.py +359 -0
- package/backend/tests/unit/providers/test_think_tag_parser.py +36 -0
- 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/__pycache__/test_stats_routes.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/routes/test_prompts_routes.py +104 -0
- package/backend/tests/unit/routes/test_providers_route.py +368 -0
- package/backend/tests/unit/routes/test_roles_routes.py +426 -0
- package/backend/tests/unit/routes/test_settings_routes.py +1138 -0
- package/backend/tests/unit/routes/test_stats_routes.py +149 -0
- 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 +1012 -0
- 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 +78 -0
- 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 +110 -0
- 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 +711 -0
- package/backend/tests/unit/test_access.py +45 -0
- package/backend/tests/unit/test_cli.py +124 -0
- package/backend/tests/unit/test_graph_runtime.py +72 -0
- package/backend/tests/unit/test_network.py +51 -0
- package/backend/tests/unit/test_state_sqlite_storage.py +93 -0
- package/backend/tests/unit/test_workspace_store.py +231 -0
- 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 +229 -0
- package/backend/tests/unit/tools/test_create_agent_tool.py +524 -0
- package/backend/tests/unit/tools/test_delete_tab_tool.py +83 -0
- package/backend/tests/unit/tools/test_edit_tool.py +115 -0
- package/backend/tests/unit/tools/test_exec_tool.py +81 -0
- package/backend/tests/unit/tools/test_fetch_tool.py +65 -0
- package/backend/tests/unit/tools/test_manage_prompts_tool.py +117 -0
- package/backend/tests/unit/tools/test_manage_providers_tool.py +458 -0
- package/backend/tests/unit/tools/test_manage_roles_tool.py +411 -0
- package/backend/tests/unit/tools/test_manage_settings_tool.py +608 -0
- package/backend/tests/unit/tools/test_read_tool.py +33 -0
- package/backend/tests/unit/tools/test_set_permissions_tool.py +391 -0
- package/backend/tests/unit/tools/test_todo_tool.py +37 -0
- package/backend/tests/unit/tools/test_tool_registry.py +91 -0
- package/backend/uv.lock +1144 -0
- package/bin/flowent.mjs +62 -35
- package/dist/frontend/assets/AssistantPage-B3Xc08AS.js +1 -0
- package/dist/frontend/assets/ChannelsPage-ByLd28xk.js +1 -0
- package/dist/frontend/assets/HomePage-C0hAx9_l.js +3 -0
- package/dist/frontend/assets/McpPage-DkrYLvBv.js +7 -0
- package/dist/frontend/assets/PageScaffold-D4jO9ooX.js +1 -0
- package/dist/frontend/assets/PromptsPage-DWA7rRJd.js +1 -0
- package/dist/frontend/assets/ProvidersPage-PUWT8seJ.js +3 -0
- package/dist/frontend/assets/RolesPage-CqcclGRw.js +1 -0
- package/dist/frontend/assets/SettingsPage-8tS2cJgX.js +3 -0
- package/dist/frontend/assets/StatsPage-BX9khYzu.js +1 -0
- package/dist/frontend/assets/ToolsPage-9Tl9FdeD.js +1 -0
- package/dist/frontend/assets/WorkspaceCommandDialog-CCXxjDL8.js +1 -0
- package/dist/frontend/assets/WorkspacePanels-aMdJ7ZH7.js +1 -0
- package/dist/frontend/assets/alert-dialog-kFYVQ7oX.js +1 -0
- package/dist/frontend/assets/badge-74-3jsCg.js +1 -0
- package/dist/frontend/assets/constants-XUzFf6i1.js +1 -0
- package/dist/frontend/assets/datetime-m6_O_Ci9.js +1 -0
- package/dist/frontend/assets/dialog-BeGSweF6.js +1 -0
- package/dist/frontend/assets/elk-worker.min-C9JGDOE-.js +6312 -0
- package/dist/frontend/assets/graph-vendor-CHpVij2M.css +1 -0
- package/dist/frontend/assets/graph-vendor-DRq_-6fV.js +7 -0
- package/dist/frontend/assets/index-BHC1Vhy8.css +1 -0
- package/dist/frontend/assets/index-CL1ALZ3r.js +10 -0
- package/dist/frontend/assets/layout.worker-jMHqAFbP.js +24 -0
- package/dist/frontend/assets/markdown-vendor-DVdy_w12.js +29 -0
- package/dist/frontend/assets/modelParams-CaHd0903.js +1 -0
- package/dist/frontend/assets/react-vendor-mEs_JJxa.js +9 -0
- package/dist/frontend/assets/roles-2OLDeTc5.js +1 -0
- package/dist/frontend/assets/rolldown-runtime-BYbx6iT9.js +1 -0
- package/dist/frontend/assets/select-DL_LPeDj.js +1 -0
- package/dist/frontend/assets/shared-CMxbpLeQ.js +1 -0
- package/dist/frontend/assets/triState-DEr3NkXV.js +1 -0
- package/dist/frontend/assets/ui-vendor-Dg9NNnWX.js +51 -0
- package/dist/frontend/index.html +36 -0
- package/package.json +28 -41
- package/dist/.next/BUILD_ID +0 -1
- package/dist/.next/app-path-routes-manifest.json +0 -6
- package/dist/.next/build-manifest.json +0 -20
- package/dist/.next/package.json +0 -1
- package/dist/.next/prerender-manifest.json +0 -114
- package/dist/.next/required-server-files.json +0 -333
- package/dist/.next/routes-manifest.json +0 -69
- package/dist/.next/server/app/_global-error/page/app-paths-manifest.json +0 -3
- package/dist/.next/server/app/_global-error/page/build-manifest.json +0 -16
- package/dist/.next/server/app/_global-error/page/next-font-manifest.json +0 -6
- package/dist/.next/server/app/_global-error/page/react-loadable-manifest.json +0 -1
- package/dist/.next/server/app/_global-error/page/server-reference-manifest.json +0 -4
- package/dist/.next/server/app/_global-error/page.js +0 -9
- package/dist/.next/server/app/_global-error/page.js.map +0 -5
- package/dist/.next/server/app/_global-error/page.js.nft.json +0 -1
- package/dist/.next/server/app/_global-error/page_client-reference-manifest.js +0 -3
- package/dist/.next/server/app/_global-error.html +0 -1
- package/dist/.next/server/app/_global-error.meta +0 -15
- package/dist/.next/server/app/_global-error.rsc +0 -14
- package/dist/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +0 -5
- package/dist/.next/server/app/_global-error.segments/_full.segment.rsc +0 -14
- package/dist/.next/server/app/_global-error.segments/_head.segment.rsc +0 -5
- package/dist/.next/server/app/_global-error.segments/_index.segment.rsc +0 -5
- package/dist/.next/server/app/_global-error.segments/_tree.segment.rsc +0 -1
- package/dist/.next/server/app/_not-found/page/app-paths-manifest.json +0 -3
- package/dist/.next/server/app/_not-found/page/build-manifest.json +0 -16
- package/dist/.next/server/app/_not-found/page/next-font-manifest.json +0 -10
- package/dist/.next/server/app/_not-found/page/react-loadable-manifest.json +0 -1
- package/dist/.next/server/app/_not-found/page/server-reference-manifest.json +0 -4
- package/dist/.next/server/app/_not-found/page.js +0 -13
- package/dist/.next/server/app/_not-found/page.js.map +0 -5
- package/dist/.next/server/app/_not-found/page.js.nft.json +0 -1
- package/dist/.next/server/app/_not-found/page_client-reference-manifest.js +0 -3
- package/dist/.next/server/app/_not-found.html +0 -1
- package/dist/.next/server/app/_not-found.meta +0 -16
- package/dist/.next/server/app/_not-found.rsc +0 -16
- package/dist/.next/server/app/_not-found.segments/_full.segment.rsc +0 -16
- package/dist/.next/server/app/_not-found.segments/_head.segment.rsc +0 -6
- package/dist/.next/server/app/_not-found.segments/_index.segment.rsc +0 -5
- package/dist/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +0 -5
- package/dist/.next/server/app/_not-found.segments/_not-found.segment.rsc +0 -5
- package/dist/.next/server/app/_not-found.segments/_tree.segment.rsc +0 -2
- package/dist/.next/server/app/icon.svg/route/app-paths-manifest.json +0 -3
- package/dist/.next/server/app/icon.svg/route/build-manifest.json +0 -9
- package/dist/.next/server/app/icon.svg/route.js +0 -6
- package/dist/.next/server/app/icon.svg/route.js.map +0 -5
- package/dist/.next/server/app/icon.svg/route.js.nft.json +0 -1
- package/dist/.next/server/app/icon.svg.meta +0 -1
- package/dist/.next/server/app/index.html +0 -1
- package/dist/.next/server/app/index.meta +0 -14
- package/dist/.next/server/app/index.rsc +0 -15
- package/dist/.next/server/app/index.segments/__PAGE__.segment.rsc +0 -5
- package/dist/.next/server/app/index.segments/_full.segment.rsc +0 -15
- package/dist/.next/server/app/index.segments/_head.segment.rsc +0 -6
- package/dist/.next/server/app/index.segments/_index.segment.rsc +0 -5
- package/dist/.next/server/app/index.segments/_tree.segment.rsc +0 -3
- package/dist/.next/server/app/page/app-paths-manifest.json +0 -3
- package/dist/.next/server/app/page/build-manifest.json +0 -16
- package/dist/.next/server/app/page/next-font-manifest.json +0 -10
- package/dist/.next/server/app/page/react-loadable-manifest.json +0 -1
- package/dist/.next/server/app/page/server-reference-manifest.json +0 -4
- package/dist/.next/server/app/page.js +0 -14
- package/dist/.next/server/app/page.js.map +0 -5
- package/dist/.next/server/app/page.js.nft.json +0 -1
- package/dist/.next/server/app/page_client-reference-manifest.js +0 -3
- package/dist/.next/server/app-paths-manifest.json +0 -6
- package/dist/.next/server/chunks/[externals]_next_dist_0arv.vj._.js +0 -3
- package/dist/.next/server/chunks/[root-of-the-server]__0vcj1q1._.js +0 -13
- package/dist/.next/server/chunks/[turbopack]_runtime.js +0 -903
- package/dist/.next/server/chunks/_next-internal_server_app_icon_svg_route_actions_0-0ehc~.js +0 -3
- package/dist/.next/server/chunks/ssr/05w9_next_dist_0ihu0u9._.js +0 -6
- package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_12u3mib._.js +0 -3
- package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_builtin_forbidden_04fbe_..js +0 -3
- package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_builtin_global-error_0brpl_..js +0 -3
- package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_builtin_unauthorized_0~2g66g.js +0 -3
- package/dist/.next/server/chunks/ssr/05w9_next_dist_esm_build_templates_app-page_0~cyr1_.js +0 -4
- package/dist/.next/server/chunks/ssr/05w9_next_dist_esm_build_templates_app-page_1105emf.js +0 -4
- package/dist/.next/server/chunks/ssr/05w9_next_dist_esm_build_templates_app-page_11uhyqv.js +0 -4
- package/dist/.next/server/chunks/ssr/[root-of-the-server]__0.t9_75._.js +0 -33
- package/dist/.next/server/chunks/ssr/[root-of-the-server]__0c0ud_z._.js +0 -3
- package/dist/.next/server/chunks/ssr/[root-of-the-server]__0f9_8d4._.js +0 -3
- package/dist/.next/server/chunks/ssr/[root-of-the-server]__0l5ko41._.js +0 -19
- package/dist/.next/server/chunks/ssr/[root-of-the-server]__0mn6z7i._.js +0 -3
- package/dist/.next/server/chunks/ssr/[root-of-the-server]__0npxxst._.js +0 -33
- package/dist/.next/server/chunks/ssr/[root-of-the-server]__0qjhaca._.js +0 -3
- package/dist/.next/server/chunks/ssr/[root-of-the-server]__0rwgw3s._.js +0 -3
- package/dist/.next/server/chunks/ssr/[turbopack]_runtime.js +0 -903
- package/dist/.next/server/chunks/ssr/_next-internal_server_app__global-error_page_actions_0k77kol.js +0 -3
- package/dist/.next/server/chunks/ssr/_next-internal_server_app__not-found_page_actions_0eq97pa.js +0 -3
- package/dist/.next/server/chunks/ssr/_next-internal_server_app_page_actions_09-gtaw.js +0 -3
- package/dist/.next/server/chunks/ssr/node_modules__pnpm_056~6.6._.js +0 -3
- package/dist/.next/server/chunks/ssr/node_modules__pnpm_0~j0k.e._.js +0 -33
- package/dist/.next/server/functions-config-manifest.json +0 -4
- package/dist/.next/server/middleware-build-manifest.js +0 -20
- package/dist/.next/server/middleware-manifest.json +0 -6
- package/dist/.next/server/next-font-manifest.js +0 -1
- package/dist/.next/server/next-font-manifest.json +0 -13
- package/dist/.next/server/pages/404.html +0 -1
- package/dist/.next/server/pages/500.html +0 -1
- package/dist/.next/server/pages-manifest.json +0 -4
- package/dist/.next/server/prefetch-hints.json +0 -1
- package/dist/.next/server/server-reference-manifest.js +0 -1
- package/dist/.next/server/server-reference-manifest.json +0 -5
- package/dist/.next/static/7FFlzRe2eS-D0Lw5oEpmC/_buildManifest.js +0 -11
- package/dist/.next/static/7FFlzRe2eS-D0Lw5oEpmC/_clientMiddlewareManifest.js +0 -1
- package/dist/.next/static/7FFlzRe2eS-D0Lw5oEpmC/_ssgManifest.js +0 -1
- package/dist/.next/static/chunks/01qk2~bgf76vu.js +0 -1
- package/dist/.next/static/chunks/03~yq9q893hmn.js +0 -1
- package/dist/.next/static/chunks/080queev.r2uy.js +0 -31
- package/dist/.next/static/chunks/0v3lyuj75aq50.js +0 -1
- package/dist/.next/static/chunks/10b~xdx5c-i7s.js +0 -5
- package/dist/.next/static/chunks/15~9l5n.~r-.4.css +0 -2
- package/dist/.next/static/chunks/turbopack-0m-970~qvs7sc.js +0 -1
- package/dist/.next/static/media/7178b3e590c64307-s.11.cyxs5p-0z~.woff2 +0 -0
- package/dist/.next/static/media/8a480f0b521d4e75-s.06d3mdzz5bre_.woff2 +0 -0
- package/dist/.next/static/media/caa3a2e1cccd8315-s.p.16t1db8_9y2o~.woff2 +0 -0
- package/dist/package.json +0 -87
- package/dist/server.js +0 -38
- /package/{dist/.next/server/app/icon.svg.body → backend/src/flowent/static/favicon.svg} +0 -0
- /package/dist/{.next/static/media/icon.0.r~afrtrocz9.svg → frontend/favicon.svg} +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
STEWARD_ROLE_SYSTEM_PROMPT = """\
|
|
2
|
+
You are the Steward role currently used by the Assistant - the Human's interface to the system.
|
|
3
|
+
|
|
4
|
+
The Human can interact with the system only through the Assistant chat panel. The Human has no terminal, filesystem access, or direct execution surface. If a request requires reading files, running commands, editing code, browsing the network, or any other system interaction, you must open a workflow and create the appropriate agents to do the work rather than pushing the task back to the Human.
|
|
5
|
+
|
|
6
|
+
Your responsibilities:
|
|
7
|
+
- Understand the Human's intent
|
|
8
|
+
- Manage task boundaries at the Workspace level
|
|
9
|
+
- Turn the Human's execution request into a clear task brief for the workflow that will own it
|
|
10
|
+
- Directly manage system configuration using management tools when requested
|
|
11
|
+
- Wait for real results and present them back to the Human
|
|
12
|
+
|
|
13
|
+
## Task Handoff
|
|
14
|
+
|
|
15
|
+
- Prefer the workflow-based control plane for execution work: `create_workflow` to open a task workspace with its bound Leader, `list_workflows` to inspect and reuse existing workspaces, and `delete_workflow` to remove a workspace that should no longer exist.
|
|
16
|
+
- When the Human asks to change a workflow's `allow_network` or `write_dirs`, use `set_permissions` to patch that workflow boundary directly.
|
|
17
|
+
- When a request requires real execution, choose or create the right workflow first, then hand the work to that workflow's Leader.
|
|
18
|
+
- Creating a workflow also creates its bound Leader. Do not leave a task workflow without a Leader.
|
|
19
|
+
- Do not directly design a workflow's internal Workflow Graph yourself. Once a workflow exists, that workflow's Leader owns its internal node creation, structure, and execution coordination.
|
|
20
|
+
- Do not directly assign execution work to a Worker or other ordinary task node as the default path. Even a simple execution task should enter the workflow through its Leader first.
|
|
21
|
+
- The first message you send to a Leader should be a task brief, not a raw copy of the Human's text. Include at least the task goal, expected artifact, success criteria, relevant context, constraints, and when the work should be escalated back to you for clarification.
|
|
22
|
+
- When continuing existing work, inspect the current workflows with `list_workflows` before creating a new one. Reuse the existing workflow when the Human is clearly referring to ongoing work.
|
|
23
|
+
- When the Human explicitly asks to remove a workflow or a finished workspace should be cleaned up, inspect with `list_workflows` and then use `delete_workflow`.
|
|
24
|
+
- After creating a new workflow, immediately dispatch the first task brief to its Leader with `send`.
|
|
25
|
+
- When a newly created or newly selected Leader is waiting for its next brief, keep using `send` until every intended Leader has been dispatched.
|
|
26
|
+
- Custom roles may also exist; choose them when the task clearly matches.
|
|
27
|
+
- Use `list_roles` when you need to inspect built-in or custom role details before choosing what to create.
|
|
28
|
+
|
|
29
|
+
## System Management
|
|
30
|
+
|
|
31
|
+
- You can manage system configuration directly without creating an agent
|
|
32
|
+
- When the Human asks about current system configuration or wants to change providers, roles, settings, or prompts, use the corresponding management tool directly
|
|
33
|
+
- When the Human asks to change an existing workflow's network or writable-directory boundary, use `set_permissions` directly instead of delegating that boundary change to the workflow's Leader
|
|
34
|
+
|
|
35
|
+
## Security Boundary
|
|
36
|
+
|
|
37
|
+
- Apply least privilege
|
|
38
|
+
- Only specify `write_dirs` when the task needs file writes, and keep them as narrow as possible
|
|
39
|
+
- Only set `allow_network=true` when the task needs network access
|
|
40
|
+
- Only grant the tools required for the task
|
|
41
|
+
|
|
42
|
+
## Workflow
|
|
43
|
+
|
|
44
|
+
1. Receive the Human's message
|
|
45
|
+
2. If the message is just casual conversation, a greeting, or common knowledge that needs no system interaction, answer directly without creating an agent
|
|
46
|
+
3. If the message is a system configuration request, use the corresponding management tool directly
|
|
47
|
+
4. If role, workflow, or tool availability is uncertain, use `list_roles`, `list_workflows`, and `list_tools` to inspect the current options before acting
|
|
48
|
+
5. Otherwise: open or choose a workflow and hand the execution brief to that workflow's Leader
|
|
49
|
+
6. Immediately send each new or newly selected Leader its next brief with `send`, including the concrete objective, expected output, relevant constraints, and escalation conditions
|
|
50
|
+
7. If a brief status update is helpful, keep it short and action-oriented, such as "正在查看"
|
|
51
|
+
8. After delegating, use `idle` to wait for messages from connected agents when you have no immediate next action
|
|
52
|
+
9. When a Leader reports back, present the real result to the Human
|
|
53
|
+
|
|
54
|
+
## Behavior Rules
|
|
55
|
+
|
|
56
|
+
- Do not personally execute system tasks
|
|
57
|
+
- Do not directly design or rewire a workflow's internal Workflow Graph once a Leader owns that workflow
|
|
58
|
+
- Do not explain internal Workflow Graph mechanics unless the Human explicitly asks
|
|
59
|
+
- Do not ask whether you should create a workflow and agents once that decision is clear; do it directly
|
|
60
|
+
- Do not invent results; wait for the delegated agent's real reply
|
|
61
|
+
- Do not re-send a task to a node that has already been dispatched or has already reported back
|
|
62
|
+
- Do not insert tool calls such as `contacts` between dispatch responses while some planned Leaders are still waiting for their next brief
|
|
63
|
+
- If the Human sends a new message while you are waiting, handle the new message instead of automatically idling again
|
|
64
|
+
"""
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any, Protocol
|
|
5
|
+
|
|
6
|
+
from flowent.models import LLMResponse, ModelInfo
|
|
7
|
+
from flowent.settings import ModelParams
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LLMProvider(Protocol):
|
|
11
|
+
def chat(
|
|
12
|
+
self,
|
|
13
|
+
messages: list[dict[str, Any]],
|
|
14
|
+
tools: list[dict[str, Any]] | None = None,
|
|
15
|
+
on_chunk: Callable[[str, str], None] | None = None,
|
|
16
|
+
register_interrupt: Callable[[Callable[[], None] | None], None] | None = None,
|
|
17
|
+
model_params: ModelParams | None = None,
|
|
18
|
+
) -> LLMResponse: ...
|
|
19
|
+
|
|
20
|
+
def list_models(
|
|
21
|
+
self,
|
|
22
|
+
register_interrupt: Callable[[Callable[[], None] | None], None] | None = None,
|
|
23
|
+
) -> list[ModelInfo]: ...
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from flowent.model_metadata import build_model_info
|
|
12
|
+
from flowent.models import LLMResponse, LLMUsage, ModelInfo
|
|
13
|
+
from flowent.models import ToolCallResult as ToolCall
|
|
14
|
+
from flowent.network import (
|
|
15
|
+
RequestException,
|
|
16
|
+
create_http_session,
|
|
17
|
+
read_response_text,
|
|
18
|
+
response_looks_like_html,
|
|
19
|
+
truncate_text,
|
|
20
|
+
)
|
|
21
|
+
from flowent.providers import LLMProvider
|
|
22
|
+
from flowent.providers.content import collapse_parts_to_text, to_anthropic_content
|
|
23
|
+
from flowent.providers.errors import (
|
|
24
|
+
build_access_blocked_error,
|
|
25
|
+
build_network_error,
|
|
26
|
+
build_status_error,
|
|
27
|
+
)
|
|
28
|
+
from flowent.providers.headers import merge_headers
|
|
29
|
+
from flowent.providers.sse import iter_sse_json
|
|
30
|
+
from flowent.settings import ModelParams
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _extract_usage_value(usage: dict[str, Any], key: str) -> int | None:
|
|
34
|
+
value = usage.get(key)
|
|
35
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
36
|
+
return None
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AnthropicProvider(LLMProvider):
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
provider_name: str,
|
|
44
|
+
api_base_url: str,
|
|
45
|
+
api_key: str = "",
|
|
46
|
+
headers: dict[str, str] | None = None,
|
|
47
|
+
model: str = "",
|
|
48
|
+
request_timeout_seconds: float = 120.0,
|
|
49
|
+
) -> None:
|
|
50
|
+
self._provider_name = provider_name
|
|
51
|
+
self._api_base_url = api_base_url.rstrip("/")
|
|
52
|
+
self._api_key = api_key
|
|
53
|
+
self._header_overrides = dict(headers or {})
|
|
54
|
+
self._model = model
|
|
55
|
+
self._request_timeout_seconds = request_timeout_seconds
|
|
56
|
+
self._client = create_http_session(timeout=self._request_timeout_seconds)
|
|
57
|
+
|
|
58
|
+
def _headers(self) -> dict[str, str]:
|
|
59
|
+
return merge_headers(
|
|
60
|
+
{
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
"x-api-key": self._api_key,
|
|
63
|
+
"anthropic-version": "2023-06-01",
|
|
64
|
+
},
|
|
65
|
+
self._header_overrides,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def _convert_messages(
|
|
69
|
+
self,
|
|
70
|
+
messages: list[dict[str, Any]],
|
|
71
|
+
) -> tuple[str | None, list[dict[str, Any]]]:
|
|
72
|
+
system_text: list[str] = []
|
|
73
|
+
converted: list[dict[str, Any]] = []
|
|
74
|
+
pending_tool_results: list[dict[str, Any]] = []
|
|
75
|
+
|
|
76
|
+
def flush_tool_results() -> None:
|
|
77
|
+
if not pending_tool_results:
|
|
78
|
+
return
|
|
79
|
+
converted.append(
|
|
80
|
+
{
|
|
81
|
+
"role": "user",
|
|
82
|
+
"content": list(pending_tool_results),
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
pending_tool_results.clear()
|
|
86
|
+
|
|
87
|
+
for msg in messages:
|
|
88
|
+
role = msg.get("role")
|
|
89
|
+
|
|
90
|
+
if role == "system":
|
|
91
|
+
content = msg.get("content")
|
|
92
|
+
if content is not None:
|
|
93
|
+
system_text.append(
|
|
94
|
+
content if isinstance(content, str) else json.dumps(content),
|
|
95
|
+
)
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
if role == "tool":
|
|
99
|
+
content = msg.get("content")
|
|
100
|
+
if content is None:
|
|
101
|
+
tool_content = ""
|
|
102
|
+
elif isinstance(content, str):
|
|
103
|
+
tool_content = content
|
|
104
|
+
else:
|
|
105
|
+
tool_content = json.dumps(content)
|
|
106
|
+
|
|
107
|
+
pending_tool_results.append(
|
|
108
|
+
{
|
|
109
|
+
"type": "tool_result",
|
|
110
|
+
"tool_use_id": msg.get("tool_call_id", ""),
|
|
111
|
+
"content": tool_content,
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
flush_tool_results()
|
|
117
|
+
|
|
118
|
+
if role == "assistant" and msg.get("tool_calls"):
|
|
119
|
+
content_blocks: list[dict[str, Any]] = []
|
|
120
|
+
content = msg.get("content")
|
|
121
|
+
assistant_text = collapse_parts_to_text(content)
|
|
122
|
+
if assistant_text:
|
|
123
|
+
content_blocks.append({"type": "text", "text": assistant_text})
|
|
124
|
+
|
|
125
|
+
for tool_call in msg.get("tool_calls", []):
|
|
126
|
+
fn = tool_call.get("function", {})
|
|
127
|
+
raw_arguments = fn.get("arguments", "{}")
|
|
128
|
+
|
|
129
|
+
if isinstance(raw_arguments, str):
|
|
130
|
+
try:
|
|
131
|
+
parsed_arguments = (
|
|
132
|
+
json.loads(raw_arguments) if raw_arguments else {}
|
|
133
|
+
)
|
|
134
|
+
except json.JSONDecodeError:
|
|
135
|
+
parsed_arguments = {}
|
|
136
|
+
elif isinstance(raw_arguments, dict):
|
|
137
|
+
parsed_arguments = raw_arguments
|
|
138
|
+
else:
|
|
139
|
+
parsed_arguments = {}
|
|
140
|
+
|
|
141
|
+
if not isinstance(parsed_arguments, dict):
|
|
142
|
+
parsed_arguments = {}
|
|
143
|
+
|
|
144
|
+
content_blocks.append(
|
|
145
|
+
{
|
|
146
|
+
"type": "tool_use",
|
|
147
|
+
"id": tool_call.get("id", ""),
|
|
148
|
+
"name": fn.get("name", ""),
|
|
149
|
+
"input": parsed_arguments,
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
converted.append({"role": "assistant", "content": content_blocks})
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
if role == "user":
|
|
157
|
+
converted.append(
|
|
158
|
+
{
|
|
159
|
+
"role": "user",
|
|
160
|
+
"content": to_anthropic_content(
|
|
161
|
+
msg.get("content"),
|
|
162
|
+
allow_images=True,
|
|
163
|
+
),
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
converted.append(
|
|
169
|
+
{"role": role, "content": collapse_parts_to_text(msg.get("content"))}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
flush_tool_results()
|
|
173
|
+
|
|
174
|
+
system = "\n\n".join(system_text) if system_text else None
|
|
175
|
+
return system, converted
|
|
176
|
+
|
|
177
|
+
def _convert_tools(self, tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
178
|
+
result = []
|
|
179
|
+
for tool in tools:
|
|
180
|
+
fn = tool.get("function", {})
|
|
181
|
+
result.append(
|
|
182
|
+
{
|
|
183
|
+
"name": fn.get("name", ""),
|
|
184
|
+
"description": fn.get("description", ""),
|
|
185
|
+
"input_schema": fn.get("parameters", {}),
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
def chat(
|
|
191
|
+
self,
|
|
192
|
+
messages: list[dict[str, Any]],
|
|
193
|
+
tools: list[dict[str, Any]] | None = None,
|
|
194
|
+
on_chunk: Callable[[str, str], None] | None = None,
|
|
195
|
+
register_interrupt: Callable[[Callable[[], None] | None], None] | None = None,
|
|
196
|
+
model_params: ModelParams | None = None,
|
|
197
|
+
) -> LLMResponse:
|
|
198
|
+
url = f"{self._api_base_url}/messages"
|
|
199
|
+
system, converted_messages = self._convert_messages(messages)
|
|
200
|
+
|
|
201
|
+
payload: dict[str, Any] = {
|
|
202
|
+
"model": self._model,
|
|
203
|
+
"messages": converted_messages,
|
|
204
|
+
"max_tokens": (
|
|
205
|
+
model_params.max_output_tokens
|
|
206
|
+
if model_params is not None
|
|
207
|
+
and model_params.max_output_tokens is not None
|
|
208
|
+
else 8192
|
|
209
|
+
),
|
|
210
|
+
"stream": True,
|
|
211
|
+
}
|
|
212
|
+
if model_params is not None:
|
|
213
|
+
if model_params.temperature is not None:
|
|
214
|
+
payload["temperature"] = model_params.temperature
|
|
215
|
+
if model_params.top_p is not None:
|
|
216
|
+
payload["top_p"] = model_params.top_p
|
|
217
|
+
if system:
|
|
218
|
+
payload["system"] = system
|
|
219
|
+
if tools:
|
|
220
|
+
payload["tools"] = self._convert_tools(tools)
|
|
221
|
+
|
|
222
|
+
logger.debug(
|
|
223
|
+
"[{}] Anthropic chat request: model={}, messages={}, tools={}",
|
|
224
|
+
self._provider_name,
|
|
225
|
+
self._model,
|
|
226
|
+
len(converted_messages),
|
|
227
|
+
len(tools) if tools else 0,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
t0 = time.perf_counter()
|
|
231
|
+
|
|
232
|
+
content_parts: list[str] = []
|
|
233
|
+
thinking_parts: list[str] = []
|
|
234
|
+
tool_calls_accum: dict[int, dict[str, Any]] = {}
|
|
235
|
+
current_block_idx = -1
|
|
236
|
+
event_count = 0
|
|
237
|
+
input_tokens: int | None = None
|
|
238
|
+
output_tokens: int | None = None
|
|
239
|
+
cached_input_tokens: int | None = None
|
|
240
|
+
cache_read_tokens: int | None = None
|
|
241
|
+
cache_write_tokens: int | None = None
|
|
242
|
+
usage_details: dict[str, int] = {}
|
|
243
|
+
raw_usage: dict[str, Any] | None = None
|
|
244
|
+
client = self._client
|
|
245
|
+
if register_interrupt is not None:
|
|
246
|
+
register_interrupt(client.close)
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
with client.stream(
|
|
250
|
+
"POST",
|
|
251
|
+
url,
|
|
252
|
+
headers=self._headers(),
|
|
253
|
+
json=payload,
|
|
254
|
+
) as response:
|
|
255
|
+
if register_interrupt is not None:
|
|
256
|
+
register_interrupt(response.close)
|
|
257
|
+
if response_looks_like_html(response):
|
|
258
|
+
raise build_access_blocked_error(
|
|
259
|
+
provider_name=self._provider_name,
|
|
260
|
+
provider_type="anthropic",
|
|
261
|
+
model=self._model,
|
|
262
|
+
base_url=self._api_base_url,
|
|
263
|
+
status_code=response.status_code,
|
|
264
|
+
detail=truncate_text(read_response_text(response)),
|
|
265
|
+
)
|
|
266
|
+
if response.status_code != 200:
|
|
267
|
+
body = truncate_text(read_response_text(response))
|
|
268
|
+
elapsed = time.perf_counter() - t0
|
|
269
|
+
logger.error(
|
|
270
|
+
"LLM API error [provider={}, model={}, type=anthropic]: {} - {} ({:.2f}s)",
|
|
271
|
+
self._provider_name,
|
|
272
|
+
self._model,
|
|
273
|
+
response.status_code,
|
|
274
|
+
body[:500],
|
|
275
|
+
elapsed,
|
|
276
|
+
)
|
|
277
|
+
raise build_status_error(
|
|
278
|
+
provider_name=self._provider_name,
|
|
279
|
+
provider_type="anthropic",
|
|
280
|
+
model=self._model,
|
|
281
|
+
base_url=self._api_base_url,
|
|
282
|
+
status_code=response.status_code,
|
|
283
|
+
body=body,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
for event in iter_sse_json(response):
|
|
287
|
+
event_count += 1
|
|
288
|
+
event_type = event.get("type", "")
|
|
289
|
+
|
|
290
|
+
if event_type == "message_start":
|
|
291
|
+
message = event.get("message", {})
|
|
292
|
+
usage = message.get("usage", {})
|
|
293
|
+
if isinstance(usage, dict):
|
|
294
|
+
raw_usage = dict(usage)
|
|
295
|
+
input_tokens = _extract_usage_value(usage, "input_tokens")
|
|
296
|
+
cache_read_tokens = _extract_usage_value(
|
|
297
|
+
usage, "cache_read_input_tokens"
|
|
298
|
+
)
|
|
299
|
+
cache_write_tokens = _extract_usage_value(
|
|
300
|
+
usage, "cache_creation_input_tokens"
|
|
301
|
+
)
|
|
302
|
+
cached_input_tokens = cache_read_tokens
|
|
303
|
+
if cached_input_tokens is None:
|
|
304
|
+
cached_input_tokens = cache_write_tokens
|
|
305
|
+
for key in (
|
|
306
|
+
"cache_creation_input_tokens",
|
|
307
|
+
"cache_read_input_tokens",
|
|
308
|
+
):
|
|
309
|
+
value = _extract_usage_value(usage, key)
|
|
310
|
+
if value is not None:
|
|
311
|
+
usage_details[key] = value
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
if event_type == "content_block_start":
|
|
315
|
+
current_block_idx += 1
|
|
316
|
+
block = event.get("content_block", {})
|
|
317
|
+
if block.get("type") == "tool_use":
|
|
318
|
+
tool_calls_accum[current_block_idx] = {
|
|
319
|
+
"id": block.get("id", str(uuid.uuid4())),
|
|
320
|
+
"name": block.get("name", ""),
|
|
321
|
+
"arguments": "",
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
elif event_type == "content_block_delta":
|
|
325
|
+
delta = event.get("delta", {})
|
|
326
|
+
delta_type = delta.get("type", "")
|
|
327
|
+
|
|
328
|
+
if delta_type == "text_delta":
|
|
329
|
+
text = delta.get("text", "")
|
|
330
|
+
if text:
|
|
331
|
+
content_parts.append(text)
|
|
332
|
+
if on_chunk:
|
|
333
|
+
on_chunk("content", text)
|
|
334
|
+
|
|
335
|
+
elif delta_type == "thinking_delta":
|
|
336
|
+
thinking = delta.get("thinking", "")
|
|
337
|
+
if thinking:
|
|
338
|
+
thinking_parts.append(thinking)
|
|
339
|
+
if on_chunk:
|
|
340
|
+
on_chunk("thinking", thinking)
|
|
341
|
+
|
|
342
|
+
elif delta_type == "input_json_delta":
|
|
343
|
+
partial = delta.get("partial_json", "")
|
|
344
|
+
if current_block_idx in tool_calls_accum:
|
|
345
|
+
tool_calls_accum[current_block_idx]["arguments"] += (
|
|
346
|
+
partial
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
elif event_type == "message_delta":
|
|
350
|
+
usage = event.get("usage", {})
|
|
351
|
+
if isinstance(usage, dict):
|
|
352
|
+
next_output_tokens = _extract_usage_value(
|
|
353
|
+
usage, "output_tokens"
|
|
354
|
+
)
|
|
355
|
+
if next_output_tokens is not None:
|
|
356
|
+
output_tokens = next_output_tokens
|
|
357
|
+
|
|
358
|
+
elif event_type == "message_stop":
|
|
359
|
+
break
|
|
360
|
+
except RequestException as exc:
|
|
361
|
+
elapsed = time.perf_counter() - t0
|
|
362
|
+
logger.warning(
|
|
363
|
+
"LLM API transport error [provider={}, model={}, type=anthropic]: {} ({:.2f}s)",
|
|
364
|
+
self._provider_name,
|
|
365
|
+
self._model,
|
|
366
|
+
exc,
|
|
367
|
+
elapsed,
|
|
368
|
+
)
|
|
369
|
+
raise build_network_error(
|
|
370
|
+
provider_name=self._provider_name,
|
|
371
|
+
provider_type="anthropic",
|
|
372
|
+
model=self._model,
|
|
373
|
+
base_url=self._api_base_url,
|
|
374
|
+
error=exc,
|
|
375
|
+
) from exc
|
|
376
|
+
finally:
|
|
377
|
+
close_client = getattr(client, "close", None)
|
|
378
|
+
if callable(close_client):
|
|
379
|
+
close_client()
|
|
380
|
+
self._client = create_http_session(
|
|
381
|
+
timeout=self._request_timeout_seconds
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
elapsed = time.perf_counter() - t0
|
|
385
|
+
content = "".join(content_parts) or None
|
|
386
|
+
thinking = "".join(thinking_parts) or None
|
|
387
|
+
usage = None
|
|
388
|
+
if input_tokens is not None or output_tokens is not None:
|
|
389
|
+
usage = LLMUsage(
|
|
390
|
+
total_tokens=(input_tokens or 0) + (output_tokens or 0),
|
|
391
|
+
input_tokens=input_tokens,
|
|
392
|
+
output_tokens=output_tokens,
|
|
393
|
+
cached_input_tokens=cached_input_tokens,
|
|
394
|
+
cache_read_tokens=cache_read_tokens,
|
|
395
|
+
cache_write_tokens=cache_write_tokens,
|
|
396
|
+
details=usage_details,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
logger.debug(
|
|
400
|
+
"[{}] Anthropic chat done: {:.2f}s, events={}, content_len={}, thinking_len={}, tool_calls={}",
|
|
401
|
+
self._provider_name,
|
|
402
|
+
elapsed,
|
|
403
|
+
event_count,
|
|
404
|
+
len(content) if content else 0,
|
|
405
|
+
len(thinking) if thinking else 0,
|
|
406
|
+
len(tool_calls_accum),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if tool_calls_accum:
|
|
410
|
+
tool_calls = []
|
|
411
|
+
for _, acc in sorted(tool_calls_accum.items()):
|
|
412
|
+
args_str = acc["arguments"]
|
|
413
|
+
try:
|
|
414
|
+
arguments = json.loads(args_str) if args_str else {}
|
|
415
|
+
except json.JSONDecodeError:
|
|
416
|
+
arguments = {}
|
|
417
|
+
tool_calls.append(
|
|
418
|
+
ToolCall(id=acc["id"], name=acc["name"], arguments=arguments),
|
|
419
|
+
)
|
|
420
|
+
return LLMResponse(
|
|
421
|
+
content=content,
|
|
422
|
+
tool_calls=tool_calls,
|
|
423
|
+
thinking=thinking,
|
|
424
|
+
usage=usage,
|
|
425
|
+
raw_usage=raw_usage,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
return LLMResponse(
|
|
429
|
+
content=content or "",
|
|
430
|
+
thinking=thinking,
|
|
431
|
+
usage=usage,
|
|
432
|
+
raw_usage=raw_usage,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
def list_models(
|
|
436
|
+
self,
|
|
437
|
+
register_interrupt: Callable[[Callable[[], None] | None], None] | None = None,
|
|
438
|
+
) -> list[ModelInfo]:
|
|
439
|
+
url = f"{self._api_base_url}/models"
|
|
440
|
+
client = self._client
|
|
441
|
+
close_client = getattr(client, "close", None)
|
|
442
|
+
if register_interrupt is not None and callable(close_client):
|
|
443
|
+
register_interrupt(close_client)
|
|
444
|
+
try:
|
|
445
|
+
resp = client.get(url, headers=self._headers())
|
|
446
|
+
resp.raise_for_status()
|
|
447
|
+
data = resp.json()
|
|
448
|
+
models = data.get("data", [])
|
|
449
|
+
return [
|
|
450
|
+
build_model_info(
|
|
451
|
+
provider_type="anthropic",
|
|
452
|
+
model_id=m["id"],
|
|
453
|
+
)
|
|
454
|
+
for m in models
|
|
455
|
+
]
|
|
456
|
+
except Exception as e:
|
|
457
|
+
logger.error(
|
|
458
|
+
"Failed to list models [provider={}, type=anthropic]: {}",
|
|
459
|
+
self._provider_name,
|
|
460
|
+
e,
|
|
461
|
+
)
|
|
462
|
+
return []
|
|
463
|
+
finally:
|
|
464
|
+
if callable(close_client):
|
|
465
|
+
close_client()
|
|
466
|
+
self._client = create_http_session(
|
|
467
|
+
timeout=self._request_timeout_seconds
|
|
468
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
PROVIDER_VERSION_SUFFIXES: Final[dict[str, str]] = {
|
|
6
|
+
"openai_compatible": "/v1",
|
|
7
|
+
"openai_responses": "/v1",
|
|
8
|
+
"anthropic": "/v1",
|
|
9
|
+
"gemini": "/v1beta",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
PROVIDER_REQUEST_PATHS: Final[dict[str, str]] = {
|
|
13
|
+
"openai_compatible": "/chat/completions",
|
|
14
|
+
"openai_responses": "/responses",
|
|
15
|
+
"anthropic": "/messages",
|
|
16
|
+
"gemini": "/models/{model}:streamGenerateContent",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_KNOWN_SUFFIXES: Final[tuple[str, ...]] = tuple(
|
|
20
|
+
sorted(set(PROVIDER_VERSION_SUFFIXES.values()), key=len, reverse=True)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _normalize_raw_base_url(base_url: str) -> str:
|
|
25
|
+
normalized = base_url.strip().rstrip("/")
|
|
26
|
+
if not normalized:
|
|
27
|
+
raise ValueError("Provider base_url is required")
|
|
28
|
+
return normalized
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _normalize_provider_type(provider_type: str) -> str:
|
|
32
|
+
normalized = provider_type.strip().lower()
|
|
33
|
+
if normalized not in PROVIDER_VERSION_SUFFIXES:
|
|
34
|
+
raise ValueError(f"Unknown provider type: {provider_type}")
|
|
35
|
+
return normalized
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def resolve_provider_base_url(provider_type: str, base_url: str) -> str:
|
|
39
|
+
normalized_type = _normalize_provider_type(provider_type)
|
|
40
|
+
normalized_base_url = _normalize_raw_base_url(base_url)
|
|
41
|
+
expected_suffix = PROVIDER_VERSION_SUFFIXES[normalized_type]
|
|
42
|
+
lower_base_url = normalized_base_url.lower()
|
|
43
|
+
|
|
44
|
+
for suffix in _KNOWN_SUFFIXES:
|
|
45
|
+
if not lower_base_url.endswith(suffix):
|
|
46
|
+
continue
|
|
47
|
+
if suffix != expected_suffix:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Provider base_url suffix '{suffix}' does not match type "
|
|
50
|
+
f"'{normalized_type}' (expected '{expected_suffix}')"
|
|
51
|
+
)
|
|
52
|
+
return normalized_base_url
|
|
53
|
+
|
|
54
|
+
return f"{normalized_base_url}{expected_suffix}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_provider_request_preview(provider_type: str, base_url: str) -> str:
|
|
58
|
+
normalized_type = _normalize_provider_type(provider_type)
|
|
59
|
+
resolved_base_url = resolve_provider_base_url(normalized_type, base_url)
|
|
60
|
+
return f"{resolved_base_url}{PROVIDER_REQUEST_PATHS[normalized_type]}"
|